65.9K
CodeProject 正在变化。 阅读更多。
Home

泛型(非 WPF)树到 LINQ 和树上的事件传播

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (44投票s)

2015年8月9日

CPOL

19分钟阅读

viewsIcon

69076

downloadIcon

480

泛型树上的导航和事件传播

引言

WPF(Windows Presentation Foundation)引入了许多非常有趣的概念,从我的角度来看,这些概念比WPF本身更宏大,并且可以在非可视化编程甚至非.NET语言(例如Java或JavaScript)中使用。

这些新概念包括

  • 附加属性
  • 绑定
  • 递归树结构(逻辑树和可视树)
  • 模板(数据和控件模板可以重复用于创建和修改此类树结构)
  • 路由事件(在树结构中向上和向下传播的事件)

我坚信,WPF范式是编程理论上的质的飞跃,其重要性可与程序化编程数十年后的OOP突破相媲美

在本文中,我将讨论递归树结构和在此类树上传播的路由附加事件。

本文不需要WPF知识,并且也独立于我之前发表的两篇关于在WPF之外实现WPF概念的文章——WPF概念的纯C#实现 - 第1部分 AProps和绑定介绍 深入WPFless属性绑定 - 单向属性绑定(WPF概念在WPF之外 - 第2部分)

一点历史

WPF引入了逻辑树和可视树。逻辑树与XAML标签层次结构紧密匹配。可视树表示视觉对象的层次结构,例如,一个Grid面板可以包含多个按钮和文本对象。按钮又可以包含面板,面板中包含更多细粒度的对象,如图标、文本和工具提示。

WPF还有一个附加路由事件的概念。就像附加属性一样,此类事件可以在调用它的对象之外定义(非侵入性原则)。此外,此类事件的处理程序可以放置在可视化树的上方,处理程序的调用顺序由路由事件的类型决定(冒泡或隧道)。这种冒泡和隧道事件传播将在下面更详细地讨论。

Microsoft 的 LINQ to XML 库 System.Xml.Linq 提供了在 XML 文档上进行 LINQ 功能查询的功能(例如,参见 LINQ to XML 概述)。

一个LINQ to XML查询的例子(用英语表述)是“找到所有继承自‘Item’标签且‘Price’属性小于‘10’的XML节点”。

基于 LINQ to XML,Colin Eberhardt 在 LINQ to Tree - 查询树状结构的通用技术Linq to Visual Tree 中提出了一种通用的 Tree to XML 技术,该技术允许使用 LINQ 查询任何树结构,只要该结构实现了非常简单的 ILinqToTree<T> 接口。

基于这种通用方法,Colin Eberhardt 为 WPF 可视化树、Windows Forms 和文件夹/文件层次结构创建了 ILinqToTree<T> 适配器。他的 LINQ to WPF 可视化树功能几乎是所有 WPF 开发人员的必备工具。

Colin 方法的一个缺点是要求树节点必须实现(或应被适配以实现)ILinqToTree<T> 接口。

在本文中,我将介绍一种更通用的 LINQ to Tree 功能,它不需要任何接口实现。相反,父子关系由两个委托设置。其中一个指定如何从子节点获取父节点;另一个指定如何从父节点获取子节点。许多 LINQ 方法只需要这些委托中的一个,而有些则需要两个。

我还将展示如何创建在此类通用树上传播的REvent(类似于在WPF可视化树上传播的附加路由事件)。REvent的名称是根据AProps(非可视化附加属性)类比而来的,AProps在WPF概念的纯C#实现 - 第1部分 AProps和绑定介绍中定义和介绍。

通用树

树图是连接的无环图,其中每个节点最多只有一个父节点和0个或更多子节点

没有子节点的节点称为树的叶子。只有一个节点没有父节点,它被称为树的根。在树的图形图片中,根通常画在顶部,子节点从其父节点向下指向。

因此,我们将从子节点到父节点的导航称为向上导航,从父节点到子节点的导航称为向下导航

软件语言中的许多结构都可以表示为树——XML、WPF可视化树、WPF逻辑树、JSON、对象结构等。

有些树只具备向上导航能力(从子节点到父节点),另一些只具备向下导航能力(从父节点到子节点),还有许多树同时具备两种导航能力。

现在,让我们从 C# 功能的角度来看待树的导航。

向上导航意味着我们从一个树节点移动到它的父节点。它可以用一个委托表示,该委托接受树节点作为参数并生成一个父树节点:Func<Node, Node> toParent

向下导航意味着我们从一个树节点移动到它的子节点集合。相应的委托接受一个节点作为参数,并返回一个子节点集合:Func<Node, IEnumerable<Node>> toChildren

使用这两个委托,可以向上和向下导航任意树。

通用树到 LINQ 示例

演示树到LINQ功能的最佳方式是通过示例,所以我就从它们开始。

非可视化组织树示例

第一个示例位于 OrganizationTreeTest 项目下。它处理一个以树形结构呈现的小型组织

树的节点类型为OrgPersonOrgPerson包含NamePosition属性,用于描述人员的姓名及其在组织中的职位。属性Boss指向另一个OrgPerson,该OrgPerson是当前人员在组织中的上司。还有一个属性ManagedPeople,表示当前人员管理的一组OrgPerson对象。

