Show detailed Folder Browser from a PropertyGrid
Asked Answered
A

3

20

Please note it is not a duplicate question.

How to show a detailed FolderBrowser as in the image below from a PropertyGrid (from the field/property which has ellipses ...) Detailed Folder Browser

Using

[EditorAttribute(typeof(System.Windows.Forms.Design.FileNameEditor), typeof(System.Drawing.Design.UITypeEditor))]

[EditorAttribute(typeof(System.Windows.Forms.Design.FolderNameEditor), typeof(System.Drawing.Design.UITypeEditor))]

We get the minimalistic folder browser

Minimalistic Folder Browser

Archaeozoic answered 12/3, 2013 at 18:5 Comment(5)
perhaps this Stackovrflow post can led you some ideas #1859460Mckenziemckeon
[EditorAttribute(typeof(FileNameEditor), typeof(UITypeEditor))] something like this should work to and you will need to reference System.Design.dllMckenziemckeon
What's the problem with FileNameEditor? you do get a dialog box like the one you showPleura
@SimonMourier the FileNameEditor finds a file, the requirement is to have a folder path so the dialog box should be for "select folder", or is there a property that I am ignoring here!Archaeozoic
@DJKRAZE To get to System.Design.dll, you need to be targeting .net framework 4.0 full, not 4.0 client profileDulaney
P
47

Here is a custom UITypeEditor that allows you to use the Vista Folder Browser:

enter image description here

You can use it like any other editor:

[EditorAttribute(typeof(FolderNameEditor2), typeof(System.Drawing.Design.UITypeEditor))]

It relies on a custom FolderBrowser2 class that I have written for the occasion. Of course, this will work only on Windows Vista and higher. On previous Windows version, there is no other folder browser than the simple one.

public class FolderNameEditor2 : UITypeEditor
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        FolderBrowser2 browser = new FolderBrowser2();
        if (value != null)
        {
            browser.DirectoryPath = string.Format("{0}", value);
        }

        if (browser.ShowDialog(null) == DialogResult.OK)
            return browser.DirectoryPath;

        return value;
    }
}

public class FolderBrowser2
{
    public string DirectoryPath { get; set; }

    public DialogResult ShowDialog(IWin32Window owner)
    {
        IntPtr hwndOwner = owner != null ? owner.Handle : GetActiveWindow();

        IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialog();
        try
        {
            IShellItem item;
            if (!string.IsNullOrEmpty(DirectoryPath))
            {
                IntPtr idl;
                uint atts = 0;
                if (SHILCreateFromPath(DirectoryPath, out idl, ref atts) == 0)
                {
                    if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)
                    {
                        dialog.SetFolder(item);
                    }
                    Marshal.FreeCoTaskMem(idl);
                }
            }
            dialog.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);
            uint hr = dialog.Show(hwndOwner);
            if (hr == ERROR_CANCELLED)
                return DialogResult.Cancel;

            if (hr != 0)
                return DialogResult.Abort;

