Click here to Skip to main content
15,881,248 members
Articles / Desktop Programming / WPF

A n-level Nested Directory Tree WPF Demo

Rate me:
Please Sign up or sign in to vote.
4.91/5 (4 votes)
15 May 2015CPOL3 min read 17.9K   494   11   2
Example of data binding for nested tree structure, and MVVM pattern.

Introduction

I was working on a 'Task Manager' project. Each task might contain a collection of sub-tasks. Each sub-task is a task, and might also contain a collection of sub-tasks, and so on.

This article is about useful techniques I've learned to solve problems like that. Let's work on a 'Directory Manager' project. A directory might contain other directories (sub-directories). Each sub-directory might contain a number of sub-directories, and so on. After generate (or populate a tree from a real computer system) a random n-level nested directory tree, we want to be able to pick exactly the directory to do something with it. In this case, we want to move it to another list for processing (screen-shot below).

Image 1

Set up the project

Image 2

Follow the MVVM pattern, right-click on the Project and create the following folders:

  1. Models
  2. ViewModels
  3. Views
  4. Helpers

The Model

Create the data-model for our directory, the DirModel class.

* Note: in an article, WPF/MVVM Quick Start Tutorial, and several other examples you might see the INotifyPropertyChanged is implemented in the ViewModel. However, I find it more natural to implement it in the Model because Properties belong to the Model.

C#
public class DirModel : INotifyPropertyChanged
    {
        #region Fields
        
        DirModel parent;
        int dirId;
        string dirName;
        bool? isSelected = false;
        ObservableCollection<dirmodel> subDirs;
        static Random random = new Random();
        
        #endregion / Fields
        
        #region Set parent for sub-dirs
        
        public void SetParent() {
            foreach (DirModel dir in this.subDirs) {
                dir.parent = this;
                dir.SetParent();
            }
        }
        
        #endregion /Set parent
        
        #region Properties
        
        public string DirDescription
        {
            get { return this.dirName + " #" + this.dirId; }
        }
        
        public DirModel Parent
        {
            get { return this.parent; }
        }
        
        public ObservableCollection<dirmodel> SubDirs
        {
            get { return this.subDirs; }
            set
            {
                this.subDirs = value;
                OnPropertyChanged("SubDirs");
            }
        }
        
        public bool? IsSelected... // more later
        
        #endregion / Properties
        
        #region Constructor and Get Instance
        
        private DirModel(string name, int id)
        {
            this.dirId = id;
            this.dirName = name;
            this.subDirs = new ObservableCollection<dirmodel>();
        }
        
        public static DirModel GetDir(string name, int upLimitDirRandomId)
        {
            return new DirModel(name, random.Next(1, upLimitDirRandomId));
        }
        
        #endregion / Constructor and Get Instance
        
        #region INotifyPropertyChanged
        
        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
        
        #endregion / INotifyPropertyChanged
    }
</dirmodel></dirmodel></dirmodel>

Besides the basic standard fields, constructors, and PropertyChanged notifications in the above code, we notice:

  • The recursive method SetParent() to apply for a directory and its sub-directory.
  • The private constructor and a method to get a directory with random generated ID number. You can do it in a different way, it's up to you.

The IsSelected property to determine the selected states of the parent and children nodes in the tree when a particular directory node is set:

C#
public class DirModel : INotifyPropertyChanged
    {
        ...
        public bool? IsSelected
        {
            get { return this.isSelected; }
            set
            {
                SetIsSelected(value, true, true);
            }
        }
        
        private void SetIsSelected(bool? value, bool isUpdateChildren, bool isUpdateParent)
        {
            if (value == this.isSelected) return;
            
            this.isSelected = value;
            
            if (isUpdateChildren && this.isSelected.HasValue)
            {
                foreach (DirModel dir in this.subDirs)
                {
                    dir.SetIsSelected(this.isSelected, true, false);
                }
            }
            
            if (isUpdateParent && this.parent != null)
            {
                this.parent.VerifyCheckedState();
            }
            
            OnPropertyChanged("IsSelected");
        }
        
        private void VerifyCheckedState()
        {
            bool? state = null;
            for (int i = 0; i < this.subDirs.Count; i++)
            {
                bool? current = this.subDirs.ElementAt(i).isSelected;
                if (i == 0)
                {
                    state = current;
                }
                else if (state != current)
                {
                    state = null;
                    break;
                }
            }
            
            SetIsSelected(state, false, true);
        }
    }

