WPF应用基础篇---TreeView

  1.前言

    最近有部分朋友经常问我,WPF的TreeView控件,如何用MVVM来实现绑定和显示?所以写下了这篇WPF应用基础篇---TreeView.

 2.介绍

  • 案例浏览:

    

                  图 1-1(案例结构图)

  • 目的:本文中做了三个简单的Demo给刚刚入门或者入门不久而且不熟悉TreeView控件在MVVM中具体实现的朋友们。希望以下3个例子能够给他们带来帮助。
  • 背景:Demo是采用现实生活中一个大网络的某一部分网络来作为案例。这里为了演示方便,整个网络由路由器、交换机、集线器等服务器组成。他们的之间的关系是多对多的关系,一个网络中有可能一个路由器包含了多个路由器、交换机、集线器;而且交换机、集线器也是相同的原理。
  • 数据:本文中用到的数据随机产生的测试数据。根据界面中树的深度(下拉框)来选择树最多有多少层,然后创建树结构的数据。这里需要注意的是我们TreeView提供的数据源必须是树结构的;为什么需要树结构的数据呢?大家可能会觉得很奇怪,其实,我们ViewModel要将数据Binding到TreeView控件上就必须指定一个ItemsSource,所以必须把节点的子节点集合绑定到模板中的ItemsSource中。
  • 案例解析:

  整个Demo分为两部分:左边是功能菜单,右边是显示具体内容,可以参考图1-1。

  基础数据:为了实现一下案例功能,我建立了一个SmlAnt.DataLibrary的数据类库,专门提供原始基本类型和基本数据。下面是具体代码:

  

  实体类:

  

  

View Code
  1 namespace DataLibrary
  2 {
  3     /// <summary>
  4     /// 设备状态
  5     /// </summary>
  6     public enum DeviceStatus
  7     {
  8         Connected,Off
  9     }
 10 
 11     /// <summary>
 12     /// 设备基类
 13     /// </summary>
 14     public class Device:INotifyPropertyChanged
 15     {
 16         //是否被选中
 17         private bool? isSelected;
 18         public bool? IsSelected 
 19         {
 20             get { return isSelected; }
 21             set
 22             {
 23                 if (isSelected != value)
 24                 {
 25                     isSelected = value;   
 26                     ChangeChildNodes(this);
 27                     ChangedParentNodes(this);
 28                     NotifyPropertyChanged("IsSelected");
 29                 }
 30             }
 31         }
 32         
 33         private DeviceStatus status;
 34         public DeviceStatus Status
 35         {
 36             get { return status; }
 37             set
 38             {
 39                 if (status != value)
 40                 {
 41                     status = value;
 42                     NotifyPropertyChanged("Status");
 43                 }
 44             }
 45         }
 46 
 47         public string Name { getset; }
 48         public string ImageUrl{get;set;}
 49 
 50         private List<Device> childNodes;
 51         public List<Device> ChildNodes
 52         {
 53             get { return childNodes; }
 54             set
 55             {
 56                 if (childNodes != value)
 57                 {
 58                     childNodes = value;
 59                     NotifyPropertyChanged("ChildNodes");
 60                 }
 61             }
 62         }
 63 
 64         private Device parentNode;
 65         public Device ParentNode
 66         {
 67             get { return parentNode; }
 68             set
 69             {
 70                 if (parentNode != value)
 71                 {
 72                     parentNode = value;
 73                     NotifyPropertyChanged("ParentNode");
 74                 }
 75             }
 76         }
 77 
 78         /// <summary>
 79         /// 向下遍历,更改孩子节点状态
 80         /// 注意:这里的父节点不是属性而是字段
 81         /// 采用字段的原因是因为不想让父节点触发访问器而触发Setter
 82         /// </summary>
 83         /// <param name="CurrentNode"></param>
 84         public void ChangeChildNodes(Device CurrentNode)
 85         {
 86             if (ChildNodes != null)
 87             {
 88                 foreach (var data in childNodes)
 89                 {
 90                     data.isSelected = CurrentNode.IsSelected;
 91                     CurrentNode.NotifyPropertyChanged("IsSelected");
 92                     if (data.ChildNodes != null)
 93                     {
 94                         data.ChangeChildNodes(data);
 95                     }
 96                 }
 97             }
 98         }
 99 
100         /// <summary>
101         /// 向上遍历,更改父节点状态
102         /// 注意:这里的父节点不是属性而是字段
103         /// 采用字段的原因是因为不想让父节点触发访问器而触发Setter
104         /// </summary>
105         /// <param name="CurrentNode"></param>
106         public void ChangedParentNodes(Device CurrentNode)
107         {
108             if (CurrentNode.ParentNode != null)
109             {
110                 bool? parentNodeState = true;
111                 int selectedCount = 0;  //被选中的个数
112                 int noSelectedCount = 0;    //不被选中的个数
113 
114                 foreach (var data in CurrentNode.ParentNode.ChildNodes)
115                 {
116                     if (data.IsSelected == true)
117                     {
118                         selectedCount++;
119                     }
120                     else if (data.IsSelected == false)
121                     {
122                         noSelectedCount++;
123                     }
124                 }
125 
126                 //如果全部被选中,则修改父节点为选中
127                 if (selectedCount == 
128                     CurrentNode.ParentNode.ChildNodes.Count)
129                 {
130                     parentNodeState = true;
131                 }
132                 //如果全部不被选中,则修改父节点为不被选中
133                 else if (noSelectedCount == 
134                     CurrentNode.ParentNode.ChildNodes.Count)
135                 {
136                     parentNodeState = false;
137                 }
138                 //否则标记父节点(例如用实体矩形填满)
139                 else
140                 {
141                     parentNodeState = null;
142                 }
143 
144                 CurrentNode.parentNode.isSelected = parentNodeState;
145                 CurrentNode.parentNode.NotifyPropertyChanged("IsSelected");
146 
147                 if (CurrentNode.ParentNode.ParentNode != null)
148                 {
149                     ChangedParentNodes(CurrentNode.parentNode);
150                 }
151             }
152         }
153 
154         public void NotifyPropertyChanged(string name)
155         {
156             if(PropertyChanged!=null)
157             PropertyChanged(this,new PropertyChangedEventArgs(name));
158         }
159         public event PropertyChangedEventHandler PropertyChanged;
160     }
161 
162     /// <summary>
163     /// 路由器
164     /// </summary>
165     public class Router : Device
166     {
167 
168     }
169 
170     /// <summary>
171     /// 交换机
172     /// </summary>
173     public class Switcher : Device
174     {
175 
176     }
177 
178     /// <summary>
179     /// 集线器
180     /// </summary>
181     public class Concentrator : Device
182     {
183 
184     }
185 }

  

  

  数据工厂:

  

