.NET WPF Folder Picker / Folder Browser

The heart of the dialog is WPF TreeView control and underlying data model. Here is the model:

    public class TreeItem : NotifiableObject
    {
        #region Properties

        public bool IsFullyLoaded { get; set; }

        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                name = value;
                NotifyPropertyChanged(() => Name);
            }
        }

        public TreeItem Parent
        {
            get
            {
                return parent;
            }
            set
            {
                parent = value;
                NotifyPropertyChanged(() => Parent);
            }
        }

        public ObservableCollection Childs
        {
            get
            {
                return childs;
            }
            set
            {
                childs = value;
                NotifyPropertyChanged(() => Childs);
            }
        }

        #endregion

        public TreeItem(string name, TreeItem parent)
        {
            Name = name;
            IsFullyLoaded = false;
            Parent = parent;
            Childs = new ObservableCollection();
        }

        public string GetFullPath()
        {
            Stack stack = new Stack();

            var ti = this;

            while (ti.Parent != null)
            {
                stack.Push(ti.Name);
                ti = ti.Parent;
            }

            string path = stack.Pop();

            while (stack.Count > 0)
            {
                path = Path.Combine(path, stack.Pop());
            }

            return path;
        }

        #region Private fields

        private string name;

        private TreeItem parent;

        private ObservableCollection childs;

        #endregion
    }

    public class DriveTreeItem : TreeItem
    {
        public DriveType DriveType { get; set; }

        public DriveTreeItem(string name, DriveType driveType, TreeItem parent)
            : base(name, parent)
        {
            DriveType = driveType;
        }
    }

And here is the control itself:

   public partial class FolderPickerControl : UserControl, INotifyPropertyChanged
    {
        #region Constants

        private static readonly string EmptyItemName = "Empty";

        #endregion

        #region Properties

        public TreeItem Root
        {
            get
            {
                return root;
            }
            private set
            {
                root = value;
                NotifyPropertyChanged(() => Root);
            }
        }

        public TreeItem SelectedItem
        {
            get
            {
                return selectedItem;
            }
            private set
            {
                selectedItem = value;
                NotifyPropertyChanged(() => SelectedItem);
            }
        }

        public string SelectedPath { get; private set; }

        #endregion

        public FolderPickerControl()
        {
            InitializeComponent();

            Init();
        }

        #region INotifyPropertyChanged Members

        public void NotifyPropertyChanged(Expression> property)
        {
            var lambda = (LambdaExpression)property;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = (UnaryExpression)lambda.Body;
                memberExpression = (MemberExpression)unaryExpression.Operand;
            }
            else memberExpression = (MemberExpression)lambda.Body;
            OnPropertyChanged(memberExpression.Member.Name);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion

        #region Private methods

        private void Init()
        {
            root = new TreeItem("root", null);
            var systemDrives = DriveInfo.GetDrives();

            foreach (var sd in systemDrives)
            {
                var item = new DriveTreeItem(sd.Name, sd.DriveType, root);
                item.Childs.Add(new TreeItem(EmptyItemName, item));

                root.Childs.Add(item);
            }

            Root = root; // to notify UI
        }

        private void TreeView_Selected(object sender, RoutedEventArgs e)
        {
            var tvi = e.OriginalSource as TreeViewItem;
            if (tvi != null)
            {
                SelectedItem = tvi.DataContext as TreeItem;
                SelectedPath = SelectedItem.GetFullPath();
            }
        }

        private void TreeView_Expanded(object sender, RoutedEventArgs e)
        {
            var tvi = e.OriginalSource as TreeViewItem;
            var treeItem = tvi.DataContext as TreeItem;

            if (treeItem != null)
            {
                if (!treeItem.IsFullyLoaded)
                {
                    treeItem.Childs.Clear();

                    string path = treeItem.GetFullPath();

                    DirectoryInfo dir = new DirectoryInfo(path);

                    try
                    {
                        var subDirs = dir.GetDirectories();
                        foreach (var sd in subDirs)
                        {
                            TreeItem item = new TreeItem(sd.Name, treeItem);
                            item.Childs.Add(new TreeItem(EmptyItemName, item));

                            treeItem.Childs.Add(item);
                        }
                    }
                    catch { }

                    treeItem.IsFullyLoaded = true;
                }
            }
            else
                throw new Exception();
        }

        #endregion

        #region Private fields

        private TreeItem root;
        private TreeItem selectedItem;

        #endregion
    }

More code can be found here. As you can see stub childs are added for each tree item if it hasn’t been expanded yet. Lazy loading is used, so actual childs are fetched only when user expands folder or drive.

Pages: 1 2

Posted in .NET. Tags: , . 27 Comments »

