Introduction
This library contains a full implementation of the IShellBrowser
interface. The implementation is used in a VS-like OpenFileDialog
and SaveFileDialog
implementation. The components can be used the same way the System.Windows.Forms.OpenFileDialog
and System.Windows.Forms.SaveFileDialog
are used. The library also contains the SelectFolderDialog
component which can be used as a replacement to the System.Windows.Forms.FolderBrowserDialog
.
Background
Ever since I first read the article, Implementing IShellBrowser to host IShellView, I wanted to implement the same thing using C#, but after several unsuccessful tries, I had to put it aside. But, when I really needed a customized implementation of the Open and Save file dialogs in one of my applications, and using templates wasn't sufficient, I had to retry one more time. So, after re-declaring my shell interfaces a couple of times, I finally succeeded. I decided to create this library and write this article to demonstrate the IShellBrowser
interface.
Using the Code
OpenFileDialog
and SaveFileDialog
are very similar to and contain almost the same properties as System.Windows.Forms.OpenFileDialog
and System.Windows.Forms.SaveFileDialog
respectively. This means that they can be used in almost the same way. The SelectFolderDialog
however doesn't look and act as the System.Windows.Forms.FolderBrowserDialog
but can still be used in a similar way.
All three components share the following two properties:
The Options
property controls the drop-down items of the OK split button. You can either add the strings using the Designer or by using code, see the following example:
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.Options.Add("Open");
openFileDialog1.Options.Add("Open As Read Only");
You can then access the selected option through the SelectedOptionIndex
property:
if (openFileDialog1.ShowDialog(this) == DialogResult.OK)
{
if (openFileDialog.SelectedOptionIndex == 1)
MessageBox.Show(this, "Open as read only selected");
}
The Places
property contains the collection of items shown in the places bar. By default it contains the Desktop, My Documents and My Computer. You can eiter change the items using the Designer or by code. It is very easy to customize the places bar using the Places Editor.
Or if you want to change the places bar programatically, see the following example:
openFileDialog1.Places.Add(new FileDialogPlace(SpecialFolder.Desktop));
openFileDialog1.Places.Add(new FileDialogPlace(SpecialFolder.MyDocuments));
openFileDialog1.Places.Add(new FileDialogPlace(SpecialFolder.MyComputer));
CustomFileDialogPlace customPlace1 = new CustomFileDialogPlace(
"C:\Documents and Settings\[User]\My Documents\Visual Studio 2005\Projects");
customPlace1.Text = "My Projects";
openFileDialog1.Places.Add(customPlace1);
Registry Support
The registry is used to store window position and file name MRU, but the code is wrapped in #if blocks. If you want to store the information in the registry, add the following code to the top of FileDialog.cs: #define REGISTRY_SUPPORT
.
How to Implement IShellBrowser
I'm now going to explain how to implement the IShellBrowser
in your own dialog. Start by including NativeMethods.cs (which is included in the source code) into your project, and perhaps change the namespace. Then, add the following attributes and inheritance to your form.
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public partial class Form1: Form, NativeMethods.IShellBrowser,
NativeMethods.IServiceProvider
{
...
}
The IShellBrowser
interface is used to host the IShellView
, and if we don't inherit IServiceProvider
, the IShellView
will open a new Explorer window every time we browse to a new folder.
Now, add the following member fields:
private NativeMethods.IShellView m_shellView;
private IntPtr m_hWndListView;
private NativeMethods.IShellFolder m_desktopFolder;
private NativeMethods.IShellFolder m_currentFolder;
private IntPtr m_pidlAbsCurrent;
private IntPtr m_desktopPidl;
private NativeMethods.FOLDERVIEWMODE m_viewMode = NativeMethods.FOLDERVIEWMODE.FVM_LIST;
private NativeMethods.FOLDERFLAGS m_flags = (NativeMethods.FOLDERFLAGS.FWF_SHOWSELALWAYS |
NativeMethods.FOLDERFLAGS.FWF_SINGLESEL |
NativeMethods.FOLDERFLAGS.FWF_NOWEBVIEW);
In the constructor, initialize the desktop PIDL and IShellFolder
.
public Form1()
{
InitializeComponent();
NativeMethods.Shell32.SHGetSpecialFolderLocation(IntPtr.Zero,
(int)SpecialFolder.Desktop, out m_desktopPidl);
IntPtr desktopFolderPtr;
NativeMethods.Shell32.SHGetDesktopFolder(out desktopFolderPtr);
m_desktopFolder = (NativeMethods.IShellFolder)
Marshal.GetObjectForIUnknown(desktopFolderPtr);
}
Now, it's time to implement the IShellBrowser
interface.
int NativeMethods.IShellBrowser.GetWindow(out IntPtr hwnd)
{
hwnd = Handle;
return NativeMethods.S_OK;
}
int NativeMethods.IShellBrowser.ContextSensitiveHelp(int fEnterMode)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.InsertMenusSB(IntPtr hmenuShared,
IntPtr lpMenuWidths)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.SetMenuSB(IntPtr hmenuShared,
IntPtr holemenuRes, IntPtr hwndActiveObject)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.RemoveMenusSB(IntPtr hmenuShared)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.SetStatusTextSB(IntPtr pszStatusText)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.EnableModelessSB(bool fEnable)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.TranslateAcceleratorSB(IntPtr pmsg, short wID)
{
return NativeMethods.S_OK;
}
int NativeMethods.IShellBrowser.BrowseObject(IntPtr pidl, uint wFlags)
{
int hr;
IntPtr folderTmpPtr;
NativeMethods.IShellFolder folderTmp;
IntPtr pidlTmp;
if (NativeMethods.Shell32.ILIsEqual(pidl, m_desktopPidl))
{
pidlTmp = m_desktopPidl;
folderTmp = m_desktopFolder;
}
else if ((wFlags & NativeMethods.SBSP_RELATIVE) != 0)
{
if ((hr = m_currentFolder.BindToObject(pidl, IntPtr.Zero,
ref NativeMethods.IID_IShellFolder,
out folderTmpPtr)) != NativeMethods.S_OK)
return hr;
pidlTmp = NativeMethods.Shell32.ILCombine(m_pidlAbsCurrent, pidl);
folderTmp = (NativeMethods.IShellFolder)
Marshal.GetObjectForIUnknown(folderTmpPtr);
}
else
{
pidlTmp = NativeMethods.Shell32.ILClone(pidl);
if ((hr = m_desktopFolder.BindToObject(pidlTmp, IntPtr.Zero,
ref NativeMethods.IID_IShellFolder,
out folderTmpPtr)) != NativeMethods.S_OK)
return hr;
folderTmp = (NativeMethods.IShellFolder)
Marshal.GetObjectForIUnknown(folderTmpPtr);
}
if (folderTmp == null)
{
NativeMethods.Shell32.ILFree(pidlTmp);
return NativeMethods.E_FAIL;
}
if (NativeMethods.Shell32.ILIsEqual(pidlTmp, m_pidlAbsCurrent))
{
Marshal.ReleaseComObject(folderTmp);
NativeMethods.Shell32.ILFree(pidlTmp);
return NativeMethods.S_OK;
}
m_currentFolder = folderTmp;
NativeMethods.FOLDERSETTINGS fs = new NativeMethods.FOLDERSETTINGS();
NativeMethods.IShellView lastIShellView = m_shellView;
if (lastIShellView != null)
lastIShellView.GetCurrentInfo(ref fs);
else
{
fs = new NativeMethods.FOLDERSETTINGS();
fs.fFlags = (uint)m_flags;
fs.ViewMode = (uint)m_viewMode;
}
IntPtr iShellViewPtr;
hr = folderTmp.CreateViewObject(Handle,
ref NativeMethods.IID_IShellView, out iShellViewPtr);
if (hr == NativeMethods.S_OK)
{
m_shellView = (NativeMethods.IShellView)
Marshal.GetObjectForIUnknown(iShellViewPtr);
m_hWndListView = IntPtr.Zero;
NativeMethods.RECT rc =
new NativeMethods.RECT(8, 8,
ClientSize.Width - 8,
ClientSize.Height - 8);
int res;
try
{
res = m_shellView.CreateViewWindow(lastIShellView, ref fs,
this, ref rc, ref m_hWndListView);
}
catch (COMException)
{
return NativeMethods.E_FAIL;
}
if (res < 0)
return NativeMethods.E_FAIL;
if (lastIShellView != null)
{
lastIShellView.GetCurrentInfo(ref fs);
lastIShellView.UIActivate((uint)
NativeMethods.SVUIA_STATUS.SVUIA_DEACTIVATE);
lastIShellView.DestroyViewWindow();
Marshal.ReleaseComObject(lastIShellView);
}
m_shellView.UIActivate((uint)
NativeMethods.SVUIA_STATUS.SVUIA_ACTIVATE_FOCUS);
m_pidlAbsCurrent = pidlTmp;
}
return NativeMethods.S_OK;
}
int NativeMethods.IShellBrowser.GetViewStateStream(uint grfMode, IntPtr ppStrm)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.GetControlWindow(uint id, out IntPtr phwnd)
{
phwnd = IntPtr.Zero;
return NativeMethods.S_FALSE;
}
int NativeMethods.IShellBrowser.SendControlMsg(uint id, uint uMsg,
uint wParam, uint lParam, IntPtr pret)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.QueryActiveShellView(
ref NativeMethods.IShellView ppshv)
{
Marshal.AddRef(Marshal.GetIUnknownForObject(m_shellView));
ppshv = m_shellView;
return NativeMethods.S_OK;
}
int NativeMethods.IShellBrowser.OnViewWindowActive(NativeMethods.IShellView pshv)
{
return NativeMethods.E_NOTIMPL;
}
int NativeMethods.IShellBrowser.SetToolbarItems(IntPtr lpButtons,
uint nButtons, uint uFlags)
{
return NativeMethods.E_NOTIMPL;
}
The only important methods in this simple implementation is GetWindow
, BrowseObject
, and QueryActiveShellView
. All the methods aren't necessary; also, note that the BrowseObject
method just contains the basic functionality. In the demo, the BrowseObject
contains more functionality such as go to parent.
Let's continue by implementing the IServiceProvider
interface.
int NativeMethods.IServiceProvider.QueryService(ref Guid guidService,
ref Guid riid, out NativeMethods.IShellBrowser ppvObject)
{
if (riid == NativeMethods.IID_IShellBrowser)
{
ppvObject = this;
return NativeMethods.S_OK;
}
ppvObject = null;
return NativeMethods.E_NOINTERFACE;
}
When you double-click on a folder, the IShellView
first calls IServiceProvier::QueryService()
for an IShellBrowser
interface, and it finds that the IShellBrowser::BrowseObject()
is invoked with the PIDL of the new folder and does nothing (i.e., waits for you to open the new folder). If you don't implement IServiceProvider
(or don't return an IShellBrowser
from QueryService
), IShellView
just launches a whole new Windows Explorer to display the new folder.
Now, the only thing that remains is the startup and cleanup.
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
((NativeMethods.IShellBrowser)this).BrowseObject(m_desktopPidl,
NativeMethods.SBSP_ABSOLUTE);
}
protected override void OnHandleDestroyed(EventArgs e)
{
m_pidlAbsCurrent = IntPtr.Zero;
if (m_shellView != null)
{
m_shellView.UIActivate((uint)NativeMethods.SVUIA_STATUS.SVUIA_DEACTIVATE);
m_shellView.DestroyViewWindow();
Marshal.ReleaseComObject(m_shellView);
m_shellView = null;
}
base.OnHandleDestroyed(e);
}
Points of Interest
One of the hardest things was to figure out which interfaces should be used and how they should be declared. I had to manually handle many things such as keyboard input and filtering. During the development, I found out the following:
- The
IShellBrowser.BrowseObject
method isn't called for 'My Documents'.
- The
WM_GETISHELLBROWSER
message is never sent.
History
- 30 Aug 2008
Initial posting.
- 24 Sep 2008
- Fixed some issues concerning the OK and Cancel buttons, when the Enter key was pressed.
- Fixed a simple bug causing the overwrite prompt to show twice.
- Fixed a problem with the auto-complete when using absolute paths.
- Added the
SelectFolderDialog
component which can be used as a more advanced FolderBrowserDialog
.
- 10 Oct 2008
- Updated article image.
- Fixed a problem when creating a new folder the traditional way.
- Fixed an issue in the
BrowseObject
method causing desktop pidl to be freed.
- After comparing with Windows and the VS open file dialog, I removed the part in the
BrowseObject
method where i was checking if I had a new pidl.
- Fixed an issue with some keys (Enter, Escape, Delete, Left, Up, Right, Down) when renaming an item.
- 5 May 2009
- Added support for shortcuts.
- Fixed some small bugs when saving.
- Fixed issue concerning the Enter key.
- Replaced the OK Button with a SplitButton control.
- Disabled the selection of special folders in the SelectFolderDialog.
- Added the ability to customize the places in the places bar through a dialog.
- Removed properties
CheckFileExists
and CheckPathExists
because neihter of them affects the behavior of the FileDialog.
- Changed the behavior of the
FileName
property.