OrgPerson 还包含一个构造函数,允许通过姓名和职位创建对象。它还有两个实用函数,简化了组织的组装

  1. void AddManaged(OrgPerson child) 将传入的 OrgPerson 对象添加到当前 OrgPerson 对象的 ManagedPeople 集合中,同时将传入对象的 Boss 属性设置为当前对象。
  2. OrgPerson AddAndCreateManaged(string name, Position position) - 创建一个具有传入姓名和职位的组织人员,并将其添加到当前对象的 ManagedPeople 集合中(同时将其 Boss 属性设置为当前对象)。它将创建的子对象返回给调用者。
public class OrgPerson
{
    // persons name
    public string Name { get; set; }

    // position within the organization
    public Position ThePosition { get; set; }

    // the boss
    public OrgPerson Boss { get; set; }

    // peole this OrgPerson is managing
    public List ManagedPeople { get; set; }

    // default constructor
    public OrgPerson()
    {
        Boss = null;
        ManagedPeople = new List();
    }

    // constractor by name and position
    public OrgPerson(string name, Position postion) : this()
    {
        Name = name;
        ThePosition = postion;
    }

    // adds an OrgPerson object to be among the 
    // ManagedPeople collection of the current node. 
    // The Boss property of the child object is set to the current node. 
    public void AddManaged(OrgPerson child)
    {
        child.Boss = this;
        ManagedPeople.Add(child);
    }

    // creates an OrgPerson object with the name and position passed as arguments
    // and adds it to the ManagedPeople collection of the current node. 
    // It returns the created child node to the caller
    public OrgPerson AddAndCreateManaged(string name, Position position)
    {
        OrgPerson createdChildObj = new OrgPerson(name, position);

        this.AddManaged(createdChildObj);

        return createdChildObj;
    }

    // returns a string containing the name and the position of the current node
    public override string ToString()
    {
        return Name + " - " + ThePosition.ToString();
    }
}

OrgPersonThePosition属性是Position枚举类型,非常简单

public enum Position
{
    Accountant,
    Developer,
    Manager,
    CEO
}  

使用示例位于 Program.Main 方法中。

首先,我们组建组织

#region ASSEMBLING THE ORGANIZATION
OrgPerson ceo = new OrgPerson("Tom", Position.CEO);

OrgPerson developmentManager = ceo.AddAndCreateManaged("John", Position.Manager);
OrgPerson accountingManager = ceo.AddAndCreateManaged("Greg", Position.Manager);

OrgPerson dev1 = developmentManager.AddAndCreateManaged("Nick", Position.Developer);
OrgPerson dev2 = developmentManager.AddAndCreateManaged("Rick", Position.Developer);

OrgPerson acct1 = accountingManager.AddAndCreateManaged("Jill", Position.Accountant);
OrgPerson acct2 = accountingManager.AddAndCreateManaged("Jane", Position.Accountant);
#endregion ASSEMBLING THE ORGANIZATION  

如你所见,Tom是公司的CEO。他有两个人向他汇报——管理开发人员的John(developmentManager)和管理会计师的Greg(accountingManager)。

约翰手下有两名开发人员:尼克和里克(dev1dev2),而格雷格手下有两名会计师:吉尔和简(acct1acct2)。

然后,我们定义向上和向下树导航委托

 
#region CREATE THE Up and Down TREE NAVIGATION Delegates
// Up Tree navigation delegate
Func toParent = (orgPerson) => orgPerson.Boss;

// Down Tree navigation delegate
Func> toChildren = (orgPerson) => orgPerson.ManagedPeople;
#endregion CREATE THE Up and Down TREE NAVIGATION Delegates

现在我们使用树到 LINQ 的功能。

首先,我们使用SelfAndDescendantsWithLevelInfo(...)扩展方法打印组织中的所有节点

#region TEST SelfAndDescendantsWithLevelInfo EXTENSION METHOD
IEnumerable<TreeNodeInfo<OrgPerson>> everyoneWithinTheOrg = 
    ceo.SelfAndDescendantsWithLevelInfo(toChildren);

Console.WriteLine("Print all members of the organization:");
foreach(TreeNodeInfo<OrgPerson> nodeInfo in everyoneWithinTheOrg)
{
    Console.WriteLine(nodeInfo.ToPrintString());
}
#endregion TEST SelfAndDescendantsWithLevelInfo EXTENSION METHOD  

SelfAndDescendantsWithLevelInfo(...)方法返回与当前节点及其所有后代对应的TreeNodeInfo<OrgPerson>类型对象的集合。

NP.Paradigms.TreeNodeInfo<OrgPerson>是一个非常简单的类,由树节点本身和整数Level组成。Level用于指定当前节点与原始节点(通常是树的根节点)的深度(距离)。

public class TreeNodeInfo<NodeType>
{
    /// <summary>
    /// A tree node object
    /// </summary>
    public NodeType TheNode;

    /// <summary>
    /// Integer specifying a distance between the original level 
    /// and the TreeNode object within the tree hierarchy.
    /// </summary>
    public int Level { get; set; }
}  

为了打印每个TreeNodeInfo<OrgPerson>,我使用了在OrgPerson.cs文件中定义的扩展函数OrgPersonExtensions.ToPrintString<OrgPerson>(this TreeNodeInfo<OrgPerson> treeNodeInfo)

// utility method that shifts the orgPerson string to the right
// by treeNodeInfo.Level number of tabs. 
public static string ToPrintString(this TreeNodeInfo treeNodeInfo)
{
    return (new string('\t', treeNodeInfo.Level)) + treeNodeInfo.TheNode.ToString();
}  

这个扩展方法会打印节点信息,并向右移动 treeNodeInfo.Level 个制表符,这样,一个人在层级中的位置越低,他就会向右移动得越多。

这是我们打印ceo.SelfAndDescendantsWithLevelInfo(toChildren)返回的结果集合时得到的内容

