初步体验数据驱动之美---TreeView

本文对比了WinForm和WPF中树控件的实现方式,介绍了如何利用WPF的数据驱动特性实现界面更新,减少逻辑处理并提高代码清晰度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  

  1.前言

  继上一篇《WPF应用基础篇---TreeView》的发布之后,有部分朋问我关于里面一些基础应用的问题,可能是我写得不够详细,所以在这里,我想再次那文章中的案例来谈谈初步体验数据驱动之美,摆脱旧WinForm编程习惯(靠触发事件来实现界面的变化)。

 2.背景

  

   我们看看以下案例图片的功能如何实现:

   

    图1-1(WinForm两态树)           图1-2(WPF三态树)

  如果我们还处在习惯于WinForm开发的时候,我们首先关注的是,我们需要重写Tree控件,在上一篇文章中有提到过,这里就不再重复。然后当我们布局和设计好数据结构后,我们关心的自然就是选中的时候要做什么,我们首先会考虑到为树节点添加事件来处理相应的逻辑处理。大致实现以下几个步骤(简单的分析)

  • 把sender或者e参数转换为TreeNode
  • 从TreeNode中的Tag数据
  • 根据Tag的类型转换为具体数据
  • 判断TreeNode选中的状态,更改Tag实例的属性的状态如(IsSelected)
  • 根据需求比如:

          全部选中-->父节点CheckBox打钩 同时修改父节点数据,根据当前修改所有子节点状态

          全部未选中-->父节点CheckBox为空 同时修改父节点数据,根据当前修改所有子节点状态

  WinForm具体代码实现两态树:

ExpandedBlockStart.gif View Code
/// <summary>
       
/// 设置父节点状态
       
/// </summary>
       
/// <param name="node"></param>
        public void SetParentNodeStatus(TreeNode node)
        {
           
if (node.Parent != null )
            {
               
bool isChecked = true ;
               
foreach (TreeNode data in node.Parent.Nodes)
                {
                   
if ( ! data.Checked)
                    {
                        isChecked
= false ;
                       
break ;
                    }
                }

               
if (isChecked)
                {
                    node.Parent.Checked
= true ;
                   
if (node.Parent.Parent != null )
                    {
                        SetParentNodeStatus(node.Parent);
                    }
                }
               
else
                {
                    node.Parent.Checked
= false ;
                }
            }
        }

       
/// <summary>
       
/// 设置孩子节点状态
       
/// </summary>
       
/// <param name="node"></param>
        public void SetChildNodeStatus(TreeNode node)
        {
           
if (node.Nodes != null )
            {
               
foreach (TreeNode data in node.Nodes)
                {
                    data.Checked
= node.Checked;
                   
if (data.Nodes != null )
                    {
                        SetChildNodeStatus(data);
                    }
                }
            }
        }

       
/// <summary>
       
/// 树节点被选中后 触发的事件
       
/// </summary>
       
/// <param name="sender"></param>
       
/// <param name="e"></param>
        private void treeView1_AfterCheck( object sender, TreeViewEventArgs e)
        {
          
// isClick是全局变量
            
// 是为了解决无限递归而是用的一个标志
            if ( ! isClick)              
              {
               
return ;
            }

            isClick
= false ;
            TreeNode node
= e.Node;           
           
if (node.Parent != null )
            {
                SetParentNodeStatus(e.Node);
            }
           
if (node.Nodes != null )
            {
                SetChildNodeStatus(node);
            }
            isClick
= true ;
        }

   而当我们开始慢慢采用WPF之后,我们的编程习惯会发生了很大的变化,我们开始有点对触发事件来改变逻辑和界面变化(事件驱动)的做法感到反感。解决上面的问题,我们只需要靠一个接口的帮助,就能实现两态树的功能。

  • 实现INotifyPropertyChanged解口
  • 当数据改变时修改父节点和相应子节点的状态,然后把数据绑定到界面上去。 

  WPF具体代码实现两态树:

  

ExpandedBlockStart.gif View Code
// 是否被选中
         private   bool ?  isSelected;
        
public   bool ?  IsSelected 
        {
            
get  {  return  isSelected; }
            
set
            {
                
if  (isSelected  !=  value)
                {
                    isSelected 
=  value;   
                    ChangeChildNodes(
this );
                    ChangedParentNodes(
this );
                    NotifyPropertyChanged(
" IsSelected " );
                }
            }
        }

///   <summary>
        
