65.9K
CodeProject is changing. Read more.
Home

Extended Folder Browser

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (16 votes)

Jun 14, 2006

3 min read

viewsIcon

141185

downloadIcon

3652

Adding functionality to a Folder Browser Dialog control.

Folder Browser with disabled 'Make New Folder' button

Introduction

Have you ever tried to change anything in the folder browser dialog of C#?

In this article, I will show you how you can make changes to the dialog. For example, what if we want to disable the ‘Make New Folder’ button every time a CD drive is selected? Or what if we want the user to select only from existing folders, except one folder that is to be allowed to make child folders?

Background

.NET has a FolderBrowserDialog control which is, basically, a wrapper for the Win32 function SHBrowseForFolder. The only issue here is that this class is a sealed class, so we can’t derive from it and add functionality. This control doesn’t provide any event we can register to and execute our code there. So basically, if you are satisfied with the control as it is, then you are OK. But once you need to do a small change, you have to figure out how the control really works.

In this article, we will see, step by step, how we can disable the ‘Make New Folder’ every time a CD drive is selected.

To achieve our goal, we will have to overcome two main issues:

  1. Getting the control to fire an event every time a selection is changed.
  2. Disabling the button.

Using the code

The first thing we are going to do is create an ExtendedFolderBrowser class. This class will hold a private class, InternalFolderBrowser, which will be derived from CommonDialog. InternalFolderBrowser is, basically, an implementation of the folder browser dialog, but this implementation will allow us to extend the functionality and add our new add-ins in the parent class. When deriving from the CommonDialog class, we need to implement two methods: Reset and Rundialog.

private class InternalFolderBrowser : CommonDialog
{
    private string m_selectedPath = null;
    private Environment.SpecialFolder m_rootFolder;
    public event EventHandler SelectedFolderChanged;
    private string m_descriptionText = String.Empty;
    
    public override void Reset()
    {
        m_rootFolder = Environment.SpecialFolder.Desktop;
        m_selectedPath = string.Empty;
    }