            dialog.GetResult(out item);
            string path;
            item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
            DirectoryPath = path;
            return DialogResult.OK;
        }
        finally
        {
            Marshal.ReleaseComObject(dialog);
        }
    }

    [DllImport("shell32.dll")]
    private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

    [DllImport("shell32.dll")]
    private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);

    [DllImport("user32.dll")]
    private static extern IntPtr GetActiveWindow();

    private const uint ERROR_CANCELLED = 0x800704C7;

    [ComImport]
    [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
    private class FileOpenDialog
    {
    }

    [ComImport]
    [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IFileOpenDialog
    {
        [PreserveSig]
        uint Show([In] IntPtr parent); // IModalWindow
        void SetFileTypes();  // not fully defined
        void SetFileTypeIndex([In] uint iFileType);
        void GetFileTypeIndex(out uint piFileType);
        void Advise(); // not fully defined
        void Unadvise();
        void SetOptions([In] FOS fos);
        void GetOptions(out FOS pfos);
        void SetDefaultFolder(IShellItem psi);
        void SetFolder(IShellItem psi);
        void GetFolder(out IShellItem ppsi);
        void GetCurrentSelection(out IShellItem ppsi);
        void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
        void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
        void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
        void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
        void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
        void GetResult(out IShellItem ppsi);
        void AddPlace(IShellItem psi, int alignment);
        void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
        void Close(int hr);
        void SetClientGuid();  // not fully defined
        void ClearClientData();
        void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
        void GetResults([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenum); // not fully defined
        void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppsai); // not fully defined
    }

    [ComImport]
    [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItem
    {
        void BindToHandler(); // not fully defined
        void GetParent(); // not fully defined
        void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
        void GetAttributes();  // not fully defined
        void Compare();  // not fully defined
    }

    private enum SIGDN : uint
    {
        SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
        SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
        SIGDN_FILESYSPATH = 0x80058000,
        SIGDN_NORMALDISPLAY = 0,
        SIGDN_PARENTRELATIVE = 0x80080001,
        SIGDN_PARENTRELATIVEEDITING = 0x80031001,
        SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
        SIGDN_PARENTRELATIVEPARSING = 0x80018001,
        SIGDN_URL = 0x80068000
    }

    [Flags]
    private enum FOS
    {
        FOS_ALLNONSTORAGEITEMS = 0x80,
        FOS_ALLOWMULTISELECT = 0x200,
        FOS_CREATEPROMPT = 0x2000,
        FOS_DEFAULTNOMINIMODE = 0x20000000,
        FOS_DONTADDTORECENT = 0x2000000,
        FOS_FILEMUSTEXIST = 0x1000,
        FOS_FORCEFILESYSTEM = 0x40,
        FOS_FORCESHOWHIDDEN = 0x10000000,
        FOS_HIDEMRUPLACES = 0x20000,
        FOS_HIDEPINNEDPLACES = 0x40000,
        FOS_NOCHANGEDIR = 8,
        FOS_NODEREFERENCELINKS = 0x100000,
        FOS_NOREADONLYRETURN = 0x8000,
        FOS_NOTESTFILECREATE = 0x10000,
        FOS_NOVALIDATE = 0x100,
        FOS_OVERWRITEPROMPT = 2,
        FOS_PATHMUSTEXIST = 0x800,
        FOS_PICKFOLDERS = 0x20,
        FOS_SHAREAWARE = 0x4000,
        FOS_STRICTFILETYPES = 4
    }
}
Pleura answered 13/3, 2013 at 13:40 Comment(6)
Holy crap, this is just what I needed for a project I'm working on, and it's been REALLY hard to find. I'm mercilessly stealing a tweaked version of this to permanently go in my code snippet repertoire. Thanks! One quick suggestion: You might want to add code to make it degrade gracefully to a FolderBrowserDialog if the Vista-style dialog isn't available.Ahmedahmedabad
Anyone, Does this code work in Win7, Win8 and Win10 (both 32 and 64-bit versions)?Culpa
@Culpa - yes it does unless proven otherwisePleura
Works on win 7 32bit and win 10 64 bit. Nice c# no hacking. Copy / paste very happy. thanks :-)Covet
string get by IShellItem::GetDisplayName needs to be freed by CoTaskMemFree. learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/…Cavefish
@Cavefish - no, since the buffer is allocated with the COM allocator, declaring it as a string will cause the CLR to automatically calls CoTaskMemFree on the internal buffer. If it was declared as IntPtr, it would be needed.Pleura
F
4

Here's a solution that does the same job as Simon Mourier's answer, without directly using interop (.Net takes care of that for you). It has the additional feature of falling back to the pre-Vista dialog if not in a high enough Windows version. Should work in Windows 7, 8, 10 and higher (theoretically).

There is one big caveat: Microsoft is free to change their internal classes at will. Since this uses Reflection to defeat scoping rules and this action is not supported (Microsoft is only interested in what's publicly exposed), this code could break if Microsoft does make changes.

using System;
using System.Reflection;
using System.Windows.Forms;

namespace ErikE.Shuriken {
    /// <summary>
    /// Present the Windows Vista-style open file dialog to select a folder. Fall back for older Windows Versions
    /// </summary>
    public class FolderSelectDialog {
        private string _initialDirectory;
        private string _title;
        private string _fileName = "";

        public string InitialDirectory {
            get { return string.IsNullOrEmpty(_initialDirectory) ? Environment.CurrentDirectory : _initialDirectory; }
            set { _initialDirectory = value; }
        }
        public string Title {
            get { return _title ?? "Select a folder"; }
            set { _title = value; }
        }
        public string FileName { get { return _fileName; } }

        public bool Show() { return Show(IntPtr.Zero); }

        /// <param name="hWndOwner">Handle of the control or window to be the parent of the file dialog</param>
        /// <returns>true if the user clicks OK</returns>
        public bool Show(IntPtr hWndOwner) {
            var result = Environment.OSVersion.Version.Major >= 6
                ? VistaDialog.Show(hWndOwner, InitialDirectory, Title)
                : ShowXpDialog(hWndOwner, InitialDirectory, Title);
            _fileName = result.FileName;
            return result.Result;
        }

        private struct ShowDialogResult {
            public bool Result { get; set; }
            public string FileName { get; set; }
        }

        private static ShowDialogResult ShowXpDialog(IntPtr ownerHandle, string initialDirectory, string title) {
            var folderBrowserDialog = new FolderBrowserDialog {
                Description = title,
                SelectedPath = initialDirectory,
                ShowNewFolderButton = false
            };
            var dialogResult = new ShowDialogResult();
            if (folderBrowserDialog.ShowDialog(new WindowWrapper(ownerHandle)) == DialogResult.OK) {
                dialogResult.Result = true;
                dialogResult.FileName = folderBrowserDialog.SelectedPath;
            }
            return dialogResult;
        }

        private static class VistaDialog {
            private const string c_foldersFilter = "Folders|\n";

            private const BindingFlags c_flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
            private readonly static Assembly s_windowsFormsAssembly = typeof(FileDialog).Assembly;
            private readonly static Type s_iFileDialogType = s_windowsFormsAssembly.GetType("System.Windows.Forms.FileDialogNative+IFileDialog");
            private readonly static MethodInfo s_createVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("CreateVistaDialog", c_flags);
            private readonly static MethodInfo s_onBeforeVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("OnBeforeVistaDialog", c_flags);
            private readonly static MethodInfo s_getOptionsMethodInfo = typeof(FileDialog).GetMethod("GetOptions", c_flags);
            private readonly static MethodInfo s_setOptionsMethodInfo = s_iFileDialogType.GetMethod("SetOptions", c_flags);
            private readonly static uint s_fosPickFoldersBitFlag = (uint) s_windowsFormsAssembly
                .GetType("System.Windows.Forms.FileDialogNative+FOS")
                .GetField("FOS_PICKFOLDERS")
                .GetValue(null);
            private readonly static ConstructorInfo s_vistaDialogEventsConstructorInfo = s_windowsFormsAssembly
                .GetType("System.Windows.Forms.FileDialog+VistaDialogEvents")
                .GetConstructor(c_flags, null, new[] { typeof(FileDialog) }, null);
            private readonly static MethodInfo s_adviseMethodInfo = s_iFileDialogType.GetMethod("Advise");
            private readonly static MethodInfo s_unAdviseMethodInfo = s_iFileDialogType.GetMethod("Unadvise");
            private readonly static MethodInfo s_showMethodInfo = s_iFileDialogType.GetMethod("Show");

            public static ShowDialogResult Show(IntPtr ownerHandle, string initialDirectory, string title) {
                var openFileDialog = new OpenFileDialog {
                    AddExtension = false,
                    CheckFileExists = false,
                    DereferenceLinks = true,
                    Filter = c_foldersFilter,
                    InitialDirectory = initialDirectory,
                    Multiselect = false,
                    Title = title
                };

                var iFileDialog = s_createVistaDialogMethodInfo.Invoke(openFileDialog, new object[] { });
                s_onBeforeVistaDialogMethodInfo.Invoke(openFileDialog, new[] { iFileDialog });
                s_setOptionsMethodInfo.Invoke(iFileDialog, new object[] { (uint) s_getOptionsMethodInfo.Invoke(openFileDialog, new object[] { }) | s_fosPickFoldersBitFlag });
                var adviseParametersWithOutputConnectionToken = new[] { s_vistaDialogEventsConstructorInfo.Invoke(new object[] { openFileDialog }), 0U };
                s_adviseMethodInfo.Invoke(iFileDialog, adviseParametersWithOutputConnectionToken);

                try {
                    int retVal = (int) s_showMethodInfo.Invoke(iFileDialog, new object[] { ownerHandle });
                    return new ShowDialogResult {
                        Result = retVal == 0,
                        FileName = openFileDialog.FileName
                    };
                }
                finally {
                    s_unAdviseMethodInfo.Invoke(iFileDialog, new[] { adviseParametersWithOutputConnectionToken[1] });
                }
            }
        }

        // Wrap an IWin32Window around an IntPtr
        private class WindowWrapper : IWin32Window {
            private readonly IntPtr _handle;
            public WindowWrapper(IntPtr handle) { _handle = handle; }
            public IntPtr Handle { get { return _handle; } }
        }
    }
}

I developed this as a cleaned up version of .NET Win 7-style folder select dialog by Bill Seddon of lyquidity.com (I have no affiliation). I wrote my own because his solution requires an additional Reflection class that isn't needed for this focused purpose, uses exception-based flow control, doesn't cache the results of its reflection calls. Note that the nested static VistaDialog class is so that its static reflection variables don't try to get populated if the Show method is never called.

It is used like so in a Windows Form:

var dialog = new FolderSelectDialog {
    InitialDirectory = musicFolderTextBox.Text
    Title = "Select a folder to import music from"
};
if (dialog.Show(Handle)) {
    musicFolderTextBox.Text = dialog.FileName;
}

You can of course play around with its options and what properties it exposes. For example, it allows multiselect in the Vista-style dialog.

Federative answered 20/11, 2015 at 21:9 Comment(9)
@Philm Thank you for the comment. I have rejected the edit because the changes required to make it work are obvious, and for one thing, your edit removes the part of the example where the form's Handle is passed in so the dialog properly "binds" to the active application modally.Federative
This solution is awesome, and the best without building "own" dialogs in my eyes. Because the underlying dialog is from Windows itself, one could argue this could also be used in projects with restrictions using foreign code. Thanks to ErikE and other contributors!Eskilstuna
You are welcome! There is one big caveat: please note that it's possible for Microsoft to change the internal classes that this relies on. Since they aren't public, and this solution uses Reflection to "cheat" and sneak the usage of the internal classes, it is subject to breakage if anything were to change.Federative
Okay I'll turn my question into a statement. Only dispose unmanaged resources and classes that implement IDisposable. Setting variables to null is not usually required in C#, especially when they go out of scope quickly. The lifetime of your acquired resource determines if you should use a using or implement IDisposable yourself.Federative
I beg to differ: I don't claim to be an expert on this, but it is for sure: 1. No unmanaged class or resource itself "implements" IDisposable, so this is a contradiction. The opposite is true, so when accessing unmanaged resources, it is the classic case for implementing this in a wrapper class. 2. Setting objects to null is a very important detail, e.g. when working with Interop and Com classes. I needed it several times. What I assume: This is only necessary, when there is no clean wrapper implemented, using IDisposable, etc.Eskilstuna
Concerning dialog boxes I am unsure. When you open dialogs not bound to a form, they remain active. I do nothing for MessageBox, and I am quite sure, it is not a big deal for other dialogs, too. It think you are right, it is enough when the dialog objects go out of scope, but when unmanaged resources are involved, I am not 100% sure about cleanness.Eskilstuna
You're not understanding my statements. Setting to null is only required for objects that have an open handle or resource that must be disposed, because the variable holding that object does not go out of scope. I was trying to address cases where people follow "cargo cult" programming of setting objects to null needlessly. Setting to null inappropriately can actually interfere with efficient garbage collection in some cases, iirc.Federative
When I said "dispose unmanaged resources" how could it not be obvious that I meant clean them up properly (release handles, close files, etc.)? Of course one can't big D Dispose them. Sigh.Federative
@Eskilstuna I see now. You took my sentence "Only dispose unmanaged resources and classes that implement IDisposable" to mean "unmanaged resources ... that implement IDisposable." But what I meant was closer to "Only implement IDisposable in a class if it holds references to IDisposables or to unmanaged resources beyond the scope of a single method."Federative
M
0

The code by ErikE does not work on .NETCore3.1 (sorry, not enough reputation to post a comment...). This line has to be changed:

    private readonly static MethodInfo s_onBeforeVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("OnBeforeVistaDialog", c_flags);

It has to be:

    private readonly static MethodInfo s_onBeforeVistaDialogMethodInfo = typeof(FileDialog).GetMethod("OnBeforeVistaDialog", c_flags);

The method "OnBeforeVistaDialog" is defined on type "FileDialog" in both .NETFramework 4.6 and .NETCore 3.1, but it seems reflection works different in .NETCore.

For .NET 6, there were more internal changes: the interface "IFileDialog" was moved to the assembly "System.Windows.Forms.Primitives" and is nested in internal class "Interop", nested class "Shell32". Same for the Enum "FOS", here the value name was also changed.

Here is the full code of the reflection part:

      private const BindingFlags c_flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
      private readonly static Assembly s_windowsFormsAssembly = typeof(FileDialog).Assembly;
      private readonly static Assembly s_windowsFormsPrimitivesAssembly = Assembly.Load("System.Windows.Forms.Primitives");
      private readonly static Type s_iFileDialogType = s_windowsFormsPrimitivesAssembly.GetType("Interop+Shell32+IFileDialog");
      private readonly static MethodInfo s_createVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("CreateVistaDialog", c_flags);
      private readonly static MethodInfo s_onBeforeVistaDialogMethodInfo = typeof(FileDialog).GetMethod("OnBeforeVistaDialog", c_flags);

      private readonly static MethodInfo s_getOptionsMethodInfo = typeof(FileDialog).GetMethod("GetOptions", c_flags);
      private readonly static MethodInfo s_setOptionsMethodInfo = s_iFileDialogType.GetMethod("SetOptions", c_flags);
      private readonly static uint s_fosPickFoldersBitFlag = (uint)s_windowsFormsPrimitivesAssembly
          .GetType("Interop+Shell32+FOS")
          .GetField("PICKFOLDERS")
          .GetValue(null);
      private readonly static ConstructorInfo s_vistaDialogEventsConstructorInfo = s_windowsFormsAssembly
          .GetType("System.Windows.Forms.FileDialog+VistaDialogEvents")
          .GetConstructor(c_flags, null, new[] { typeof(FileDialog) }, null);
      private readonly static MethodInfo s_adviseMethodInfo = s_iFileDialogType.GetMethod("Advise");
      private readonly static MethodInfo s_unAdviseMethodInfo = s_iFileDialogType.GetMethod("Unadvise");
      private readonly static MethodInfo s_showMethodInfo = s_iFileDialogType.GetMethod("Show");

In .NET8, I could not make this solution work due to internal code changes - there is one method that I could not invoke (maybe because it is unsafe).

But actually this workaround is not necessary at all, because since .NET Core 3.0, the "FolderBrowserDialog" has the modern look and feel.

Mcabee answered 22/5, 2020 at 15:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.