Print all members of the organization:
Tom - CEO
        John - Manager
                Nick - Developer
                Rick - Developer
        Greg - Manager
                Jill - Accountant
                Jane - Accountant  

接下来我们演示SelfAndAncestors(...)方法

#region TEST SelfAndAncestors EXTENSION METHOD
Console.WriteLine("\n");
Console.WriteLine("Print developer Nick and the hierarchy of his bosses:");
foreach (OrgPerson orgPersonInfo in dev1.SelfAndAncestors(toParent))
{
    Console.WriteLine(orgPersonInfo);
}
#endregion TEST SelfAndAncestors EXTENSION METHOD  

SelfAndAncestors(...) 将返回一个集合,该集合由调用节点及其祖先组成,按从节点到根节点的顺序排列。在我们的示例中,它将打印 Nick,然后是 Nick 的经理 John,然后是 John 的经理——Tom——CEO。

Print developer Nick and the hierarchy of his bosses:
Nick - Developer
John - Manager
Tom - CEO  

扩展方法Descendants(...)将返回传入节点的所有后代(但不包括节点本身)

#region TEST Descendants EXTENSION METHOD
Console.WriteLine("\n");
Console.WriteLine("Print all reports of Greg (the accountant Manager):");
foreach (OrgPerson orgPersonInfo in accountingManager.Descendants(toChildren))
{
    Console.WriteLine(orgPersonInfo);
}
#endregion TEST Descendants EXTENSION METHOD  

上面的代码将显示所有会计师(accountingManager节点的后代)

Print all reports of Greg (the accountant Manager):
Jill - Accountant
Jane - Accountant  

我们上面介绍的方法只要求一个委托(要么是向上树委托toParent,要么是向下树委托toChildren)。现在我将展示几个需要两者的方法。

AncestorsAndDescendantsFromTop(...) 扩展方法返回当前节点的所有祖先(从根节点开始)、当前节点本身及其所有后代的集合

#region TEST AncestorsAndDescendantsFromTop EXTENSION METHOD
Console.WriteLine("\n");
Console.WriteLine("Print Greg's (the accountant Manager's) seniors, himself and his reports:");
foreach (TreeNodeInfo<OrgPerson> orgPersonAndLevelInfo in accountingManager.AncestorsAndDescendantsFromTop(toParent, toChildren))
{
    Console.WriteLine(orgPersonAndLevelInfo.ToPrintString());
}
#endregion TEST AncestorsAndDescendantsFromTop EXTENSION METHOD  

正如所承诺的,上述功能将打印格雷格(会计经理)的上级、他自己和他的下属

Print Greg's (the accountant Manager's) seniors, himself and his reports:
Tom - CEO
        Greg - Manager
                Jill - Accountant
                Jane - Accountant

最后,方法AllButAncestorsAndDescendants将返回一个包含除当前节点、其祖先和后代之外的所有节点的集合

#region TEST AllButAncestorsAndDescendants EXTENSION METHOD
Console.WriteLine("\n");
Console.WriteLine("Print everyone except for Greg's (the accountant Manager's) seniors, himself and his reports:");
foreach (TreeNodeInfo<OrgPerson> orgPersonAndLevelInfo in accountingManager.AllButAncestorsAndDescendants(toParent, toChildren))
{
    Console.WriteLine(orgPersonAndLevelInfo.ToPrintString());
}
#endregion TEST AllButAncestorsAndDescendants EXTENSION METHOD  

将打印

Print everyone except for Greg's (the accountant Manager's) seniors, himself and his reports:
        John - Manager
                Nick - Developer
                Rick - Developer  

扩展方法定义在文档完备的 NP.Paradigms.TreeUtils 静态类中。

可视化和逻辑WPF树到LINQ

我们可以使用上一节演示的TreeUtils扩展方法,也可以在WPF可视化和逻辑树上执行LINQ操作。TESTS文件夹下的VisualTreeTests项目展示了这样的例子。

请注意,该项目不仅依赖于 NP.Paradigms 项目,还依赖于 NP.Paradigms.Windows 项目。后者项目依赖于 Microsoft 的可视化库:WindowBase.dll 和 PresentationCore.dll(而前者项目不需要它们)。NP.Paradigms.Windows 项目的目的是使 WPF 概念的纯 C# 实现适应在 WPF 和 XAML 中使用。

HP.Paradigms.Windows 项目中,我们感兴趣的有两个静态类——VisualTreeUtilsLogicalTreeUtils。通常,它们是 NP.Paradigms.TreeUtils 方法的包装器,只是我们使用不同的前缀(以便区分它们),并且向上和向下树的委托由相应地向上或向下导航可视化树或逻辑树的方法定义。

让我们看看 NP.Paradigms.Windows.VisualTreeUtils 静态类。它是这样定义向上和向下树委托的

static Func<FrameworkElement, FrameworkElement> toParent =
        (obj) => VisualTreeHelper.GetParent(obj) as FrameworkElement;

static Func<FrameworkElement, IEnumerable<FrameworkElement>> toChildren =
    (parentObj) =>
    {
        int childCount = VisualTreeHelper.GetChildrenCount(parentObj);

        List<FrameworkElement> result = new List<FrameworkElement>();
        for (int i = 0; i < childCount; i++)
        {
            result.Add(VisualTreeHelper.GetChild(parentObj, i) as FrameworkElement);
        }

        return result;
    };  

我们使用VisualTreeHelper来实现委托。