The ViewModel

Create DirViewModel class as a coordinator between the Model and the View:

C#
public class DirViewModel
    {
        #region Fields
        
        DirModel myDir;
        ObservableCollection<dirmodel> dirs = new ObservableCollection<dirmodel>();
        ObservableCollection<dirmodel> processDirs = new ObservableCollection<dirmodel>();
        
        #endregion / Fields

        #region Constructor and Generate random directories and nested sub-directories
        
        public DirViewModel()
        {
            GenerateRandomNestedDirs();
            #region Set parent
            foreach (DirModel dir in this.dirs)
            {
                dir.SetParent();
            }
            #endregion / Set parent
        }
        
        private void GenerateRandomNestedDirs()
        {
            #region 1st level
            for (int i = 0; i < 6; i++)
            {
                this.myDir = DirModel.GetDir("Dir", 1000);
                this.dirs.Add(this.myDir);
            }
            #endregion
            
            #region 2nd level
            foreach (DirModel dir in this.dirs)
            {
                for (int i = 0; i < 2; i++)
                {
                    this.myDir = DirModel.GetDir("Sub-Dir", 1000);
                    dir.SubDirs.Add(this.myDir);
                }
            }
            #endregion
            
            #region 3rd level
            foreach (DirModel dir in this.dirs)
            {
                foreach (DirModel subDir in dir.SubDirs)
                {
                    for (int i = 0; i < 2; i++)
                    {
                        this.myDir = DirModel.GetDir("Sub-sub-Dir", 1000);
                        subDir.SubDirs.Add(this.myDir);
                    }
                }
            }
            #endregion
            
            #region 4th level
            foreach (DirModel dir in this.dirs)
            {
                foreach (DirModel subDir in dir.SubDirs)
                {
                    foreach (DirModel subSubDir in subDir.SubDirs)
                    {
                        for (int i = 0; i < 2; i++)
                        {
                            this.myDir = DirModel.GetDir("Sub-sub-sub-Dir", 1000);
                            subSubDir.SubDirs.Add(this.myDir);
                        }
                    }
                }
            }
            #endregion
        }
        
        #endregion / Constructor and Generate random
        
        #region Properties 
        public ObservableCollection<dirmodel> Dirs
        {
            get { return this.dirs; }
            set { this.dirs = value; }
        }
        
        public ObservableCollection<dirmodel> ProcessDirs
        {
            get { return this.processDirs; }
            set { this.processDirs = value; }
        }
        #endregion / Properties
        
        #region Commands
        ... later
        #endregion / Commands
    }
</dirmodel></dirmodel></dirmodel></dirmodel></dirmodel></dirmodel>

Several points about the above directory view-model, DirViewModel:

  • DirViewModel exposes public properties, those are 2 ObservableCollection<t>s<T>: Dirs and ProcessDirs for data binding in XAML of the View.
  • We generate 4-levels nested directory and sub-directories for testing, with random IDs from 1-999.
  • Command binding, will discuss later.

The View

First, we move the MainWindow.xaml file to the View folder like MVVM patterns does, then in the App.xaml file we modify file the line:

C#
StartupUri="MainWindow.xaml"

to:

C#
StartupUri="Views/MainWindow.xaml"

In the Main Window XAML:

XAML
<window 
    x:class="DirectoryManager.MainWindow" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:mycustomdata="clr-namespace:DirectoryManager.Models" 
    xmlns:mycustomlocal="clr-namespace:DirectoryManager.ViewModels" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <window.datacontext>
        <mycustomlocal:dirviewmodel>
    </mycustomlocal:dirviewmodel></window.datacontext>
    
    ... more later
</window>

Notice the 2 custom define namespaces: myCustomData and myCustomLocal, and it sets the DataContext to be an instance of our ViewModel, DirViewModel class.

Let's see how TreeView is set-up in the XAML:

XAML
<treeview itemssource="{Binding Dirs}">
    <treeview.itemtemplate>
        <hierarchicaldatatemplate datatype="{x:Type myCustomData:DirModel}"
            itemssource="{Binding SubDirs}">

            <wrappanel checkbox="" content="{Binding DirDescription}"
                ischecked="{Binding IsSelected}" x:name="chkDir">

                <Button x:Name="btnMoveDir"Content="Move"
                    Tag="{Binding}"
                    Command="{Binding DataContext.MoveDirCmd,
                        RelativeSource={RelativeSource FindAncestor,
                        AncestorType=TreeView}}"
                    CommandParameter="{Binding ElementName=btnMoveDir}"/>
            </wrappanel>

        </hierarchicaldatatemplate>
    </treeview.itemtemplate>
