TreeView漫游者—封装递归操作的复杂性(原创翻译)

简介:

最近在封装TreeView的时候发现了一篇十分优秀的文章,在这里给大家翻译出来共享一下,^_^。

原文标题:TreeViewWalker - Simplifying Recursion
原文地址:http://www.codeproject.com/cs/miscctrl/TreeViewWalker.asp
源码地址:http://www.codeproject.com/cs/miscctrl/TreeViewWalker/TreeViewWalker_src.zip


        
介绍

使用TreeView控件的时候,经常要用到递归操作(一个方法自己调用自己)。一些开发者花了很多时间来实现或维护递归方法。在本文中奉上的TreeviewWalker类是被创建用来帮助大家从商业逻辑中抽象出递归方法的;让我们可以创建一个简单易用的TreeView漫游程序模型。
    
背景知识

TreeViewWalker类使用了两个开发者必须知道的感念:兄弟节点(Sibling nodes)和后代节点(Descendant nodes)。如果你还不熟悉这些感念,下面的解释将为你阐明它们。

兄弟节点(Sibling nodes)

节点是在相同节点集合(TreeNodeCollection)里的所有其他节点的兄弟。在文章上面的截屏图片里,"Lunch" 和"Dinner" 是兄弟。"Ham Sandwich" 和"Risotto" 不是, 因为他们属于不同的节点结合。

后代节点(Descendant nodes)

一个后代节点是在另一个节点的节点集合里的节点,或者是哪个节点的孩子们中的一个,或者是孩子们中的某一个的孩子,等等。在上面的截屏图象中,"Apple Sauce" 是"Meals"、 "Lunch"和"Food"的后代. "Bread and Butter" 并不是"Lunch"的后代。

代码使用

TreeView漫游者使用相当简单。你创建一个类的实例,指定要操纵的TreeView,绑定ProcessNode事件,然后再调用ProcessTree方法即可。例如:

1 TreeViewWalker treeViewWalker  =   new  TreeViewWalker(  this .treeView );
2 treeViewWalker.ProcessNode  +=   new  ProcessNodeEventHandler( treeViewWalker_ProcessNode );
3 treeViewWalker.ProcessTree();

TreeViewWalker遇到的每个节点都会触发ProcessNode事件。所有节点以自上而下的方式导航,当一个节点的所有后代节点都访问过以后,将继续为该节点的下一个兄弟节点触发ProcessNode事件。为了更具体地解释它的用法,请看文章顶部的Demo程序的屏幕截图。在这个图中的情况下,ProcessNode事件将最先被这些节点顺序触发: "Meals", "Lunch", "Food", "Ham Sandwich", "Apple Sauce", "Drinks", "Iced Tea", "Dinner", 等等。
使用这个类的最后一步就是一创建一个绑定到ProcessNode事件的方法。下面是一个这样方法的实例,它用来反转(toggle)树中每个TreeNode的Checked属性。

1 private   void  treeViewWalker_ProcessNode(  object  sender, ProcessNodeEventArgs e )
2 {
3    e.Node.Checked = ! e.Node.Checked;
4}

5
6

    
ProcessNodeEventArgs
事件类型暴露了一些你在操作树节点的时候能够用到的属性。
  
Node
 
- 返回被处理的树节点. 
ProcessDescendants
- 获取/设置一个值,用来决定ProcessNode是否会被当前树节点的所有子节点触发。默认值为true。如果StopProcessing属性被设置成true,这个属性将被忽略。
ProcessSiblings 
- 获取/设置一个值,用来决定ProcessNode是否会被当前树节点的所有未处理过的兄弟节点触发。默认值为true。如果StopProcessing属性被设置成true,这个属性将被忽略。
StopProcessing 
- 获取/设置一个值,用来决定ProcessNode是否会被当前树节点的没被操作过的剩余节点触发。如果这个属性被设置为true,ProcessDescendant属性和ProcessSiblings属性都将被忽略。

假如你确认不再需要让TreeView漫游者遍历节点的后代。在这种情况下你可以设置ProcessDescendants属性为false即可,参考下面的例子:

 1 private   void  treeViewWalker_ProcessNode_HighlightFoodNodes( object  sender, ProcessNodeEventArgs e )
 2 {
 3    if( e.Node.FullPath.IndexOf( "Food" ) > -1 )
 4    {
 5        e.Node.BackColor = Color.LightGreen;
 6    }

 7    else if( e.Node.Text == "Drinks" || e.Node.Text == "Dessert" )
 8    {
 9        // 不需要处理Drinks或Dessert分支的任何节点。
10        // 所以告诉漫游者跳过它们。
11        e.ProcessDescendants = false;
12    }

13}

