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方法即可。例如:

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

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

1private 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即可,参考下面的例子:

 1private 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即可,参考下面的例子:

 1private 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即可,就象:

 1private 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()方法里配置的。
3private TreeViewWalker tvWalkerFirst;
4private TreeViewWalker tvWalkerPrev;
5private TreeViewWalker tvWalkerNext;
6private TreeViewWalker tvWalkerLast;
7//用来辅助搜索的两个变量
8private TreeNode matchingNode;
9private bool processedSelectedNode;

找到第一个匹配节点

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

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

 5
 6private 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"节点。(相对于被选中的节点)

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

 6
 7private 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"节点相反的逻辑。当从被选中的节点往后搜索到匹配的节点的时候就停止遍历。

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

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

 1private 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
16private 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漫游者是如何工作的感兴趣,这个方法就是他的核心:

 1private 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}

历史

略。

posted @ 2006-05-02 16:08  Justin  阅读(3550)  评论(6编辑  收藏  举报