</treeview>

There are several points here:

  1. The ItemsSource of TreeView is binded to the ObservableCollection<dirmodel><Dirs> property of our ViewModel
  2. HierarchicalDataTemplate's DataType is set to our Model (DirModel) and it's ItemsSource is binded to SubDirs property. That's the trick to let the Binding system to take care of our nested tree.
  3. The CheckBox for each directory node: its Content property is binded to DirDescription property of our Model.
  4. And, according to MVVM pattern, we elimiate event-handlers in code-behind. Instead, we use Command Binding. As we see above, the Button's command is binded to the MoveDirCmd. (Note that we specify DataContext.MoveDirCmd). The ViewModel is reponsible to take action. This is a trick! We use the Tag to attach a specific directory node to the button (a name is needed), and CommandParameter will send this object (which will be casted to our Model) to the action method implemented by ViewModel.

The situation is much easier if we bind commands outside of ItemTemplate. We have optional parameter to send, for example, a string "This is test command parameter!" as below to the method to be implemented in the ViewModel:

XAML
<Button Content="Test Click Me"
    Command="{Binding TestCmd}"
    CommandParameter="This is test command parameter!"/>

Before going into the implementation of command binding in ViewModel, let's just have a look at:

The standard RelayCommand

C#
public class RelayCommand : ICommand
{
    #region Fields
    readonly Action<object> execute;
    readonly Predicate<object> canExecute;
    #endregion

    #region Constructors
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {

    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null) throw new ArgumentNullException("execute");
        this.execute = execute;
        this.canExecute = canExecute;
    }
    #endregion

    #region ICommand Members [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return canExecute == null ? true : canExecute(parameter);
    }
    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
        }
    }
    public void Execute(object parameter)
    {
        execute(parameter);
    }
    #endregion
}

We can determine value of CanExecute based on different factors, but for this project, we just make it true and go ahead invoke the XYZExecute(object) method:

C#
// in public class DirViewModel

#region TestCmd *outside of ItemTemplate*

    ICommand testCmd;
    public ICommand TestCmd
    {
        get
        {
            return this.testCmd ??
                (this.testCmd = new RelayCommand(this.TestExe));
        }
    }

    void TestExe(object obj)
    {
        MessageBox.Show(obj.ToString());
        // A string obj was send: "This is test command parameter!"
    }

    #endregion / TestCmd *outside of ItemTemplate*

    #region MoveDirCmd *inside of ItemTemplate*

    ICommand moveDirCmd;
    public ICommand MoveDirCmd
    {
        get
        {
            return this.moveDirCmd ??
                (this.moveDirCmd = new RelayCommand(this.MoveDirExe));
        }
    }

    private void MoveDirExe(object obj)
    {
        Button clickedBtn = obj as Button;
        DirModel dir = clickedBtn.Tag as DirModel;

        MoveThisDir(dir);
    }

    private void MoveThisDir(DirModel d)
    {
        if (d.IsSelected == true)
        {
            if (d.Parent == null)
            {
                this.dirs.Remove(d);
                this.processDirs.Add(d);
            }
            else
            {
                DirModel parent = d.Parent;
                parent.SubDirs.Remove(d);
                this.processDirs.Add(d);
            }
        }

        else if (d.IsSelected == null)
        {
            MessageBox.Show(d.DirDescription + " has sub-Dir not selected");
        }

        else
        {
            MessageBox.Show("Please explicitly select " + d.DirDescription);
        }
    }

    #endregion / MoveDirCmd *inside of ItemTemplate*

History

  • Project first set-up: 05/15/2015

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
United States United States
while (live) {
try {
learn();
code();
food();
...
} catch (Exception ex) {
recover();
}
}

Comments and Discussions

 
PraiseA comment Pin
Oleksiy Sokolovskiy30-Jan-16 19:00
Oleksiy Sokolovskiy30-Jan-16 19:00 
GeneralRe: A comment Pin
Lộc Nguyễn1-Feb-16 12:48
professionalLộc Nguyễn1-Feb-16 12:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.