14
15

如果不再需要TreeViewWalker遍历节点的兄弟节点,则设置ProcessSiblings属性为false即可,参考下面的例子:

 1 private   void  treeViewWalker_ProcessNode_HighlightSecondNodeInEachNodeIsland( object  sender, ProcessNodeEventArgs e)
 2 {
 3    if( e.Node.Index == 1 )
 4    {
 5        e.Node.BackColor = Color.LightGreen;
 6        // 每次分支里的第二个节点被高亮显示,不需要再
 7        // 处理相同分支下面的其他节点,所有告诉漫游者
 8        // 不要再为兄弟节点触发ProcessNode事件。
 9        e.ProcessSiblings = false;
10    }

11}

让TreeViewWalker完全停止遍历树,仅需要设置StopProcessing属性为true即可,就象:

 1 private   void  treeViewWalker_ProcessNode_HighlightAsparagusNode(  object  sender, ProcessNodeEventArgs e )
 2 {
 3    if( e.Node.Text == "Asparagus" )
 4    {
 5        e.Node.BackColor = Color.LightGreen;
 6
 7        // 如果"Asparagus" 节点已经发现,就告诉漫游者停止操作。
 8        e.StopProcessing = true;
 9    }

10}

如果你想处理树中的某个特殊的节点分支,那么可以调用TreeView漫游者的ProcessBranch方法,并且传递分支的根节点给它。Demo程序在文章开头示范过。


      
高级应用

到目前为止,我只演示了如何在简单的情况下使用TreeViewWalker。现在你已经知道了如果了为什么和如果使用它了,接下来让我们看一个更复杂的例子。这个实例演示了在把树节点按需要的方式载入的时候,如何使用TreeViewWalker。它向用户提供了一个文本框和四个可以在树节点里导航的按纽来实现在树中搜索包含文本框内容的节点。树里的节点即是用户机器的目录集合。
就象上面的截屏图象一样,上面有四个导航按纽;等价于第一个,上一个,下一个,和最后一个。上一个和下一个按纽分别是用来搜索当前TreeView里选中节点的上一个和下一个节点。因为节点是在需要的时候加载的,且搜索逻辑必须遍历树上的每一个逻辑节点,所以动态加载节点的过程必须发生在ProcessNode事件绑定的方法中。从另一个角度来说,由于TreeView不总是包含所有节点,所以节点必须在执行查询的时候动态加载。

变量

每个按纽对应一个TreeView漫游者。还有两个辅助的变量用来帮助执行查询操作,如下面:

1 // 每个搜索按纽都有一个漫游者。
2 // 它们都是在InitializeAdvancedDemo()方法里配置的。
3 private  TreeViewWalker tvWalkerFirst;
4 private  TreeViewWalker tvWalkerPrev;
5 private  TreeViewWalker tvWalkerNext;
6 private  TreeViewWalker tvWalkerLast;
7 // 用来辅助搜索的两个变量
8 private  TreeNode matchingNode;
9 private   bool  processedSelectedNode;

找到第一个匹配节点

让我们来看看当用户点击"First"按纽的时候发生了什么。一发现节点名字里包含要搜索的字符串,就将节点选中且通知TreeView漫游者停止继续遍历。如果没有匹配的节点,则必须检查后代节点。不管怎样,因为节点是在需要的时候才加载的,所以节点的子节点可能还没有加载。如果是这样的话,TreeView漫游者将出发ProcessNode事件来加载子节点。

 1 private   void  btnFirst_Click( object  sender, System.EventArgs e)
 2 {
 3    this.tvWalkerFirst.ProcessTree();
 4}

 5
 6 private   void  tvWalkerFirst_ProcessNode( object  sender, ProcessNodeEventArgs e)
 7 {
 8    //一发现匹配的节点,节点就会被选中,漫游者也将停止漫游。
 9    ifthis.ContainsSearchText( e.Node ) )
10    {
11        this.treeAdvanced.SelectedNode = e.Node;
12        e.StopProcessing = true;
13    }