现在,VisualTreeUtils 类中的每个扩展方法都是 NP.Paradigms.TreeUtils 类中相应方法的包装器,其中适当的委托作为参数之一传入。方法名称与 TreeUtils 相同,只是前缀为 'Visual'。例如,以下是 VisualAncestors 方法的实现

public static IEnumerable<FrameworkElement> VisualAncestors<T>(this FrameworkElement element)
{
    return element.Ancestors(toParent);
}

VisualTreeUtils.VisualAncestors 方法包装了 TreeUtils.Ancestors 方法,并将 toParent 委托作为参数传递给它。

另一个例子是VisualDescendantsWithLevelInfo(...)方法

public static IEnumerable<TreeNodeInfo<FrameworkElement>> VisualDescendantsWithLevelInfo
(
    this FrameworkElement node
)
{
    return node.DescendantsWithLevelInfo(toChildren);
}

现在,我们来看看 NP.Paradigms.Windows.LogicalTreeUtils 类。以下是我们在此处定义向上和向下树委托的方式

static Func<object, object> toParent =
        (obj) =>
        {
            if ( !(obj is DependencyObject) )
            {
                return null;
            }

            return LogicalTreeHelper.GetParent(obj as DependencyObject);
        };

static Func<object, IEnumerable<object>> toChildren =
    (parentObj) =>
    {
        if (!(parentObj is DependencyObject))
        {
            return null;
        }

        return LogicalTreeHelper.GetChildren(parentObj as DependencyObject).Cast<object>();
    };  

包装方法的名称前缀是Logical而不是Visual,例如

public static IEnumerable<object> LogicalAncestors(this object element)
{
    return element.Ancestors(toParent);
}

此外,那些使用WPF的人知道逻辑节点不一定是FrameworkElement类型,它们可以是简单类型,例如string。因此,我们被迫假设节点是对象,而不是像可视树那样是FrameworkElement

现在,让我们运行 VisualTreeTests 项目——这是我们得到的结果

我们可以看到包含文本“Element To Display”的网格面板以及按钮层次结构的可视化和逻辑树节点。

要在WPF窗口中显示单个树节点,我们使用类VisualTreeTests.TreeNodeDisplayer

它有一个属性string StringToDisplay,其中包含要显示的字符串。它还有两个构造函数,允许为可视树节点和逻辑树节点创建TreeNodeDisplayer。每个构造函数都设置对象的StringToDisplay属性,以显示有关节点的信息(其类型和名称)。

MainWindow类包含两个依赖属性VisualTreeCollectionLogicalTreeCollection——两者都是与视觉和逻辑层次结构对应的TreeNodeDisplayer对象集合。这些集合在MainWindow_Loaded事件处理程序中设置

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    // get all collection of ElementToDisplay and its descendants within the visual tree
    IEnumerable<TreeNodeInfo<FrameworkElement>> visualTreeNodes = ElementToDisplay.VisualSelfAndDescendantsWithLevelInfo();

    // set the VisualTreeCollection to contain TreeNodeDisplayer objects
    // obtained from visualTreeNodes
    VisualTreeCollection = visualTreeNodes.Select((TreeNodeInfo) => new TreeNodeDisplayer(TreeNodeInfo)).ToList();

    // get all collection of ElementToDisplay and its descendants within the logical tree
    IEnumerable<TreeNodeInfo<object>> logicalTreeNodes = ElementToDisplay.LogicalSelfAndDescendantsWithLevelInfo();

    // set the LogicalTreeCollection to contain TreeNodeDisplayer objects
    // obtained from logicalTreeNodes
    LogicalTreeCollection = logicalTreeNodes.Select((TreeNodeInfo) => new TreeNodeDisplayer(TreeNodeInfo)).ToList();
}

MainWindow.xaml文件包含我们要显示的元素——一个带有文本和按钮的网格

<Grid x:Name="ElementToDisplay"
      Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Text="Element To Display"
               FontSize="20"
               Margin="0,5"
               HorizontalAlignment="Center" />

    <Button x:Name="MyButton"
            Grid.Row="1"
            Width="100"
            Height="25"
            Content="TheButton" />
</Grid> 

它还包含两个ItemsControl元素——一个用于显示可视化树信息,一个用于显示逻辑树信息

<Grid x:Name="VisualTreePanel"
      Grid.Row="2"
      Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Text="Visual Tree"
               FontSize="20"
               Margin="0,5" 
               HorizontalAlignment="Center"/>

    <ItemsControl Grid.Row="1" 
                  ItemsSource="{Binding VisualTreeCollection, RelativeSource={RelativeSource AncestorType=Window}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=StringToDisplay}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

<Grid x:Name="LogicalTreePanel"
      Grid.Row="4"
      Background="White">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <TextBlock Text="Logical Tree"
               FontSize="20"
               Margin="0,5"
               HorizontalAlignment="Center" />

    <ItemsControl Grid.Row="1"
                  ItemsSource="{Binding LogicalTreeCollection, RelativeSource={RelativeSource AncestorType=Window}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Path=StringToDisplay}" />
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>  

这些ItemsControl对象的ItemsSource属性绑定到MainWindowVisualTreeCollectionLogicalTreeCollection依赖属性。

树上的REvents

WPF可视化树上的路由事件

WPF 中有一个附加路由事件的概念。与普通的 C# 事件不同,它们

  1. 可以在触发它们的类之外定义并“附加”到对象上。
  2. 可以在WPF可视树中向上和向下传播——这意味着事件可以由一个树节点触发,并在另一个树节点(触发节点的祖先之一)上处理。

