WPF中使用MVVM模式操作TreeView
程序功能
程序使用MVVM模式实现了对WPF TreeView中节点的添加,重命名,删除,上(下)移动,并且可以统计当前TreeView选择的节点和全部接点个数。
(截图)
摘要:
|
TreeView的特点
无论是哪种UI框架,TreeView都显得比较特殊,WPF中的TreeView亦如此。让我们来简要的看一下TreeView在WPF中的特殊地位:
1. 理论上讲,TreeView也属于选择器,但由于其特殊的存储结构WPF中的TreeView并没有像ListBox,TabControl等控件一样继承与Selector类,而是直接继承自ItemsControl类,并定义自己的SelectedItem,SelectedValue等Selector所有的属性。
2. TreeView使用HierarchicalDataTemplate和TreeViewItem(继承自HeaderedItemsControl)。
3. 没有对CollectionView的支持,因此也没有IsSynchronizedWithCurrentItem属性。
在CodeProject上看到一篇非常好的文章(对我帮助很大):Simplifying the WPF TreeView by Using the ViewModel Pattern,地址:http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx
不过这篇文章没有讲节点的添加删除移动操作和信息的统计,希望大家都看看这篇非常出色的文章。
节点的操作源:NodeViewModel
MVVM中的ViewModel是数据的最终操作者,而View则反映并且也会影响着ViewModel中数据的结果,本例中至关重要的TreeViewItem的操作者也是数据的代表者:NodeViewModel定义这整个TreeView的节点存储模式。通过在View层的TreeView的DataTemplate在中间做桥梁,两端对象互相影响。
那么NodeViewModel首先具备定义数据的存储形式(一切都是以数据为中心)
那么最原始的NodeViewModel应该至少有如下结构,来反映当前节点的显示名称和子节点。
ObservableCollection<NodeViewModel> children; public string Name { get; private set; } public ReadOnlyObservableCollection<NodeViewModel> Children { get { return new ReadOnlyObservableCollection<NodeViewModel>(children); } } |
其次,一个ViewModel不仅仅是用来存储View的数据源,还需要有其他数据,用来影响View的特性或者影响自己的操作模式,这些大家都懂,那么就这个例子来说,我们需要定义节点是否被选择,是否展开,以及一些相应事件和操作。并且注意需要继承INotifyPropertyChanged来反映属性变化,用ObservableCollection对象来反映容器变化。两者分别在System.ComponentModel和System.Collections.ObjectModel命名空间内。
NodeViewModel的事件部分
#region 事件 public event EventHandler IsExpandedChanged; public event EventHandler IsSelectedChanged; protected virtual void OnIsExpandedChanged() { if (IsExpandedChanged != null) IsExpandedChanged(this, EventArgs.Empty); } protected virtual void OnIsSelectedChanged() { if (IsSelectedChanged != null) IsSelectedChanged(this, EventArgs.Empty); if (IsSelected) NodeInfo.SelectedNode = this; } #endregion #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string proName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(proName)); } #endregion |
定义好事件后要在相应属性改编后,或者函数操作中对触发事件,这样依赖于事件的另一方代码才会被正确执行。
NodeViewModel的节点操作代码就是基本的容器操作,实现起来比较简单,这里就不再多说了。
掌握TreeView的信息:NodeInfo类
那么现在如果NodeViewModel构建好后,如果我们要在ViewModel层上知道TreeView的当前选择节点和全部节点个数,该怎么办?
我们在定义NodeViewModel时就说过要额外定义一些其他特性比如节点的选择控制,并且View和ViewModel可以通过一些桥梁而互相影响(比如绑定),这是每一个节点在View中是否被选择会立即反映到ViewModel中,问题是怎样知道当前那个被选择的节点?此时,我们之前定义的事件有了用处,即利用每个节点被选择后发出的事件,我们把这个节点信息反馈到一个公共的地方,这个公共的地方就是:NodeInfo类。
节点个数的统计原理也一样,每当节点个数发生变化时,我们都会对总的节点个数进行相应的更新。
NodeInfo类代码(部分)
class NodeInfo : INotifyPropertyChanged { NodeViewModel selectedNode; int count; public NodeViewModel SelectedNode { get { return selectedNode; } set { if (selectedNode != value) { selectedNode = value; OnSelectedNodeChanged(); OnPropertyChanged("SelectedNode"); } } } public int Count { get { return count; } private set { if (count != value) { count = value; OnPropertyChanged("Count"); } } } internal void SetCount(int newcount) { Count = newcount; } public event EventHandler SelectedNodeChanged; protected virtual void OnSelectedNodeChanged() { if (SelectedNodeChanged != null) SelectedNodeChanged(this, EventArgs.Empty); } } |
最后在每一个NodeViewModel中加入这个统一的NodeInfo,NodeInfo就成为节点操作后统计信息的公共场所,最终的NodeInfo会被存放着MainViewModel中(后面会讲MainViewModel)。
你的命令逻辑
这个子标题是“你的命令逻辑”,什么意思?就是说你可以按照自己的方式在ViewModel中定义命令。你可以直接写一个继承ICommand的类,或者用其他MVVM框架中常见的类,比如Josh Smith的RelayCommand或者Prism中的DelegateCommand。
我们就简简单单用DelegateCommand并根据需要另外定义DisplayName代表命令的显示名称,和HasCanExecuted来判断命令是否需要刷新,如果是则调用DelegateCommand的RaiseCanExecuteChanged方法。
参考:
RelayCommand:http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
DelegateCommand:http://msdn.microsoft.com/en-us/library/ff654132.aspx
MainViewModel
确定好了命令逻辑实现,我们就可以在MainViewModel定义最终的命令,在构造函数中初始化所有命令并将他们存在一个容器中,在需要更新的时候对相应命令进行更新。
同时,我们在这里初始化NodeInfo和NodeViewModel。这样程序在ViewModel层中的所有操作就完成了。
MainViewModel
class MainViewModel : INotifyPropertyChanged { public NodeViewModel Nodes { get; private set; } public NodeInfo NodeInfo { get; private set; } public ReadOnlyCollection<DelegateCommand> Commands { get; private set; } public MainViewModel() { Commands = new ReadOnlyCollection<DelegateCommand>(new DelegateCommand[] { new DelegateCommand(p=>GetOperationNode().Add(NodeViewModel.GetNextDataName()), "在开头添加"), new DelegateCommand(p=>GetOperationNode().Append(NodeViewModel.GetNextDataName()), "在结尾添加"), new DelegateCommand(p=>NodeInfo.SelectedNode.Rename(),CheckSelection,"重命名"), new DelegateCommand(p=>NodeInfo.SelectedNode.Remove(), CheckSelection,"删除"), new DelegateCommand(p=>NodeInfo.SelectedNode.MoveUp(), CheckSelection,"上移"), new DelegateCommand(p=>NodeInfo.SelectedNode.MoveDown(), CheckSelection,"下移") }); NodeInfo = new NodeInfo(); NodeInfo.SelectedNodeChanged += (s, e) => RefreshCommands(); Nodes = new NodeViewModel(NodeInfo); } NodeViewModel GetOperationNode() { if (NodeInfo.SelectedNode == null) return Nodes; return NodeInfo.SelectedNode; } bool CheckSelection(object obj) { return NodeInfo.SelectedNode != null; } void RefreshCommands() { foreach (var cmd in Commands) if (cmd.HasCanExecuted) cmd.RaiseCanExecuteChanged(); } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string proName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(proName)); } #endregion } |
View层的主要实现
TreeView的DataTemplate和TreeViewItem设置
<HierarchicalDataTemplate x:Key="dtTreeView" ItemsSource="{Binding Children}"> <TextBlock Text="{Binding Name}" /> </HierarchicalDataTemplate> <Style x:Key="stTreeViewItem" TargetType="TreeViewItem"> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /> <Setter Property="FontWeight" Value="Normal" /> <Style.Triggers> <Trigger Property="IsSelected" Value="True"> <Setter Property="FontWeight" Value="Bold" /> </Trigger> </Style.Triggers> </Style> |
命令的绑定
<DataTemplate x:Key="dtCommands"> <ListBox ItemsSource="{Binding}" HorizontalContentAlignment="Stretch" Background="Transparent" Margin="10,3"> <ListBox.ItemTemplate> <DataTemplate> <Button Command="{Binding}" Content="{Binding DisplayName}" Margin="0 3"/> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </DataTemplate> |
MainView
整个ViewModel构建好后,再来讲讲View层,View层应该按照ViewModel的需要来反映(同时也有可能改变ViewModel的数据),注意这个“反映”两个字不仅仅代表者视觉上的输出,还可以执行ViewModel上的命令。
另外View可以是任何控件,不过通常是Window或者UserControl
将一些资源元素省去,MainView的XAML结构一目了然
<UserControl.Resources> <ResourceDictionary Source="Res.xaml"></ResourceDictionary> </UserControl.Resources> <UserControl.DataContext> <loc:MainViewModel></loc:MainViewModel> </UserControl.DataContext> <DockPanel> <HeaderedContentControl Header="命令" Content="{Binding Commands}" ContentTemplate="{StaticResource dtCommands}" DockPanel.Dock="Left"/> <HeaderedContentControl Header="信息" Content="{Binding NodeInfo}" ContentTemplate="{StaticResource dtNodeInfo}" DockPanel.Dock="Top"/> <HeaderedContentControl Header="TreeView"> <TreeView ItemTemplate="{StaticResource dtTreeView}" ItemsSource="{Binding Path=Nodes.Children}" ItemContainerStyle="{StaticResource stTreeViewItem}"/> </HeaderedContentControl> </DockPanel> |
最上面是引用的资源。
下面将View的DataContext设置成MainViewModel,
接着DockPanel显示主界面的三大模块:
左面显示绑定的命令,
右上面是NodeInfo信息,
剩下的是主TreeView。
整个MVVM程序完成。
源代码下载
环境:Microsoft Visual C# 2008 Express Editon
参考文献
WPF Apps With The Model-View-ViewModel Design Pattern
http://msdn.microsoft.com/en-us/magazine/dd419663.aspx
Simplifying the WPF TreeView by Using the ViewModel Pattern
http://www.codeproject.com/KB/WPF/TreeViewWithViewModel.aspx
Working with Checkboxes in the WPF TreeView
http://www.codeproject.com/KB/WPF/TreeViewWithCheckBoxes.aspx
How to implement a reusable ICommand
http://www.wpftutorial.net/DelegateCommand.html
版权
作者:Mgen(刘圆圆),转载请注明此出处。