N 级嵌套目录树 WPF 演示






4.91/5 (4投票s)
嵌套树结构和 MVVM 模式的数据绑定示例。
引言
我正在开发一个“任务管理器”项目。每个任务可能包含一个子任务集合。每个子任务都是一个任务,也可能包含一个子任务集合,依此类推。
本文是关于我学到的解决此类问题的有用技巧。让我们开发一个“目录管理器”项目。一个目录可能包含其他目录(子目录)。每个子目录可能包含许多子目录,依此类推。在生成(或从真实的计算机系统中填充树)一个随机的 N 层嵌套目录树后,我们希望能够准确地选择目录来执行某些操作。在本例中,我们希望将其移动到另一个列表进行处理(下面的屏幕截图)。
设置项目
遵循 MVVM 模式,右键单击项目并创建以下文件夹
- 模型
- ViewModels
- 视图
- 辅助函数
模型
为我们的目录创建数据模型,即 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 类作为 Model 和 View 之间的协调器
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 个 ObservableCollection
s<T>:Dirs 和 ProcessDirs 用于在 View 的 XAML 中进行数据绑定。 - 我们生成 4 层 嵌套目录和子目录进行测试,随机 ID 从 1-999。
- 命令绑定,稍后讨论。
View
首先,我们将 MainWindow.xaml 文件移动到像 MVVM 模式那样的 View 文件夹中,然后在 App.xaml 文件中修改该行
StartupUri="MainWindow.xaml"
to
StartupUri="Views/MainWindow.xaml"
在主窗口 XAML 中
... more later
注意 2 个自定义定义的命名空间:myCustomData 和 myCustomLocal,它将 DataContext 设置为我们的 ViewModel、DirViewModel 类的实例。
让我们看看 TreeView 是如何在 XAML 中设置的
<Button x:Name="btnMoveDir"Content="Move"
Tag="{Binding}"
Command="{Binding DataContext.MoveDirCmd,
RelativeSource={RelativeSource FindAncestor,
AncestorType=TreeView}}"
CommandParameter="{Binding ElementName=btnMoveDir}"/>
这里有几点
- TreeView 的 ItemsSource 绑定到 ObservableCollection
我们的 ViewModel 的 <Dirs> 属性 - HierarchicalDataTemplate 的 DataType 设置为我们的 Model (DirModel),它的 ItemsSource 绑定到 SubDirs 属性。这就是让 Binding 系统处理我们的嵌套树的技巧。
- 每个目录节点的 CheckBox:它的 Content 属性绑定到我们的 Model 的 DirDescription 属性。
- 并且,根据 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