路由事件有3种不同的传播模式

  1. 直接 - 这意味着事件只能在触发它的同一个可视化树节点上处理。
  2. 冒泡 - 事件从当前节点(触发事件的节点)向可视化树的根部传播,并可以在途中任何位置处理。
  3. 隧道 - 事件从可视化树的根节点传播到当前节点(触发事件的节点)。

以下图片描绘了冒泡和隧道事件传播

请注意,冒泡和隧道事件都在节点及其祖先上操作(只是方向相反)。没有路由事件传播模式会从一个节点传播到其后代。

如果冒泡或隧道路由事件在某个节点上被处理,它仍然会继续传播到最后,并且可以在第一个处理点之后设置路由事件处理程序。

任意树上的REvents

我在这里描述了一种与路由事件非常相似但更强大、更通用的范式的实现。我称之为 REvents(就像在本系列的第一篇文章中我将附加属性的通用非 WPF 实现称为 AProps 一样——请参阅 WPF 概念的纯 C# 实现 - 第 1 部分 AProps 和绑定简介)。

REvents 比路由事件更强大,因为

  1. 它们可以操作任意树,而不仅仅是 WPF 可视化树。它们可以操作的树应由上面描述的向上和向下树委托定义。
  2. 除了冒泡和隧道模式,还有一种特殊的模式,用于从触发事件的节点向下传播到节点的后代。
  3. 路由事件的传播模式只能在路由事件创建时确定,而REvent的模式可以在其调用期间设置,因此同一个REvent可以与不同的传播模式一起使用。

我们将这种额外的REvent传播模式称为——PropagateToDescendants模式。它允许REvent处理发生在节点的后代上(冒泡和隧道传播都发生在节点的祖先上,只是顺序不同)。

重要提示:上图和下面的示例显示了事件从树的根节点传播到其后代。实际上,REvent功能支持从任何树节点传播到其后代(触发REvent的节点不必是根节点)。

REvents 的使用示例位于 TESTS/REventsTest 解决方案下。

在这个项目中,我们展示了与第一个示例相同的树上的事件传播示例——一个表示小型组织的树

所有演示REvent用法的代码都位于Program.Main()方法中。

首先,我们像上面一样组装组织

#region ASSEMBLING THE ORGANIZATION
OrgPerson ceo = new OrgPerson("Tom", Position.CEO);

OrgPerson developmentManager = ceo.AddAndCreateManaged("John", Position.Manager);
OrgPerson accountingManager = ceo.AddAndCreateManaged("Greg", Position.Manager);

OrgPerson dev1 = developmentManager.AddAndCreateManaged("Nick", Position.Developer);
OrgPerson dev2 = developmentManager.AddAndCreateManaged("Rick", Position.Developer);

OrgPerson acct1 = accountingManager.AddAndCreateManaged("Jill", Position.Accountant);
OrgPerson acct2 = accountingManager.AddAndCreateManaged("Jane", Position.Accountant);
#endregion ASSEMBLING THE ORGANIZATION

然后我们创建向上和向下树导航委托

#region CREATE THE Up and Down TREE NAVIGATION delegates
// Up Tree navigation delegate
Func<OrgPerson, OrgPerson> toParent = (orgPerson) => orgPerson.Boss;

// Down Tree navigation delegate
Func<OrgPerson, IEnumerable<OrgPerson>> toChildren = (orgPerson) => orgPerson.ManagedPeople;
#endregion CREATE THE Up and Down TREE NAVIGATION delegates  

接下来,我们创建一个REvent approveExpenseEvent,它接受string expenseNamedouble dollarAmount作为参数

 REvent<OrgPerson, string, double> approveExpenseEvent = new REvent<OrgPerson, string, double>();  

现在,我们为这个事件创建一个处理程序。处理程序将批准任何低于10000美元的费用(打印相应的消息,详细说明费用名称和金额)。高于10000美元的费用将被拒绝,并且消息传播将停止。

Action<REventInfo<OrgPerson>, string, double> approveExpenseHandler =
    (rEventInfo, expenseName, dollarAmount) =>
    {
        OrgPerson senderNode = rEventInfo.SenderNode;
        OrgPerson handlingNode = rEventInfo.HandlerNode;

        if (dollarAmount > 10000)
        {
            Console.WriteLine(handlingNode.ToString() + " rejected " + expenseName + " expense for $" + dollarAmount + " for " + senderNode.ToString());                        
            rEventInfo.IsCanceled = true; // stop message propagation
        }
        else
        {
            Console.WriteLine(handlingNode.ToString() + " approved " + expenseName + " expense for $" + dollarAmount + " for " + senderNode.ToString());            
        }
    };  

请注意,传递给REvent处理程序的第一个参数始终是REventInfo<NodeType>类型,它包含

  1. SenderNode - 指定触发事件的节点
  2. HandlerNode - 指定处理事件的节点
  3. IsCanceled - 允许取消事件传播的标志
public class REventInfo<NodeType>
{
    public REventInfo()
    {
        IsCanceled = false;
    }

    public REventInfo(NodeType senderNode, NodeType handlerNode) : this()
    {
        SenderNode = senderNode;
        HandlerNode = handlerNode;
    }

    public REventInfo(NodeType node) : this(node, node)
    {
    }

    public NodeType SenderNode { get; internal set; }

    public NodeType HandlerNode { get; internal set; }

    public bool IsCanceled { get; set; }
}  

接下来,我们将此事件处理程序添加到ceodevelopmentManager节点

approveExpenseEvent.AddHander(ceo, approveExpenseHandler);
approveExpenseEvent.AddHander(developmentManager, approveExpenseHandler);  

现在我们在dev1节点上触发冒泡事件,expenseName为“差旅费”,费用金额为1000美元。这个金额低于10000美元的阈值,因此事件应该从dev1冒泡到developmentManager,再到ceo,开发经理和CEO都将批准该费用。

approveExpenseEvent.RaiseBubbleEvent(dev1, toParent, "Travel Expense", 1000);  

这将导致打印以下文本

John - Manager approved Travel Expense expense for $1000 for Nick - Developer
Tom - CEO approved Travel Expense expense for $1000 for Nick - Developer

请注意,由于事件是冒泡的,它将首先由 developmentManager 处理,然后由 ceo 处理。

接下来,我们将演示事件取消,假设dev2触发相同的事件,金额为20000美元(高于10000美元的阈值)。在这种情况下,它将首先发送给developmentManager John,他将拒绝,并且永远不会到达CEO。

approveExpenseEvent.RaiseBubbleEvent(dev2, toParent, "Travel Expense", 20000);  

将导致以下打印输出

John - Manager rejected Travel Expense expense for $20000 for Rick - Developer

现在让我们演示事件隧道。假设公司有一个非标准程序,任何费用首先发送给CEO审批,CEO(批准后)将其向下发送给相应的较低级别经理。如果CEO(或经理)拒绝,事件将不再向下传播。

approveExpenseEvent.RaiseTunnelEvent(dev1, toParent, "Travel Expense", 1000);  

这将导致以下打印输出

Tom - CEO approved Travel Expense expense for $1000 for Nick - Developer
John - Manager approved Travel Expense expense for $1000 for Nick - Developer  

请注意,我们从组织树的根节点——CEO Tom 开始,然后向下移动到触发隧道事件的节点。

现在我们演示带取消功能的隧道

approveExpenseEvent.RaiseTunnelEvent(dev2, toParent, "Travel Expense", 20000);  

将导致

Tom - CEO rejected Travel Expense expense for $20000 for Rick - Developer  

最后,我们将演示事件从树节点传播到其所有后代的功能——这是WPF路由事件无法做到的。

我们创建一个新的REvent readMemoEvent,它接受一个字符串参数,对应于备忘录名称

 REvent readMemoEvent = new REvent();  

这对应于一份备忘录,它从CEO发送给组织中的每个成员。

现在,我们指定备忘录事件处理程序

Action<REventInfo<OrgPerson>, string> readMemoHandler = (rEventInfo, memoTitle) =>
{
    OrgPerson handlingNode = rEventInfo.HandlerNode;
    Console.WriteLine(handlingNode.ToString() + " read memo '" + memoTitle + "'");
};  

它打印出组织内相应人员阅读了备忘录。

我们将此处理程序设置在树中的每个节点上

foreach (OrgPerson orgPerson in ceo.SelfAndDescendants(toChildren))
{
    readMemoEvent.AddHander(orgPerson, readMemoHandler);
}  

现在我们调用ceo节点上的RaiseEventPropagateToDescendants(...)方法,以在该节点本身上执行事件,并将其传播到该节点的所有后代

readMemoEvent.RaiseEventPropagateToDescendents(ceo, toChildren, "A Very Important Memo");  

这将导致以下打印输出

CEO Tom read memo 'A Very Important Memo'
Manager John read memo 'A Very Important Memo'
Developer Nick read memo 'A Very Important Memo'
Developer Rick read memo 'A Very Important Memo'
Manager Greg read memo 'A Very Important Memo'
Accountant Jill read memo 'A Very Important Memo'
Accountant Jane read memo 'A Very Important Memo'  

任何节点都可以取消“向后代”传播,就像冒泡和隧道一样(尽管我们没有在演示中展示)。

REvent 实现说明

NP.Paradigms.REvent 类定义在 NP.Paradigms 项目下的 REvent.cs 文件中。

public class REvent<NodeType, T1, T2, T3, T4> where NodeType : class
{
    AProp<NodeType, REventWrapper<NodeType, T1, T2, T3, T4>> _aProperty = new AProp<NodeType, REventWrapper<NodeType, T1, T2, T3, T4>>();

    ...
}  

AProp _aProperty 存储树节点与 EventWrapper 对象之间的映射。EventWrapper 是在同一文件中定义的私有类。

REvent 类中定义了几个版本的 AddHandler(...) 方法——用于添加 5、4、3、2 和 1 个参数的处理程序。处理程序的第一个参数始终是上面描述的 REventInfo<NodeType> 类型。接下来的 4、3、2 或 1 个参数可以是任何泛型类型。下面是一个 5 参数 AddHandler 方法的示例

public void AddHander(NodeType obj, Action<REventInfo<NodeType>, T1, T2, T3, T4> handler)
{
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = GetOrCreateEventWrapper(obj);
    eventWrapper.TheEvent += handler;
}  

要获取节点的事件包装器,我们使用GetOrCreateEventWrapper(...)方法

REventWrapper<NodeType, T1, T2, T3, T4> GetOrCreateEventWrapper(NodeType node)
{
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(node);

    if (eventWrapper == null)
    {
        eventWrapper = new REventWrapper<NodeType, T1, T2, T3, T4>();
        _aProperty.SetProperty(node, eventWrapper);
    }

    return eventWrapper;
}  

EventWrapper 是一个私有类,允许添加和删除事件处理程序。它有一个类型为 List<Action<REventInfo<NodeType>, T1, T2, T3, T4>>_event 字段,用于积累和调用事件处理程序。它还有一个事件 TheEvent,用于向 _event 字段添加和从中移除处理程序。

List<Action<REventInfo<NodeType>, T1, T2, T3, T4>> _event = null;

internal event Action<REventInfo<NodeType>, T1, T2, T3, T4> TheEvent
{
    add
    {
        if (_event == null)
            _event = new List<Action<REventInfo<NodeType>, T1, T2, T3, T4>>();

        _event.Add(value);
    }

    remove
    {
        _event.Remove(value);

        if (_event.Count == 0)
            _event = null;
    }
}  

我们基本重新实现了MS事件的原因是,如果有多个处理程序,其中一个取消了处理,我们希望跳过执行其余的处理程序。

internal bool RaiseEvent
(
    REventInfo<NodeType> eventInfo, 
    T1 t1 = default(T1), 
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4))
{
    if (_event != null)
        foreach (Action<REventInfo<NodeType>, T1, T2, T3, T4> action in _event)
        {
            action(eventInfo, t1, t2, t3, t4);

            // if cancelled, stop immediately
            if (eventInfo.IsCanceled)
            {
                return false;
            }
        }

    return true;
}  

