Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Full implementation of IShellBrowser

0.00/5 (No votes)
5 May 2009 1  
A VS-like open and save file dialog implementation.

FileDialogs.png

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.

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 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here