Full implementation of IShellBrowser
A VS-like open and save file dialog implementation.
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:
Options
Places
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; // The current IShellView
private IntPtr m_hWndListView; // The handle of the listview
private NativeMethods.IShellFolder m_desktopFolder; // The desktop IShellFolder
private NativeMethods.IShellFolder m_currentFolder; // The current IShellFolder
private IntPtr m_pidlAbsCurrent; // The current absolute pidl
private IntPtr m_desktopPidl; // The desktop pidl
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.
// Return the window handle.
int NativeMethods.IShellBrowser.GetWindow(out IntPtr hwnd)
{
hwnd = Handle;
return NativeMethods.S_OK;
}
int NativeMethods.IShellBrowser.ContextSensitiveHelp(int fEnterMode)
{
return NativeMethods.E_NOTIMPL;
}
// Allows the container to insert its menu groups into the composite menu
// that is displayed when an extended namespace is being viewed or used.
int NativeMethods.IShellBrowser.InsertMenusSB(IntPtr hmenuShared,
IntPtr lpMenuWidths)
{
return NativeMethods.E_NOTIMPL;
}
// Installs the composite menu in the view window.
int NativeMethods.IShellBrowser.SetMenuSB(IntPtr hmenuShared,
IntPtr holemenuRes, IntPtr hwndActiveObject)
{
return NativeMethods.E_NOTIMPL;
}
// Permits the container to remove any of its menu elements from the
// in-place composite menu and to free all associated resources.
int NativeMethods.IShellBrowser.RemoveMenusSB(IntPtr hmenuShared)
{
return NativeMethods.E_NOTIMPL;
}
// Sets and displays status text about the in-place object in the
// container's frame-window status bar.
int NativeMethods.IShellBrowser.SetStatusTextSB(IntPtr pszStatusText)
{
return NativeMethods.E_NOTIMPL;
}
// Tells Microsoft Windows Explorer
// to enable or disable its modeless dialog boxes.
int NativeMethods.IShellBrowser.EnableModelessSB(bool fEnable)
{
return NativeMethods.E_NOTIMPL;
}
// Translates accelerator keystrokes intended
// for the browser's frame while the view is active.
int NativeMethods.IShellBrowser.TranslateAcceleratorSB(IntPtr pmsg, short wID)
{
return NativeMethods.S_OK;
}
// Informs Microsoft Windows Explorer to browse to another folder.
int NativeMethods.IShellBrowser.BrowseObject(IntPtr pidl, uint wFlags)
{
int hr;
IntPtr folderTmpPtr;
NativeMethods.IShellFolder folderTmp;
IntPtr pidlTmp;
if (NativeMethods.Shell32.ILIsEqual(pidl, m_desktopPidl))
{
// pidl is desktop folder
pidlTmp = m_desktopPidl;
folderTmp = m_desktopFolder;
}
else if ((wFlags & NativeMethods.SBSP_RELATIVE) != 0)
{
// SBSP_RELATIVE - pidl is relative from the current folder
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
{
// SBSP_ABSOLUTE - pidl is an absolute pidl (relative from desktop)
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;
}
// Check that we have a new pidl
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);
// Copy the old folder settings
else
{
fs = new NativeMethods.FOLDERSETTINGS();
fs.fFlags = (uint)m_flags;
fs.ViewMode = (uint)m_viewMode;
}
// Create the IShellView
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
{
// Create the actual list view
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;
// Release the old IShellView
if (lastIShellView != null)
{
lastIShellView.GetCurrentInfo(ref fs);
lastIShellView.UIActivate((uint)
NativeMethods.SVUIA_STATUS.SVUIA_DEACTIVATE);
lastIShellView.DestroyViewWindow();
Marshal.ReleaseComObject(lastIShellView);
}
// Set focus to the IShellView
m_shellView.UIActivate((uint)
NativeMethods.SVUIA_STATUS.SVUIA_ACTIVATE_FOCUS);
m_pidlAbsCurrent = pidlTmp;
}
return NativeMethods.S_OK;
}
// This method is used to save and restore the persistent state for a view
// (the icon positions, the column widths,
// and the current scroll position, for example).
int NativeMethods.IShellBrowser.GetViewStateStream(uint grfMode, IntPtr ppStrm)
{
return NativeMethods.E_NOTIMPL;
}
// GetControlWindow is used so views
// can directly manipulate the browser's controls.
int NativeMethods.IShellBrowser.GetControlWindow(uint id, out IntPtr phwnd)
{
phwnd = IntPtr.Zero;
return NativeMethods.S_FALSE;
}
// Sends control messages to either the toolbar or the status bar
// in a Microsoft Windows Explorer window.
int NativeMethods.IShellBrowser.SendControlMsg(uint id, uint uMsg,
uint wParam, uint lParam, IntPtr pret)
{
return NativeMethods.E_NOTIMPL;
}
// Retrieves the currently active (displayed) Shell view object.
int NativeMethods.IShellBrowser.QueryActiveShellView(
ref NativeMethods.IShellView ppshv)
{
Marshal.AddRef(Marshal.GetIUnknownForObject(m_shellView));
ppshv = m_shellView;
return NativeMethods.S_OK;
}
// This method informs the browser that the view is getting the focus
// (when the mouse is clicked on the view, for example).
int NativeMethods.IShellBrowser.OnViewWindowActive(NativeMethods.IShellView pshv)
{
return NativeMethods.E_NOTIMPL;
}
// Adds toolbar items to Microsoft Windows Explorer's toolbar.
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;
// Release the IShellView
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 advancedFolderBrowserDialog
.
- 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
andCheckPathExists
because neihter of them affects the behavior of the FileDialog. - Changed the behavior of the
FileName
property.