请注意,REventWrapper<NodeType, T1, T2, T3, T4> 类的各种 AddHandler(...) 方法允许添加具有不同参数数量(5、4、3、2 和 1)的处理程序。就像 REvent 的情况一样,第一个参数始终是 REventInfo<NodeType> 类型,而其余参数的类型可以任意选择。

这里有几个例子

internal void AddHandler(Action<REventInfo<NodeType>, T1, T2, T3, T4> action)
{
    Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToAdd = (eventInfo, t1, t2, t3, t4) =>
    {
        action(eventInfo, t1, t2, t3, t4);
    };

    AddFunctionFromDelegate(action, actionToAdd);
}  

添加一个5参数处理程序,同时

internal void AddHandler(Action<REventInfo<NodeType>, T1> action)
{
    Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToAdd = (eventInfo, t1, t2, t3, t4) =>
    {
        action(eventInfo, t1);
    };

    AddFunctionFromDelegate(action, actionToAdd);
}  

添加一个两个参数的处理程序。

由于TheEvent始终是Action<REventInfo<NodeType>, T1, T2, T3, T4>类型,我们使用委托表达式将参数较少的处理程序转换为TheEvent的类型。例如,用于两个参数委托的代码是

// convert the two argument handler to the 5 argument delegate:
Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToAdd = (eventInfo, t1, t2, t3, t4) =>
{
    action(eventInfo, t1);
};