View Code
  1 public class DataFactory
  2     {
  3         /// <summary>
  4         /// 随机数据产生器
  5         /// </summary>
  6         static Random random = new Random();        
  7 
  8         /// <summary>
  9         /// 根据参数获取设备状态
 10         /// </summary>
 11         /// <param name="intValue"></param>
 12         /// <returns></returns>
 13         private static DeviceStatus GetStatus(int intValue)
 14         {
 15             return intValue % 2 == 0 ? DeviceStatus.Off : DeviceStatus.Connected;
 16         }
 17         
 18         /// <summary>
 19         /// 
 20         /// </summary>
 21         /// <param name="intValue"></param>
 22         /// <returns></returns>
 23         private static String GetName(int intValue)
 24         {
 25             string refValue = "路由器";
 26             if (intValue % 3 == 0)
 27             {
 28                 refValue = "路由器";
 29             }
 30             else if (intValue % 3 == 1)
 31             {
 32                 refValue = "交换机";
 33             }
 34             else
 35             {
 36                 refValue = "集线器";
 37             }
 38             return refValue;
 39         }
 40 
 41         /// <summary>
 42         /// 根据参数创建设备(简单工厂-参数工厂)
 43         /// </summary>
 44         /// <param name="typeValue"></param>
 45         /// <returns></returns>
 46         public static Device DeviceFactory(int typeValue)
 47         {
 48             Device refEntity = null;
 49             if (typeValue % 3 == 0)
 50             {
 51                 refEntity = new Router();
 52             }
 53             else if (typeValue % 3 == 1)
 54             {
 55                 refEntity = new Switcher();
 56             }
 57             else
 58             {
 59                 refEntity = new Concentrator();
 60             }
 61             return refEntity;
 62         }
 63 
 64         /// <summary>
 65         /// 随即获取基类设备数据
 66         /// </summary>
 67         /// <param name="level">当前节点所在层</param>
 68         /// <param name="MaxLevel">树最大深度</param>
 69         /// <returns>设备树</returns>
 70         public static List<Device> GetBaseTypeDevices(int level, int MaxLevel)
 71         {
 72             level++;
 73             var count = random.Next(610);
 74             List<Device> listTo = new List<Device>();
 75             for (int i = 1; i < count; i++)
 76             {
 77                 Device entity = new Device();
 78                 var typeValue = random.Next(16);
 79                 entity.Name = GetName(typeValue);
 80                 entity.ImageUrl = "..\\..\\Resource\\" + entity.Name + ".png";
 81                 entity.Status = GetStatus(typeValue);
 82                 if (level <= MaxLevel)
 83                     entity.ChildNodes = GetBaseTypeDevices(level, MaxLevel);
 84                 listTo.Add(entity);
 85             }
 86             return listTo;
 87         }
 88 
 89         /// <summary>
 90         /// 随即获取所有子类型设备数据
 91         /// </summary>
 92         /// <param name="level">当前节点所在层</param>
 93         /// <param name="MaxLevel">树最大深度</param>
 94         /// <returns>设备树</returns>
 95         public static List<Device> GetAllTypeDevice(int level,int MaxLevel)
 96         {
 97             level++;
 98             var count = random.Next(610);
 99             List<Device> listTo = new List<Device>();
100             for (int i = 1; i < count; i++)
101             {
102                 var typeValue = random.Next(16);
103                 Device entity = DeviceFactory(typeValue);                
104                 entity.Name = GetName(typeValue);
105                 entity.ImageUrl = "..\\..\\Resource\\" + entity.Name + ".png";
106                 entity.Status = GetStatus(typeValue); 
107                 if (level <= MaxLevel)
108                     entity.ChildNodes = GetAllTypeDevice(level,MaxLevel);
109                 listTo.Add(entity);
110             }
111             return listTo;
112         }
113 
114         /// <summary>
115         /// 随即获取所有子类型设备数据
116         /// </summary>
117         /// <param name="level">当前节点所在层</param>
118         /// <param name="MaxLevel">树最大深度</param>
119         /// <param name="parentNode">父节点</param>
120         /// <returns>设备树</returns>
121         public static List<Device> GetAllTypeDevice(int level, int MaxLevel, Device parentNode)
122         {
123             level++;
124             var count = random.Next(610);
125             List<Device> listTo = new List<Device>();
126             for (int i = 1; i < count; i++)
127             {
128                 var typeValue = random.Next(16);
129                 Device entity = DeviceFactory(typeValue);
130                 entity.IsSelected = false;
131                 entity.Name = GetName(typeValue);
132                 entity.ParentNode = parentNode;
133                 entity.ImageUrl = "..\\..\\Resource\\" + entity.Name + ".png";
134                 entity.Status = GetStatus(typeValue);               
135                 if (level <= MaxLevel)
136                     entity.ChildNodes = GetAllTypeDevice(level, MaxLevel, entity);
137                 listTo.Add(entity);
138             }
139             return listTo;
140         }
141     }

 

 

  案例一,主要为大家介绍如何创建一个无限级的树,其实说简单点就是采用HierarchicalDataTemplate 作为树模板,然后通过Binding把数据绑定到树上。因为模板是HierarchicalDataTemplate这个模板,这里就不详细讲解,如果了解多点可以到MSDN,所以会无限级别的增加,只要数据结构上能支持,数据有多少级别,View中显示的树也会对应有多少级别。而如果采用的是DataTemplate的话,则只能有一层的数据。

  效果图如下:

  

        图 1-2(无限级别树)

  View(XAML)代码 代码1-3:

  