14    else ifthis.HasDummyNode( e.Node ) )
15        this.LoadDirectoryNodes( e.Node );
16}

17

找到上一个匹配节点

当用户点击"Previous"按纽的时候,需要更多的逻辑操作。基本的想法是我们允许为被选中节点之前的所有节点触发ProcessNode事件。最后一个匹配的节点就是我们要找的"previous"节点。(相对于被选中的节点)

 1 private   void  btnPrev_Click( object  sender, System.EventArgs e)
 2 {
 3    this.matchingNode = null;            
 4    this.tvWalkerPrev.ProcessTree();
 5}

 6
 7 private   void  tvWalkerPrev_ProcessNode( object  sender,ProcessNodeEventArgs e)
 8 {
 9    //我们最近发现的匹配节点就是"previous"节点,并且将会被选中。
10    if( e.Node == this.treeAdvanced.SelectedNode )
11    {
12        ifthis.matchingNode != null )
13            this.treeAdvanced.SelectedNode = this.matchingNode;
14        e.StopProcessing = true;
15        return;
16    }

17    
18    ifthis.ContainsSearchText( e.Node ) )
19        this.matchingNode = e.Node;
20
21    ifthis.HasDummyNode( e.Node ) )
22        this.LoadDirectoryNodes( e.Node );
23}

找到下一个匹配节点

搜索"next"节点使用跟搜索"previous"节点相反的逻辑。当从被选中的节点往后搜索到匹配的节点的时候就停止遍历。

 1 private   void  btnNext_Click( object  sender, System.EventArgs e)
 2 {
 3    this.processedSelectedNode = false;
 4    this.tvWalkerNext.ProcessTree();
 5}

 6
 7 private   void  tvWalkerNext_ProcessNode( object  sender, 
 8                                       ProcessNodeEventArgs e)
 9 {
10    if( e.Node == this.treeAdvanced.SelectedNode )
11        this.processedSelectedNode = true;                
12    
13    ifthis.processedSelectedNode               &&
14        e.Node != this.treeAdvanced.SelectedNode &&
15        this.ContainsSearchText( e.Node ) )
16    {
17        this.treeAdvanced.SelectedNode = e.Node;
18        e.StopProcessing = true;
19        return;
20    }

21    
22    ifthis.HasDummyNode( e.Node ) )
23        this.LoadDirectoryNodes( e.Node );
24}

找到最后一个匹配节点

搜索"last"节点包括搜索整个树,然后选折最后一个匹配的节点。这个操作是最花费时间的,因为在操作结束之前,所有的节点都必须被加载一遍。

 1 private   void  btnLast_Click( object  sender, System.EventArgs e)
 2 {
 3    this.Cursor = Cursors.WaitCursor;
 4
 5    this.matchingNode = null;
 6    this.tvWalkerLast.ProcessTree();
 7    ifthis.matchingNode != null )
 8    {
 9        // 'matchingNode'变量指向最后匹配的节点
10        this.treeAdvanced.SelectedNode = this.matchingNode;
11    }

12
13    this.Cursor = Cursors.Default;
14}

15
16 private   void  tvWalkerLast_ProcessNode( object  sender, ProcessNodeEventArgs e)
17 {
18    ifthis.ContainsSearchText( e.Node ) )
19        this.matchingNode = e.Node;
20    
21    ifthis.HasDummyNode( e.Node ) )
22        this.LoadDirectoryNodes( e.Node );
23}

虽然上述方法未必是在TreeView搜索的最有效方式,但无疑是最简单。使用TreeView漫游者简化了程序模型,并且可以很自然把不同的搜索程序分离到隔离的方法中。希望这个例子示范出了TreeView漫游者的灵活性和能力,能够给大家一些指点,以便在更复杂的情况下使用它。

关键的地方

如果你对TreeView漫游者是如何工作的感兴趣,这个方法就是他的核心:

 1 private   bool  WalkNodes( TreeNode node )
 2 {
 3    // 触发ProcessNode事件.
 4    ProcessNodeEventArgs args = ProcessNodeEventArgs.CreateInstance( node );
 5    this.OnProcessNode( args );
 6
 7    // 缓存ProcessSiblings的值,因为ProcessNodeEventArgs是单态的。
 8    bool processSiblings = args.ProcessSiblings;
 9
10    if( args.StopProcessing )
11    {
12        this.stopProcessing = true;
13    }

14    else if( args.ProcessDescendants )
15    {
16        foreach( TreeNode childNode in node.Nodes )
17            if! this.WalkNodes( childNode ) || 
18                  this.stopProcessing )
19                break;
20    }