// add the 5 argument delegate to TheEvent
AddFunctionFromDelegate(action, actionToAdd);

AddFunctionFromDelegate 方法将 5 参数委托添加到 TheEvent,但也在 _delegateToFuncMap 字典中存储了原始操作和 5 参数委托之间的映射

void AddFunctionFromDelegate(object action, Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToAdd)
{
    TheEvent += actionToAdd;

    // create the dictionary if has not been created before
    if (_delegateToFuncMap == null)
    {
        _delegateToFuncMap = new Dictionary<object, Action<REventInfo<NodeType>, T1, T2, T3, T4>>();
    }

     // add the action to dictionary
    _delegateToFuncMap[action] = actionToAdd;
}  

这样做是为了在调用RemoveActionHandler方法时,能够通过操作移除5参数委托。

internal void RemoveActionHandler(object action)
{
    Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToRemove = action as Action<REventInfo<NodeType>, T1, T2, T3, T4>;

    if (actionToRemove == null)
    {
        if (!_delegateToFuncMap.TryGetValue(action, out actionToRemove))
        {
            return;
        }

        // remove the corresponding 5 arg delegate
        _delegateToFuncMap.Remove(action);

        if (_delegateToFuncMap.Count == 0)
            _delegateToFuncMap = null;
    } 

    TheEvent -= actionToRemove;
}  

还有一个方法RemoveAllHandlers,它会清除REventWrapper<NodeType>对象中的所有事件处理程序。

internal void RemoveAllHandlers()
{
    if (_event != null)
    {
        _event.Clear();
        _event = null;
    }

    if (_delegateToFuncMap != null)
        _delegateToFuncMap.Clear();
}

现在,让我们回到REvent<NodeType, T1, T2, T3, T4>类。正如我们上面提到的,它包含一个AProp _aProperty,该属性允许为在树上积累节点事件处理程序创建REventWrapper对象。这样的对象通过GetOrCreateEventWrapper(...)私有辅助方法创建或查找。

REventWrapper<NodeType, T1, T2, T3, T4> GetOrCreateEventWrapper(NodeType node)
{
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(node);

    if (eventWrapper == null)
    {
        // if event wrapper does not exist, create one and add 
        // it as the attached property for the node. 
        eventWrapper = new REventWrapper<NodeType, T1, T2, T3, T4>();
        _aProperty.SetProperty(node, eventWrapper);
    }

    return eventWrapper;
}  

如前所述,我们有多个 AddHandler 方法,允许添加具有不同参数数量的处理程序。例如,这是用于添加 4 个参数处理程序的 AddHandler 方法

public void AddHander(NodeType obj, Action<REventInfo<NodeType>, T1, T2, T3> handler)
{
    // get or create the event wrapper for the node
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = GetOrCreateEventWrapper(obj);

    // add the handler to the event wrapper object
    eventWrapper.AddHandler(handler);
}  

要从节点中移除处理程序,我们可以使用RemoveHandler(...)方法