View Code
1 <HierarchicalDataTemplate x:Key="TreeViewTemplate" ItemsSource="{Binding ChildNodes}">
2             <StackPanel Orientation="Horizontal">
3                 <Image Source="{Binding ImageUrl}" Margin="2"/>
4                 <TextBlock Text="{Binding Name}" Margin="2"/>
5             </StackPanel>
6         </HierarchicalDataTemplate>
7 
8 <TreeView Grid.Row="1" ItemTemplate="{StaticResource TreeViewTemplate}" ItemsSource="{Binding DataSource}" Margin="5"/>

  

  ViewModel代码:

View Code
 1 private List<Device> dataSource;
 2         public List<Device> DataSource
 3         {
 4             get { return dataSource; }
 5             set
 6             {
 7                 if (dataSource != value)
 8                 {
 9                     dataSource = value;
10                     RaisePropertyChanged("DataSource");
11                 }
12             }
13         }
14 
15 DataSource = DataFactory.GetBaseTypeDevices(1, SelectedLevel);

   

  

  案例二,主要给大家讲解的是,如何采用DataTmeplateSelector通过重写SelectTemplate方法来实现的。来控制显示样式、右键菜单等功能。这里主要讲的是,不同服务器之间显示不一样,而且连快捷菜单也对应不一样。这里有个特别说明的是:因为功能显示的需求,这里把集线器定义为没有子设备的模板。还有另外一个功能就是当我按下重启的时候,断开按钮就不能使用。这里用到的是Command。园里前辈们写了很多这方面的文章,我这里就不对ICommand进行详细讨论。

  效果图:图1-1

  快捷菜单(如下图):

  

  

  图 1-3(路由器快捷菜单)   图 1-4(交换机快捷菜单)       图1-5(集线器快捷菜单)

  快捷菜单代码:

  

