File: winforms\Managed\System\WinForms\FolderBrowserDialog.cs
Project: ndp\fx\src\System.Windows.Forms.csproj (System.Windows.Forms)
//------------------------------------------------------------------------------
// <copyright file="FolderBrowserDialog.cs" company="Microsoft">
//     Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>                                                                
//------------------------------------------------------------------------------
 
namespace System.Windows.Forms 
{
    using System;
    using System.IO;
    using System.Collections;
    using System.ComponentModel;
    using System.Drawing;
    using System.Drawing.Design;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    using System.Diagnostics.CodeAnalysis;
    
    /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog"]/*' />
    /// <devdoc>
    ///    <para>
    ///       Represents a common dialog box that allows the user to specify options for 
    ///       selecting a folder. This class cannot be inherited.
    ///    </para>
    /// </devdoc>
    [
    DefaultEvent("HelpRequest"),
    DefaultProperty("SelectedPath"),
    Designer("System.Windows.Forms.Design.FolderBrowserDialogDesigner, " + AssemblyRef.SystemDesign),    
    SRDescription(SR.DescriptionFolderBrowserDialog)
    ]
    public sealed class FolderBrowserDialog : CommonDialog
    {
        // Root node of the tree view.
        private Environment.SpecialFolder rootFolder;
    
        // Description text to show.
        private string descriptionText;
    
        // Folder picked by the user.
        private string selectedPath;
 
        // Show the 'New Folder' button?
        private bool showNewFolderButton;
 
        // set to True when selectedPath is set after the dialog box returns
        // set to False when selectedPath is set via the SelectedPath property.
        // Warning! Be careful about race conditions when touching this variable.
        // This variable is determining if the PathDiscovery security check should
        // be bypassed or not.
        private bool selectedPathNeedsCheck;
 