public void RemoveHandler(NodeType node, object handler)
{
    // get the event wrapper for a node
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(node);

    if (eventWrapper == null)
        return;

    // remove the handler from the event wrapper
    eventWrapper.RemoveActionHandler(handler);

    // if there are no more handlers within the event wrapper object,
    // remove the event wrapper object itself
    if (!eventWrapper.HasEvent)
    {
        _aProperty.ClearAProperty(node);
    }
}  

还有一个方法RemoveAllHandlers(NodeType node),它从树节点中移除所有REvent处理程序。

  
public void RemoveAllHandlers(NodeType node)
{
    REventWrapper eventWrapper = _aProperty.GetProperty(node);

    if (eventWrapper == null)
        return;

    // remove all handlers from the event wrapper
    eventWrapper.RemoveAllHandlers();

    // remove the event wrapper itself
    _aProperty.ClearAProperty(node);
}

我们有几种触发REvent的方法。

RaiseEvent 方法导致事件(可能)在触发它的同一个节点上处理,对应于路由事件的直接模式。

// raise event to be handled only on the same node
// This would correspond to direct Routed Event mode
public void RaiseEvent
(
    NodeType node, 
    T1 t1 = default(T1), 
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4)
)
{
    REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(node);

    // the node does not have handlers, so
    // we do not need to do anything
    if (eventWrapper == null)
        return;

    // 
    REventInfo<NodeType> eventInfo = new REventInfo<NodeType>(node);

    eventWrapper.RaiseEvent(eventInfo, t1, t2, t3, t4);
}  

私有方法 RaiseEventIterateThroughAncestors 用于冒泡和隧道 REvents。

// bubbling or tunneling implementation
private bool RaiseEventIterateThroughAncestors
(
    NodeType node, 
    Func<NodeType, NodeType> toParentFunction, 
    bool shouldBubbleOrTunnel, // true for bubbling and false for tunneling
    T1 t1 = default(T1), 
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4)
)
{
    // get a collection containing the current node and all its ancestors. 
    IEnumerable<NodeType> ancestorIterator = node.SelfAndAncestors(toParentFunction);

    // if tunneling, reverse the order of the ancestors starting with the
    // root node and ending with the current node
    if (!shouldBubbleOrTunnel) // if we tunnel - reverse
    {
        ancestorIterator = ancestorIterator.Reverse();
    }

    REventInfo<NodeType> eventInfo = new REventInfo<NodeType>(node, null);

    // raise the event on each of the nodes within ancestorIterator collection
    foreach (NodeType currentNode in ancestorIterator)
    {
        REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(currentNode);

        if (eventWrapper == null)
            continue;

        eventInfo.HandlerNode = currentNode;

        // if the evewnt has been cancelled, stop the iteration
        if (!eventWrapper.RaiseEvent(eventInfo, t1, t2, t3, t4)) // false return value means 'stop propagation'
            return false;
    }

    return true;
}  

有两个方法RaiseBubbleEvent(...)RaiseTunnelEvent(...)分别用于触发冒泡和隧道REvent,它们都使用上面描述的实用方法RaiseEventIterateThroughAncestors(...)

public bool RaiseBubbleEvent
(
    NodeType node, 
    Func<NodeType, NodeType> toParentFunction, 
    T1 t1 = default(T1), 
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4)
)
{
    return RaiseEventIterateThroughAncestors(node, toParentFunction, true /*true for bubbling*/, t1, t2, t3, t4);
}

public bool RaiseTunnelEvent
(
    NodeType node, 
    Func<NodeType, NodeType> toParentFunction, 
    T1 t1 = default(T1), 
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4)
)
{
    return RaiseEventIterateThroughAncestors(node, toParentFunction, false /*false for tunneling*/, t1, t2, t3, t4);
}  

最后,有一个方法RaiseEventPropagateToDescendants(...),它将事件传播到后代——向下遍历树。

public bool RaiseEventPropagateToDescendents
(
    NodeType node,
    Func<NodeType, IEnumerable<NodeType>> toChildrenFunction,
    T1 t1 = default(T1),
    T2 t2 = default(T2), 
    T3 t3 = default(T3), 
    T4 t4 = default(T4)
)
{
    REventInfo<NodeType> eventInfo = new REventInfo<NodeType>(node, null);

    foreach (NodeType currentNode in node.SelfAndDescendants(toChildrenFunction))
    {
        REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(currentNode);

        if (eventWrapper == null)
            continue;

        eventInfo.HandlerNode = currentNode;

        if (!eventWrapper.RaiseEvent(eventInfo, t1, t2, t3, t4))
            return false;
    }

    return true;
}  

摘要

本文继续讨论我在WPF概念的纯C#实现 - 第1部分 AProps和绑定介绍深入WPFless属性绑定 - 单向属性绑定(WPF概念在WPF之外 - 第2部分)中开始的在WPF之外实现WPF概念的主题。

在这里我讨论了如何使用toParenttoChildren委托实现通用树结构,以及如何实现REvent概念——在通用树上传播的事件,它们与WPF的路由事件相似,但更强大、更通用。

致谢

我感谢CodeProject内外最优秀、最聪明、最多产的人,他们激励我寻找新的方法来开发软件并与他人分享,包括但不限于Sacha Barber、Colin Eberhardt、Paulo Zemek、Florian Rappl、Dr. ABell、Alex Volynsky。

我还要感谢我亲爱的7岁女儿,她要求我加上这句感谢,因为她希望像她姐姐几年前一样被承认,请参阅Silverlight/MEF的Prism简易示例。第3部分 - 模块间的通信文章末尾。

© . All rights reserved.