///  向下遍历,更改孩子节点状态
        
///  注意:这里的父节点不是属性而是字段
        
///  采用字段的原因是因为不想让父节点触发访问器而触发Setter
        
///   </summary>
        
///   <param name="CurrentNode"></param>
         public   void  ChangeChildNodes(Device CurrentNode)
        {
            
if  (CurrentNode.ChildNodes  !=   null )
            {
                
foreach  (var data  in  CurrentNode.ChildNodes)
                {
                    data.isSelected 
=  CurrentNode.IsSelected;
                    data.NotifyPropertyChanged(
" IsSelected " );
                    
if  (data.ChildNodes  !=   null )
                    {
                        data.ChangeChildNodes(data);
                    }
                }
            }
        }

        
///   <summary>
        
///  向上遍历,更改父节点状态
        
///  注意:这里的父节点不是属性而是字段
        
///  采用字段的原因是因为不想让父节点触发访问器而触发Setter
        
///   </summary>
        
///   <param name="CurrentNode"></param>
         public   void  ChangedParentNode(Device CurrentNode)
        {
            
if  (CurrentNode.ParentNode  !=   null )
            {
                
bool  isCheck  =   true ;
                
foreach  (var data  in  CurrentNode.ParentNode.ChildNodes)
                {
                    
if  (data.IsSelected  !=   true )
                    {
                        isCheck 
=   false ;
                        
break ;
                    }
                }
                CurrentNode.parentNode.isSelected 
=  isCheck;
                CurrentNode.parentNode.NotifyPropertyChanged(
" IsSelected " );
            }
        }

  从两段代码可以看出,WinForm实现代码是事件驱动,首先触发一个事件,然后进行一些逻辑判断,而且还需要借助全部变量IsClick来防止代码无限递归。而WPF的实现则是靠数据驱动,数据变化了,然后才调用方法来更改数据的相应状态。最后才通知界面刷新数据。其实可以看出现在的需求很简单就是,根据节点选中状态操作树,但是如果我的需求变化了,例如图1-2的需求一样,如果我需要打钩的时候,操作按钮的状态,比如打钩就连接,不打钩则断开。WinForm的话又要在代码中做一些逻辑判断,这很容易实现,但是如果我断开按钮按下的时候,只能点击连接,这时候WinForm的事件就要做很多逻辑处理,如果需求要求的功能多的话,事件的后台代码将越来越复杂,最后导致逻辑混乱。而WPF实现的话,则是根据数据变化而且在界面上显示,当我点击的时候,修改下数据的状态则可以。后台无需要做太多的处理,这样代码结构和逻辑会变得相对清晰。

 3.三态树具体实现

  这里将为大家介绍下三态树在WPF中的实现,也是对上一篇文章的补充。本案例是在基于MVVM的基础上实现的。要实现图1-2(三态树)只需要做以下两个步骤。

  • 定义好数据结构,并在数据上通过实现INotifyPropertyChanged接口,来属性变化后通知View刷新数据。
  • 把想对应的属性Binding到View的控件上。

  

  数据结构实体代码:

  

ExpandedBlockStart.gif View Code
///   <summary>
    
///  设备基类
    