    protected override bool RunDialog(System.IntPtr hwndOwner)
    {
        IntPtr ptr1 = IntPtr.Zero;
        bool flag1 = false;
        Win32API.SHGetSpecialFolderLocation(hwndOwner, 
                         (int)m_rootFolder, ref ptr1);
        if (ptr1 == IntPtr.Zero)
        {
            Win32API.SHGetSpecialFolderLocation(hwndOwner, 0, ref ptr1);
            if (ptr1 == IntPtr.Zero)
            {
                throw new Exception("FolderBrowserDialogNoRootFolder");
            }
        }

        //Initialize the OLE to current thread.
        Application.OleRequired();
        IntPtr ptr2 = IntPtr.Zero;
        try
        {
            Win32API.BROWSEINFO browseinfo1 = new Win32API.BROWSEINFO();
            IntPtr ptr3 = Marshal.AllocHGlobal((int) 
                          (260 * Marshal.SystemDefaultCharSize));
            IntPtr ptr4 = Marshal.AllocHGlobal((int) 
                          (260 * Marshal.SystemDefaultCharSize));
            Win32API.BrowseCallbackProc proc1 = 
              new Win32API.BrowseCallbackProc(
              this.FolderBrowserDialog_BrowseCallbackProc);
            browseinfo1.pidlRoot = ptr1;
            browseinfo1.hwndOwner = hwndOwner;
            browseinfo1.pszDisplayName = ptr3;
            browseinfo1.lpszTitle = m_descriptionText;
            browseinfo1.ulFlags = 0x40;
            browseinfo1.lpfn = proc1;
            browseinfo1.lParam = IntPtr.Zero;
            browseinfo1.iImage = 0;
            ptr2 = Win32API.SHBrowseForFolder(browseinfo1);

            string s = Marshal.PtrToStringAuto(ptr3);

            if (ptr2 != IntPtr.Zero)
            {
                Win32API.SHGetPathFromIDList(ptr2, ptr4);
                this.m_selectedPath = Marshal.PtrToStringAuto(ptr4);
                Marshal.FreeHGlobal(ptr4);
                Marshal.FreeHGlobal(ptr3);
                flag1 = true;
            }
        }
        finally
        {
            Win32API.IMalloc malloc1 = GetSHMalloc();
            malloc1.Free(ptr1);
            if (ptr2 != IntPtr.Zero)
            {
                malloc1.Free(ptr2);
            }
        }
        return flag1;
    }

When we create the BROWSEINFO struct, we pass a delegate to the function that will be called on every change in the control:

private int FolderBrowserDialog_BrowseCallbackProc(IntPtr hwnd, 
                         int msg, IntPtr lParam, IntPtr lpData)
{
    switch (msg)
    {
        case Win32API.BFFM_INITIALIZED:
            if (m_selectedPath != string.Empty)
            {
                Win32API.SendMessage(new HandleRef(null, hwnd), 
                                     0x467, 1, m_selectedPath);
            }
            break;

        case Win32API.BFFM_SELCHANGED: //Selction Changed
        {
            IntPtr ptr1 = lParam;
            if (ptr1 != IntPtr.Zero)
            {
                IntPtr ptr2 = Marshal.AllocHGlobal((int) 
                              (260 * Marshal.SystemDefaultCharSize));
                bool flag1 = Win32API.SHGetPathFromIDList(ptr1, ptr2);
                m_selectedPath = Marshal.PtrToStringAuto(ptr2);

                //Fire Event
                if(SelectedFolderChanged != null)
                {
                    SelectedFolderChanged(this,null);
                }
                Marshal.FreeHGlobal(ptr2);
                Win32API.SendMessage2(new HandleRef(null, hwnd), 
                                      0x465, 0, flag1 ? 1 : 0);
            }
            break;
        }
    }
    return 0;
}

Every time a selection is changed, we will be notified by the BFFM_SELCHANGED message. Once we receive a message, we will fire an event so our parent class, ExtendedFolderBrowser, will be notified too.

The last thing we should do is register the event of the inner class and disable the button.

When the selection changes, we will execute a CheckState method:

/// <summary>
/// Check if we should disable the 'Make New Folder' button
/// </summary>
private void CheckState()
{
    if(m_ShowNewButtonHandler != null)
    {
        if(m_ShowNewButtonHandler(SelectedPath))
        {
            //Disabel the button
            UpdateButtonState(GetButtonHandle(IntPtr.Zero), false);
        }
        else
        {
            //Enable the button
            UpdateButtonState(GetButtonHandle(IntPtr.Zero),true);
        }
    }
}

m_ShowNewButtonHandler is a delegate to the user function that will check if we should disable the button according to the current selection. When a client creates a dialog, it will pass a delegate to its function to make a decision about the state of the button.

To change the state of the button, we simply use the EnableWindow API:

private void UpdateButtonState(IntPtr handle, bool state)
{
    if(handle != IntPtr.Zero)
    {
        Win32API.EnableWindow(handle,state);
    }
}

The last tricky thing left to do is to get the handle for the button. To get the handle, we will search for the class name "#32770", this is the class of the folder browser dialog. The problem is that this class name is not unique, so we have to make another check whether this handle we found is actually from the same thread that we are running. If it is from the same thread, bingo!, we have found the handler for the dialog control, otherwise we will continue searching.

Once we have found the dialog handle, all that is left is to return the first child handle of the Button type. This will be the handle to the 'Make New Folder" button.

private bool isFromTheSameThread(IntPtr windowHandle)
{
    //Get the thread that running given handler
    IntPtr activeThreadID = Win32API.GetWindowThreadProcessId(windowHandle, 
                                                              IntPtr.Zero); 

    //Get current thread
    int currentThread = AppDomain.GetCurrentThreadId();
    
    return (currentThread == activeThreadID.ToInt32());

}
        
private IntPtr GetButtonHandle(IntPtr handle)
{
    //First time
    if(handle == IntPtr.Zero)
    {
        //Get Handle for class with name "#32770"
        IntPtr parent = Win32API.FindWindow(BROWSE_FOR_FOLDER_CLASS_NAME,null);

        //If the window we found is in the same thread we are running
        //then it is The 'Browse For Folder' Dialog, otherwise keep searching
        if(!isFromTheSameThread(parent))
        {
            //Keep searching from this point
            return GetButtonHandle(parent);
        }
        else
        {
            return   Win32API.FindWindowEx(parent,IntPtr.Zero,"Button", null);
        }
    }
    else
    {
        //Find next window
        IntPtr parent = Win32API.FindWindowEx(IntPtr.Zero, 
                        handle,BROWSE_FOR_FOLDER_CLASS_NAME, null);
        if(!isFromTheSameThread(parent))
        {
            return GetButtonHandle(parent);
        }
        else
        {
            //We found the 'Browse For Folder' Dialog handler.
            //Lets return the child handler of 'Maker new Folder' button
            return   Win32API.FindWindowEx(parent,IntPtr.Zero,"Button", null);
        }
    }
}

The last thing I want to show you is how to use this control:

ExtendedFolderBrowser m_ExtendedFolderBrowser = 
                      new ExtendedFolderBrowser();

m_ExtendedFolderBrowser.Description = "Folder Browser";

//Create a hanlder to a function which will check 
//if to show the 'Make New Button' button
ShowNewButtonHandler handler = new ShowNewButtonHandler(IsCDDrive);
//Set the handler
m_ExtendedFolderBrowser.SetNewFolderButtonCondition = handler;

m_ExtendedFolderBrowser.ShowDialog();