View Code
 1 <ContextMenu x:Key="RouterMenu">
 2             <MenuItem Header="启动路由器">
 3                 <MenuItem.Icon>
 4                     <Image Source="..\..\Resource\Connect.png"/>
 5                 </MenuItem.Icon>
 6             </MenuItem>
 7             <MenuItem Header="断开路由器">
 8                 <MenuItem.Icon>
 9                     <Image Source="..\..\Resource\Break.png"/>
10                 </MenuItem.Icon>
11             </MenuItem>
12         </ContextMenu>
13         <ContextMenu x:Key="SwitchMenu">
14             <MenuItem Header="启动交换机">
15                 <MenuItem.Icon>
16                     <Image Source="..\..\Resource\Connect.png"/>
17                 </MenuItem.Icon>
18             </MenuItem>
19             <MenuItem Header="断开交换机">
20                 <MenuItem.Icon>
21                     <Image Source="..\..\Resource\Break.png"/>
22                 </MenuItem.Icon>
23             </MenuItem>
24         </ContextMenu>
25         <ContextMenu x:Key="ConcentratorMenu">
26             <MenuItem Header="启动集线器">
27                 <MenuItem.Icon>
28                     <Image Source="..\..\Resource\Connect.png"/>
29                 </MenuItem.Icon>
30             </MenuItem>
31             <MenuItem Header="断开集线器">
32                 <MenuItem.Icon>
33                     <Image Source="..\..\Resource\Break.png"/>
34                 </MenuItem.Icon>
35             </MenuItem>
36         </ContextMenu>

  TreeView模板代码:

View Code
 1 xmlns:LocalTmeplate="clr-namespace:Smlant.DataTemplates"      
 2 
 3 <LocalTmeplate:ContextMenuDataTemplateSelector x:Key="ContextMenuDataTemplateSelector"/>
 4 
 5 <!--交换机模板-->
 6         <HierarchicalDataTemplate x:Key="SwitchTemplate" ItemsSource="{Binding ChildNodes}" DataType="{x:Type DataLib:Switcher}">
 7             <StackPanel Orientation="Horizontal" ContextMenu="{StaticResource SwitchMenu}">
 8                 <Image Source="{Binding ImageUrl}" Margin="2"/>
 9                 <TextBlock Text="{Binding Name}" Margin="2" VerticalAlignment="Center"/>