        // Callback function for the folder browser dialog (see VSWhidbey 551866)
        private UnsafeNativeMethods.BrowseCallbackProc callback;
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.FolderBrowserDialog"]/*' />
        /// <devdoc>
        ///    <para>
        ///       Initializes a new instance of the <see cref='System.Windows.Forms.FolderBrowserDialog'/> class.
        ///    </para>
        /// </devdoc>
        public FolderBrowserDialog() 
        {
            Reset();
        }
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.HelpRequest"]/*' />
        /// <internalonly/>
        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
        public new event EventHandler HelpRequest 
        {
            add 
            {
                base.HelpRequest += value;
            }
            remove 
            {
                base.HelpRequest -= value;
            }
        }
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.ShowNewFolderButton"]/*' />
        /// <devdoc>
        ///     Determines if the 'New Folder' button should be exposed.
        /// </devdoc>
        [
        Browsable(true),
        DefaultValue(true),
        Localizable(false),
        SRCategory(SR.CatFolderBrowsing),
        SRDescription(SR.FolderBrowserDialogShowNewFolderButton)
        ]
        public bool ShowNewFolderButton
        {
            get
            {
                return showNewFolderButton;
            }
            set
            {
                showNewFolderButton = value;
            }
        }
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.SelectedPath"]/*' />
        /// <devdoc>
        ///     Gets the directory path of the folder the user picked.
        ///     Sets the directory path of the initial folder shown in the dialog box.
        /// </devdoc>
        [
        Browsable(true),
        DefaultValue(""),
        Editor("System.Windows.Forms.Design.SelectedPathEditor, " + AssemblyRef.SystemDesign, typeof(UITypeEditor)),
        Localizable(true),
        SRCategory(SR.CatFolderBrowsing),
        SRDescription(SR.FolderBrowserDialogSelectedPath)
        ]
        /// SECREVIEW: ReviewImperativeSecurity
        ///   vulnerability to watch out for: A method uses imperative security and might be constructing the permission using state information or return values that can change while the demand is active.
        ///   reason for exclude: we want to protect when the dialog has been set by the user - this is directly set when in RunDialog
        [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")]
        public string SelectedPath
        {
            get
            {
                if (selectedPath == null || selectedPath.Length == 0)
                {
                    return selectedPath;
                }
 
                if (selectedPathNeedsCheck)
                {
                    Debug.WriteLineIf(IntSecurity.SecurityDemand.TraceVerbose, "FileIO(" + selectedPath + ") Demanded");
                    new FileIOPermission(FileIOPermissionAccess.PathDiscovery, selectedPath).Demand();
                }
                return selectedPath;
            }
            set
            {
                selectedPath = (value == null) ? String.Empty : value;
                selectedPathNeedsCheck = false;
            }
        }
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.RootFolder"]/*' />
        /// <devdoc>
        ///     Gets/sets the root node of the directory tree.
        /// </devdoc>
        [
        Browsable(true),
        DefaultValue(System.Environment.SpecialFolder.Desktop),
        Localizable(false),
        SRCategory(SR.CatFolderBrowsing),
        SRDescription(SR.FolderBrowserDialogRootFolder),
        TypeConverter(typeof(SpecialFolderEnumConverter))
        ]
        public System.Environment.SpecialFolder RootFolder
        {
            get
            {
                return rootFolder;
            }            
            [SuppressMessage("Microsoft.Performance", "CA1803:AvoidCostlyCallsWherePossible")]            
            set
            {
                // FXCop:
                // leaving in Enum.IsDefined because this Enum is likely to grow and we dont own it.
                if (!Enum.IsDefined(typeof(System.Environment.SpecialFolder), value)) 
                {
                    throw new InvalidEnumArgumentException("value", (int)value, typeof(System.Environment.SpecialFolder));
                }
                rootFolder = value;
            }
        }
    
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.Description"]/*' />
        /// <devdoc>
        ///    <para>
        ///       Gets or sets a description to show above the folders. Here you can provide instructions for
        ///       selecting a folder.
        ///    </para>
        /// </devdoc>
        [
        Browsable(true),
        DefaultValue(""),
        Localizable(true),
        SRCategory(SR.CatFolderBrowsing),
        SRDescription(SR.FolderBrowserDialogDescription)
        ]
        public string Description
        {
            get
            {
                return descriptionText;
            }
            set
            {
                descriptionText = (value == null) ? String.Empty : value;
            }
        }
    
        /// <devdoc>
        ///     Helper function that returns the IMalloc interface used by the shell.
        /// </devdoc>
        private static UnsafeNativeMethods.IMalloc GetSHMalloc()
        {
            UnsafeNativeMethods.IMalloc[] malloc = new UnsafeNativeMethods.IMalloc[1];
            UnsafeNativeMethods.Shell32.SHGetMalloc(malloc);
            return malloc[0];
        }
    
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.Reset"]/*' />
        /// <devdoc>
        ///    <para>
        ///       Resets all properties to their default values.
        ///    </para>
        /// </devdoc>
        public override void Reset() 
        {
            rootFolder = System.Environment.SpecialFolder.Desktop;
            descriptionText = String.Empty;
            selectedPath = String.Empty;
            selectedPathNeedsCheck = false;
            showNewFolderButton = true;
        }
 
        /// <include file='doc\FolderBrowserDialog.uex' path='docs/doc[@for="FolderBrowserDialog.RunDialog"]/*' />
        /// <devdoc>
        ///    Implements running of a folder browser dialog.
        /// </devdoc>
        protected override bool RunDialog(IntPtr hWndOwner) 
        {
            IntPtr pidlRoot = IntPtr.Zero;
            bool returnValue = false;
    
            UnsafeNativeMethods.Shell32.SHGetSpecialFolderLocation(hWndOwner, (int) rootFolder, ref pidlRoot);
            if (pidlRoot == IntPtr.Zero)
            {
                UnsafeNativeMethods.Shell32.SHGetSpecialFolderLocation(hWndOwner, NativeMethods.CSIDL_DESKTOP, ref pidlRoot);
                if (pidlRoot == IntPtr.Zero)
                {
                    throw new InvalidOperationException(SR.GetString(SR.FolderBrowserDialogNoRootFolder));
                }
            }
 
            int mergedOptions = unchecked( (int) (long)UnsafeNativeMethods.BrowseInfos.NewDialogStyle);
            if (!showNewFolderButton)
            {
                mergedOptions += unchecked( (int) (long)UnsafeNativeMethods.BrowseInfos.HideNewFolderButton);
            }
 
            // The SHBrowserForFolder dialog is OLE/COM based, and documented as only being safe to use under the STA
            // threading model if the BIF_NEWDIALOGSTYLE flag has been requested (which we always do in mergedOptions
            // above). So make sure OLE is initialized, and throw an exception if caller attempts to invoke dialog
            // under the MTA threading model (...dialog does appear under MTA, but is totally non-functional).
            if (Control.CheckForIllegalCrossThreadCalls && Application.OleRequired() != System.Threading.ApartmentState.STA)
            {
                throw new System.Threading.ThreadStateException(SR.GetString(SR.DebuggingExceptionOnly, SR.GetString(SR.ThreadMustBeSTA)));
            }
    
            IntPtr pidlRet = IntPtr.Zero;
            IntPtr pszDisplayName = IntPtr.Zero;
            IntPtr pszSelectedPath = IntPtr.Zero;
 
            try
            {
                // Construct a BROWSEINFO
                UnsafeNativeMethods.BROWSEINFO bi = new UnsafeNativeMethods.BROWSEINFO();
    
                pszDisplayName = Marshal.AllocHGlobal(NativeMethods.MAX_PATH * Marshal.SystemDefaultCharSize);
                pszSelectedPath = Marshal.AllocHGlobal((NativeMethods.MAX_PATH + 1) * Marshal.SystemDefaultCharSize);
                this.callback = new UnsafeNativeMethods.BrowseCallbackProc(this.FolderBrowserDialog_BrowseCallbackProc);
 
                bi.pidlRoot = pidlRoot;
                bi.hwndOwner = hWndOwner;
                bi.pszDisplayName = pszDisplayName;
                bi.lpszTitle = descriptionText;
                bi.ulFlags = mergedOptions;
                bi.lpfn = callback;
                bi.lParam = IntPtr.Zero;
                bi.iImage = 0;
    
                // And show the dialog
                pidlRet = UnsafeNativeMethods.Shell32.SHBrowseForFolder(bi);
    
                if (pidlRet != IntPtr.Zero)
                {
                    // Then retrieve the path from the IDList
                    UnsafeNativeMethods.Shell32.SHGetPathFromIDListLongPath(pidlRet, ref pszSelectedPath);
    
                    // set the flag to True before selectedPath is set to
                    // assure security check and avoid bogus race condition
                    selectedPathNeedsCheck = true;
 
                    // Convert to a string
                    selectedPath = Marshal.PtrToStringAuto(pszSelectedPath);
 
                    returnValue = true;
                }
            }
            finally 
            {
                UnsafeNativeMethods.CoTaskMemFree(pidlRoot);
                if (pidlRet != IntPtr.Zero)
                {
                    UnsafeNativeMethods.CoTaskMemFree(pidlRet);
                }
 
                // Then free all the stuff we've allocated or the SH API gave us
                if (pszSelectedPath != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(pszSelectedPath);
                }
                if (pszDisplayName != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(pszDisplayName);
                }
 
                this.callback = null;
            }
            return returnValue;
        }
 
        /// <devdoc>
        ///    Callback function used to enable/disable the OK button,
        ///    and select the initial folder.
        /// </devdoc>
        private int FolderBrowserDialog_BrowseCallbackProc(IntPtr hwnd,
                                                           int msg, 
                                                           IntPtr lParam, 
                                                           IntPtr lpData)
        {
            switch (msg)
            {
                case NativeMethods.BFFM_INITIALIZED: 
                    // Indicates the browse dialog box has finished initializing. The lpData value is zero. 
                    if (selectedPath.Length != 0) 
                    {
                        // Try to select the folder specified by selectedPath
                        UnsafeNativeMethods.SendMessage(new HandleRef(null, hwnd), (int) NativeMethods.BFFM_SETSELECTION, 1, selectedPath);
                    }
                    break;
                case NativeMethods.BFFM_SELCHANGED: 
                    // Indicates the selection has changed. The lpData parameter points to the item identifier list for the newly selected item. 
                    IntPtr selectedPidl = lParam;
                    if (selectedPidl != IntPtr.Zero)
                    {
                        IntPtr pszSelectedPath = Marshal.AllocHGlobal((NativeMethods.MAX_PATH + 1) * Marshal.SystemDefaultCharSize);
                        // Try to retrieve the path from the IDList
                        bool isFileSystemFolder = UnsafeNativeMethods.Shell32.SHGetPathFromIDListLongPath(selectedPidl, ref pszSelectedPath);
                        Marshal.FreeHGlobal(pszSelectedPath);
                        UnsafeNativeMethods.SendMessage(new HandleRef(null, hwnd), (int) NativeMethods.BFFM_ENABLEOK, 0, isFileSystemFolder ? 1 : 0);
                    }
                    break;
            }
            return 0;
        }
    }
}