65.9K
CodeProject 正在变化。 阅读更多。
Home

N 级嵌套目录树 WPF 演示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (4投票s)

2015年5月16日

CPOL

3分钟阅读

viewsIcon

19175

downloadIcon

543

嵌套树结构和 MVVM 模式的数据绑定示例。

引言

我正在开发一个“任务管理器”项目。每个任务可能包含一个子任务集合。每个子任务都是一个任务,也可能包含一个子任务集合,依此类推。

本文是关于我学到的解决此类问题的有用技巧。让我们开发一个“目录管理器”项目。一个目录可能包含其他目录(子目录)。每个子目录可能包含许多子目录,依此类推。在生成(或从真实的计算机系统中填充树)一个随机的 N 层嵌套目录树后,我们希望能够准确地选择目录来执行某些操作。在本例中,我们希望将其移动到另一个列表进行处理(下面的屏幕截图)。

设置项目

遵循 MVVM 模式,右键单击项目并创建以下文件夹

  1. 模型
  2. ViewModels
  3. 视图
  4. 辅助函数

模型

为我们的目录创建数据模型,即 DirModel 类。

* 注意:在一篇文章 WPF/MVVM 快速入门教程 以及其他一些示例中,您可能会看到 INotifyPropertyChanged 在 ViewModel 中实现。但是,我发现在 Model 中实现它更自然,因为 Properties 属于 Model。

public class DirModel : INotifyPropertyChanged
    {
        #region Fields
        
        DirModel parent;
        int dirId;
        string dirName;
        bool? isSelected = false;
        ObservableCollection 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 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();
        }
        
        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
    }

除了上述代码中的基本标准字段、构造函数和 PropertyChanged 通知之外,我们注意到

  • 递归方法 SetParent() 用于应用于目录及其子目录。
  • 私有构造函数和一个获取具有随机生成的 ID 号的目录的方法。你可以用不同的方式来做,这取决于你。

IsSelected 属性用于确定当设置特定目录节点时,树中父节点和子节点的选择状态

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);
        }
    }

ViewModel

创建 DirViewModel 类作为 ModelView 之间的协调器

public class DirViewModel
    {
        #region Fields
        
        DirModel myDir;
        ObservableCollection dirs = new ObservableCollection();
        ObservableCollection processDirs = new ObservableCollection();
        
        #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 Dirs
        {
            get { return this.dirs; }
            set { this.dirs = value; }
        }
        
        public ObservableCollection ProcessDirs
        {
            get { return this.processDirs; }
            set { this.processDirs = value; }
        }
        #endregion / Properties
        
        #region Commands
        ... later
        #endregion / Commands
    }

关于上面的目录视图模型 DirViewModel 的几点说明

  • DirViewModel 公开公共属性,即 2 个 ObservableCollections<T>DirsProcessDirs 用于在 ViewXAML 中进行数据绑定。
  • 我们生成 4 层 嵌套目录和子目录进行测试,随机 ID 从 1-999。
  • 命令绑定,稍后讨论。

View

首先,我们将 MainWindow.xaml 文件移动到像 MVVM 模式那样的 View 文件夹中,然后在 App.xaml 文件中修改该行

    StartupUri="MainWindow.xaml"

to

    StartupUri="Views/MainWindow.xaml"

在主窗口 XAML 中


    
        
    
    
    ... more later

注意 2 个自定义定义的命名空间:myCustomDatamyCustomLocal,它将 DataContext 设置为我们的 ViewModelDirViewModel 类的实例。

让我们看看 TreeView 是如何在 XAML 中设置的

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

这里有几点

  1. TreeViewItemsSource 绑定到 ObservableCollection我们的 ViewModel<Dirs> 属性
  2. HierarchicalDataTemplateDataType 设置为我们的 Model (DirModel),它的 ItemsSource 绑定到 SubDirs 属性。这就是让 Binding 系统处理我们的嵌套树的技巧。
  3. 每个目录节点的 CheckBox:它的 Content 属性绑定到我们的 Model 的 DirDescription 属性。
  4. 并且,根据 MVVM 模式,我们消除了代码隐藏中的事件处理程序。相反,我们使用 Command Binding。如上所示,Button 的 command 绑定到 MoveDirCmd。(请注意,我们指定了 DataContext.MoveDirCmd)。ViewModel 负责采取行动。这是一个技巧!我们使用 Tag 将特定的目录节点附加到按钮(需要一个名称),并且 CommandParameter 会将这个对象(将被强制转换为我们的 Model)发送到 ViewModel 实现的操作方法。

如果我们绑定在 ItemTemplate 之外的命令,情况会容易得多。我们可以选择发送参数,例如,一个字符串“This is test command parameter!”,如下所示,发送到要在 ViewModel 中实现的方法

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

在进入 ViewModel 中的命令绑定实现之前,让我们先看看

标准 RelayCommand

    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 
    }

我们可以根据不同的因素确定 CanExecute 的值,但对于这个项目,我们只需将其设置为 true,然后继续调用 XYZExecute(object) 方法

    // 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*

历史

  • 项目首次设置:2015/05/15
© . All rights reserved.