///   </summary>
     public   class  Device:INotifyPropertyChanged
    {
        
// 是否被选中
         private   bool ?  isSelected;
        
public   bool ?  IsSelected 
        {
            
get  {  return  isSelected; }
            
set
            {
                
if  (isSelected  !=  value)
                {
                    isSelected 
=  value;   
                    ChangeChildNodes(
this );
                    ChangedParentNodes(
this );
                    NotifyPropertyChanged(
" IsSelected " );
                }
            }
        }
        
        
private  DeviceStatus status;
        
public  DeviceStatus Status
        {
            
get  {  return  status; }
            
set
            {
                
if  (status  !=  value)
                {
                    status 
=  value;
                    NotifyPropertyChanged(
" Status " );
                }
            }
        }

        
public   string  Name {  get set ; }
        
public   string  ImageUrl{ get ; set ;}

        
private  List < Device >  childNodes;
        
public  List < Device >  ChildNodes
        {
            
get  {  return  childNodes; }
            
set
            {
                
if  (childNodes  !=  value)
                {
                    childNodes 
=  value;
                    NotifyPropertyChanged(
" ChildNodes " );
                }
            }
        }

        
private  Device parentNode;
        
public  Device ParentNode
        {
            
get  {  return  parentNode; }
            
set
            {
                
if  (parentNode  !=  value)
                {
                    parentNode 
=  value;
                    NotifyPropertyChanged(
" ParentNode " );
                }
            }
        }

        
///   <summary>
        
///  向下遍历,更改孩子节点状态
        
///  注意:这里的父节点不是属性而是字段
        
///  采用字段的原因是因为不想让父节点触发访问器而触发Setter
        
///   </summary>
        
///   <param name="CurrentNode"></param>
         public   void  ChangeChildNodes(Device CurrentNode)
        {
            
if  (CurrentNode.ChildNodes  !=   null )
            {
                
foreach  (var data  in  CurrentNode.ChildNodes)
                {
                    data.isSelected 
=  CurrentNode.IsSelected;
                    data.NotifyPropertyChanged(
" IsSelected " );
                    
if  (data.ChildNodes  !=   null )
                    {
                        data.ChangeChildNodes(data);
                    }
                }
            }
        }

        
///   <summary>
        
///  向上遍历,更改父节点状态
        
///  注意:这里的父节点不是属性而是字段
        
///  采用字段的原因是因为不想让父节点触发访问器而触发Setter
        
///   </summary>
        
///   <param name="CurrentNode"></param>
         public   void  ChangedParentNodes(Device CurrentNode)
        {
            
if  (CurrentNode.ParentNode  !=   null )
            {
                
bool ?  parentNodeState  =   true ;
                
int  selectedCount  =   0 ;   // 被选中的个数
                 int  noSelectedCount  =   0 ;     // 不被选中的个数

                
foreach  (var data  in  CurrentNode.ParentNode.ChildNodes)
                {
                    
if  (data.IsSelected  ==   true )
                    {
                        selectedCount
++ ;
                    }
                    
else   if  (data.IsSelected  ==   false )
                    {
                        noSelectedCount
++ ;
                    }
                }

                
// 如果全部被选中,则修改父节点为选中
                 if  (selectedCount  ==  
                    CurrentNode.ParentNode.ChildNodes.Count)
                {
                    parentNodeState 
=   true ;
                }
                
// 如果全部不被选中,则修改父节点为不被选中
                 else   if  (noSelectedCount  ==  
                    CurrentNode.ParentNode.ChildNodes.Count)
                {
                    parentNodeState 
=   false ;
                }
                
// 否则标记父节点(例如用实体矩形填满)
                 else
                {
                    parentNodeState 
=   null ;
                }

                CurrentNode.parentNode.isSelected 
=  parentNodeState;
                CurrentNode.parentNode.NotifyPropertyChanged(
" IsSelected " );

                
if  (CurrentNode.ParentNode.ParentNode  !=   null )
                {
                    ChangedParentNodes(CurrentNode.parentNode);
                }
            }
        }

        
public   void  NotifyPropertyChanged( string  name)
        {
            
if (PropertyChanged != null )
            PropertyChanged(
this , new  PropertyChangedEventArgs(name));
        }
        
public   event  PropertyChangedEventHandler PropertyChanged;
    }

  

  View具体实现代码:

  

ExpandedBlockStart.gif View Code
< CheckBox IsChecked = " {Binding IsSelected,Mode=TwoWay} "  Margin = " 2 "  VerticalAlignment = " Center " />

    这里只需要把实体的IsSelected属性Bingding到View上,Mode是双向的就可以了,具体的逻辑有实体内部做处理,这样更能体现出View中代码的干净,而且更能让View和ViewModel耦合性降到最低。实现三态树的时候有一个小技巧,让代码避开了无限递归的问题,这里采用属性如IsSelected,属性有setter和gettter访问器,当我们向上、下遍历的时候,改变的是数据中的字段isSelected,这样就不会触发了属性的setter。这也是数据驱动的一个优点之一。

  4.总结     

  WPF的主要思想是用数据驱动来代替事件驱动。当数据发生变化的时候才做出一些相应的处理。这样的好处就是:

  • 使得代码逻辑更加清晰。
  • 可以让数据发生变化,通过属性访问器来控制相应的逻辑变化(其实也是数据变化),最后通知View。这样简化了逻辑处理而且减少了逻辑混乱的局面。
  • 有利于降低View和ViewModel(或后台具体实现代码)之间的耦合度,也就是说有利于把强依赖关系转为弱依赖甚至没依赖关系。  

  5.附加源码:点击下载      

转载于:https://www.cnblogs.com/smlAnt/archive/2011/08/09/2130334.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值