27 Responses to “.NET WPF Folder Picker / Folder Browser”

  1. Canyou Says:

    is it free ? no license ?

    • tillias Says:

      Yep it’s under GNU GPL v3 so you can use it both for open source and commercial software. Feel free to modify for your needs or ask for features

      • Jared Says:

        Tillas,

        Thanks for pointing me to this on my blog. However, I cannot use it because it is GPL. You statement in the comment above is not accurate.

        Actually the GPL means you CANNOT use it for free in Commercial software.

        If you want it to be free to use in Commercial software, you need to use a more permissive BSD license.

        I have post about these license differences.
        Differences between the BSD/FreeBSD Copyrights and the GNU Public License (GPL)

        Reading your comment, it seems like you intended it to be a free license that can be used for commerically. If that is your intent, you must change the license to be a more permissive license. BSD, MIT, etc…

      • tillias Says:

        Thanks for you clarification. I will update licensing ASAP.

      • Rutix Says:

        Jared, that is not really correct. You can use GPL for free in Commercial Software. It only means that that commercial software automaticly also gets the GPL license and the GPL license also applies to that software. A direct comment from the GPL license site:

        Does the GPL allow me to sell copies of the program for money? (#DoesTheGPLAllowMoney)
        - Yes, the GPL allows everyone to do this. The right to sell copies is part of the definition of free software. Except in one special situation, there is no limit on what price you can charge. (The one exception is the required written offer to provide source code that must accompany binary-only release.)

        Source: http://www.gnu.org/licenses/gpl-faq.html#DoesTheGPLAllowMoney

  2. Oliver Says:

    Nice work! I just have one issue (maybe the problem is on my side ;-)): When i choose to “Cancel” the folder select dialog and then try to open it again – i.e. hit the ‘Browse…’ – Button again – i get an InvalidOperationException when i execute dlg.ShowDialog():
    “DialogResult can be set only after Window is created and shown as dialog.”
    Any idea?

    • tillias Says:

      I think you use dialog.Show() instead if dialog.ShowDialog(). Can you show me the usage code please?

      • Oliver Says:

        Sure, here’s the code snippet:

        private void btnSelInDir_Click(object sender, RoutedEventArgs e) {
        FolderPickerDialog dlg = new FolderPickerDialog();
        //dlg.Title = “Select Input Directory”;
        if (dlg.ShowDialog() == true) {
        txfInputDir.Text = dlg.SelectedPath;
        }
        }

        Do i need to do some cleanup/dispose?

      • tillias Says:

        Very strange. I’ve added following internal check while closing dialog:
        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
        if (ComponentDispatcher.IsThreadModal)
        {
        DialogResult = false;
        }
        else
        {
        Close();
        }
        }
        This is done according to http://stackoverflow.com/questions/1378602/setting-dialogresult-only-after-showdialog-in-wpf

        Please download new binray package from google.code (links on the first page are updated) and let me know wheter it helps or not. Thanks!

  3. Oliver Says:

    The new version solved the problem. Thank you!

  4. John Says:

    Hi,
    I think there’s a problem with your read-only SVN checkout link as I cannot get it to work.
    Thanks
    John

  5. Maria Simlinger Says:

    Hello,

    First thanks for providing this solution.

    Is it possible to set a Startup value for the SelectedPath prior to opening the Dialog? This Property is read-only (r133).

    Thanks
    Maria

    • tillias Says:

      Hello Maria! Thanks for request! I’ve created issue http://code.google.com/p/wmediacatalog/issues/detail?id=38 and will fix it in several two days. You can monitor status of your issue using link given above.

      I will provide additional InitialPath property with public setter which will allow you to start dialog with expected initial folder expanded. Though I still suggest to leave SelectedPath read-only to avoid confusion.

  6. kyosuke Says:

    Hello is the multiple selected folders features are there for this lib?

  7. Rossano Says:

    Hi Tillias,

    very good works thanks for providing it!
    I am interested on the improvement identified by Maria, and I see it is fixed, can you tell me when it will be available to download?

    Thanks
    Rossano

  8. Tim Says:

    Hi Tillias,
    Thanks for this! Nice work.
    Tim

  9. Michael Says:

    Very nice.

    I too am worried about the GPL. It still says GPL on Google Code.

    Also, one feature that would be great is a new folder button.

    If the license changes, I may try to add some features to it, because I would use it in my projects.

    • tillias Says:

      It makes sense to change licensing model for FolderPicker to BSD license. I’ve created issue for new folder functionality

  10. Kris C Says:

    Is there a way you can sign the posted assembly so it can be used in projects that are signed? Without this I can’t use the dll directly.

  11. David B. Bitton Says:

    How about creating a NuGet package for this? Thanks!

  12. davidbitton Says:

    I’m in the process of modifying FolderPickerControl. I made InitialPath a DependencyProperty so I could set it via binding. I’ve also ran through the code with ReSharper and made the usual suggested changes (and Childs is now Children). Next step is to code it such that the InitialPath can be the root. What I’m not understanding is the use of Tasks in the initial update to the UI. From what I can see from setting a breakpoint inside the ContinueWith, you’re waiting for the tree to become visible before painting it. Is that correct? I put the tree in an Expander, so the ContinueWith block isn’t called until the Expander is expanded. Why wait? You’ve already loaded the initial folders under the drive. Thanks!

    • tillias Says:

      Tasks are needed to handle container generation stuff. Without them we can’t implement lazy loading. Feel free to commit changes — I’ll provide you with google.code credentials — just send me your google e-mail

      • davidbitton Says:

        I understand lazy loading but why this and not just hook the Expanded event? Also, have you considered using Reactive Framework in lieu of Tasks? Also, have you thought about moving to GitHub so I can fork the project? Thanks!

      • tillias Says:

        davidbitton:

        Have you tested it with Expanded event? Does it work? I used Tasks to wait until ContainerGenerator finished generating treeView items. What is the advantage of Reactive Framework? I think it should be as simple and lightweight as possible by the means of external dependencies.

        And yes — gitHub is nice place to store it. Though we should think about default MS icons and licensing in case of moving to gitHub. Can you please advise about it?

      • davidbitton Says:

        As far as icons go, I’ve used Win32 calls for that. I use SHGetFileInfo(…). Now using p/invoke precludes the lib’s use in mono. Not sure if that matters. Regarding Reactive Framework, I find that it makes subscribing to asynch operation very easy. Sure, the lib would then have a dependence on another lib, but if coders rely on NuGet, then it just becomes a dependency and Rx is loaded as necessary. Email me so we can discuss further.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.