21
22    return processSiblings;
23}

历史

略。


本文转自Justin博客园博客,原文链接:http://www.cnblogs.com/justinw/archive/2006/05/02/390634.html,如需转载请自行联系原作者

相关文章
|
运维 Kubernetes 负载均衡
Kubernetes介绍篇:是什么?为什么要用?
是时候该学习Kubernetes了,不然都不敢说自己了解容器、了解Docker。
1489 0
Kubernetes介绍篇:是什么?为什么要用?
|
10月前
|
安全 数据库 数据安全/隐私保护
处理用户输入数据格式验证不通过的情况时,如何给出友好的提示信息?
处理用户输入数据格式验证不通过的情况时,如何给出友好的提示信息?
677 78
|
10月前
|
存储 人工智能 运维
首批!阿里云飞天企业版率先通过中国信通院一云多算能力评估
阿里云飞天企业版率先参加中国信通院组织的首批一云多算系列标准的评估,并成功通过该标准的验收测试与专家评审。
271 2
首批!阿里云飞天企业版率先通过中国信通院一云多算能力评估
|
10月前
|
Kubernetes 容灾 Cloud Native
服务网格容灾系列场景(三):使用服务网格应对服务级故障容灾
文章介绍了使用服务网格应对服务级故障容灾的实践:服务网格ASM通过多集群、多地域部署和基于地理位置的故障转移机制,实现服务级故障的自动检测与秒级流量切换,能够确保业务在复杂故障场景下的高可用性。
|
10月前
|
监控 UED
跨部门协作中的任务协调:上级管理者的高效方法
在现代企业中,跨部门协作至关重要,但常因职能差异、信息不对称和沟通不畅导致任务分配不明确、资源浪费。上级管理者需充当战略目标传达者、任务协调者、信息共享推动者及冲突调解者,通过明确职责、建立协作机制、优化信息流程、引入高效工具等策略,避免重复劳动,提升组织效率。
669 15
|
存储 人工智能 数据管理
如何借助AI技术为NAS注入新活力
【8月更文挑战第11天】文件存储NAS是高性能、可共享访问的分布式文件系统,支持弹性扩展与高可靠性。通过融合AI技术,NAS能在数据存储路径上实现最优规划,提升存储效率;借助AI自学习能力优化数据管理流程;并实现精准的数据共享,最大化数据价值。
如何借助AI技术为NAS注入新活力
|
安全 数据安全/隐私保护 网络架构
CAPWAP 和 LWAPP 的区别
【8月更文挑战第24天】
484 0
|
存储 弹性计算 关系型数据库
企业购买阿里云服务器如何申请合同?
一般企业用户在购买阿里云服务器的同时需要申请云服务器购买合同,阿里云提供纸质合同和线上合同。大部分阿里云产品和订单支持线上合同自助化申请,具体的纸质合同和线上合同流程如下:
2480 1
企业购买阿里云服务器如何申请合同?
|
机器学习/深度学习 弹性计算 虚拟化
阿里云服务器ECS通用型g5和g6有什么区别?应该如何选择?
阿里云在官方活动中,对于通用型实例的云服务器ECS,主要推荐的是g5和g6这两个实例,那么阿里云服务器ECS通用型g6和通用型g5实例有什么区别?我们又该如何选择呢?本文来说说通用型g6和通用型g5的区别以及选择方法:
872 0
阿里云服务器ECS通用型g5和g6有什么区别?应该如何选择?
|
机器学习/深度学习 存储 数据采集
阿里云 ACP是什么?阿里云 ACP有什么用?
直到现在,还有很多从事互联网的工作人员都并不是清楚阿里云 ACP是什么,它是阿里云企业推出的针对于数据分析工程师的资格认证,有极高的含金量。因为阿里云在国内市场处于领先地位,他们推出的资格认证自然而然受到很多人的欢迎,很多互联网行业从业人员都以获得阿里ACP认证为荣。那么,阿里云 ACP是什么?阿里云 ACP有什么用?在认证大使官网上查阅了相关资料,我得到了答案。
1166 0
阿里云 ACP是什么?阿里云 ACP有什么用?