程序功能

程序使用MVVM模式实现了对WPF TreeView中节点的添加,重命名,删除,上(下)移动,并且可以统计当前TreeView选择的节点和全部接点个数。

 

(截图)

image

 

摘要:

  • TreeView特点
  • 节点的操作源:NodeViewModel
  • 掌握TreeView的信息:NodeInfo
  • 你的命令逻辑
  • MainViewModel
  • View层的主要实现
  • MainView
  • 原代码下载
  • 参考文献

 

TreeView的特点

无论是哪种UI框架,TreeView都显得比较特殊,WPF中的TreeView亦如此。让我们来简要的看一下TreeView在WPF中的特殊地位:

 

image

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

image

 

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(刘圆圆),转载请注明此出处。

 

 

作者: _Mgen 发表于 2011-06-20 17:39 原文链接

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架