10                 <Button Margin="2" Command="{Binding DataContext.OffCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
11                         CommandParameter="{Binding}">
12                     <StackPanel>
13                         <Image Source="..\..\Resource\Connect.png" ToolTip="重新连接"/>
14                     </StackPanel>
15                 </Button>
16                 <Button Margin="2" Command="{Binding DataContext.ConnectionCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
17                         CommandParameter="{Binding}">
18                     <StackPanel>
19                         <Image Source="..\..\Resource\Break.png" ToolTip="断开连接"/>
20                     </StackPanel>
21                 </Button>
22             </StackPanel>
23         </HierarchicalDataTemplate>
24         <!--路由器模板-->
25         <HierarchicalDataTemplate x:Key="RouterTemplate" ItemsSource="{Binding ChildNodes}" DataType="{x:Type DataLib:Router}">
26             <StackPanel Orientation="Horizontal" ContextMenu="{StaticResource RouterMenu}">
27                 <Image Source="{Binding ImageUrl}" Margin="2"/>
28                 <TextBlock Text="{Binding Name}" Margin="2" VerticalAlignment="Center"/>
29                 <Button Margin="2" Content="重启路由" Command="{Binding DataContext.OffCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
30                         CommandParameter="{Binding}">
31                 </Button>
32                 <Button Margin="2" Content="断开连接"  Command="{Binding DataContext.ConnectionCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
33                         CommandParameter="{Binding}">
34                 </Button>
35             </StackPanel>
36         </HierarchicalDataTemplate>
37         <!--集线器模板-->
38         <DataTemplate x:Key="ConcentratorTemplate" DataType="{x:Type DataLib:Concentrator}">
39             <StackPanel Orientation="Horizontal" ContextMenu="{StaticResource ConcentratorMenu}">
40                 <Image Source="{Binding ImageUrl}" Margin="2"/>
41                 <TextBlock Text="{Binding Name}" Margin="2" VerticalAlignment="Center"/>
42                 <Button Margin="2" Content="重新连接" Command="{Binding DataContext.OffCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
43                         CommandParameter="{Binding}"/>
44                 <Button Margin="2" Content="断开连接"  Command="{Binding DataContext.ConnectionCommand,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=UserControl}}"
45                         CommandParameter="{Binding}"/>
46             </StackPanel>
47         </DataTemplate>

  DataTemplateSelector代码:

  

View Code
 1 public class ContextMenuDataTemplateSelector:DataTemplateSelector
 2     {
 3         public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
 4         {
 5             FrameworkElement element = container as FrameworkElement;
 6             DataTemplate template = null;
 7             if (item is Router)
 8             {
 9                 template = element.FindResource("RouterTemplate"as HierarchicalDataTemplate;
10             }
11             else if (item is Switcher)
12             {
13                 template = element.FindResource("SwitchTemplate"as HierarchicalDataTemplate;
14             }
15             else if (item is Concentrator)
16             {
17                 template = element.FindResource("ConcentratorTemplate"as DataTemplate;
18             }
19             return template;
20         }
21     }

  ViewModel代码: 

  

View Code
 1 private List<Device> dataSource;
 2         public List<Device> DataSource
 3         {
 4             get { return dataSource; }
 5             set
 6             {
 7                 if (dataSource != value)
 8                 {
 9                     dataSource = value;
10                     RaisePropertyChanged("DataSource");
11                 }
12             }
13         }
14 
15  DataSource = DataFactory.GetAllTypeDevice(1, SelectedLevel);

 

  

 案例三,主要跟大家分享的是,如何在TreeView上实现三态树的功能。具体什么是三态树的话我在这里就不多说了。以下是案例三的具体结构图和代码:

  结构图:

  

       图 1-6(三态树)

  代码:具体代码实现在上面的实体类代码的 IDevice中实现。请参考上面代码。

  

  

  3.个人观点

    很多朋友都抱怨说WPF的TreeView是一个很麻烦的东西,而且不好用。这点我持反对的意见,每一种新东西,在我们还不熟悉的时候,是挺麻烦的。但是WPF--TreeView较WinForm--Tree来说,WPF提供一个强大的模板功能,能让我们根据自己的需要,灵活地更换模板。如果在做WinForm开发的时候,我想实现一棵树上保存N种数据类型的数据,而且根据不同的类型,在节点上显示不一样的状态和样式,也许你会花很多的时间来重写Tree的控件,而WPF提供了一个模板功能,而且具体的模板是我们自己来实现的。

  

  4.附加代码:

    百度网盘  :http://pan.baidu.com/s/1kVqRyrt

    密码:cm4k

  

 

posted @ 2011-07-31 01:55  Smlant.  阅读(11649)  评论(33编辑  收藏