Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Generic (Non-WPF) Tree to LINQ and Event Propagation on Trees

0.00/5 (No votes)
26 Aug 2015 1  
Navigation and event propagation on generic trees

Introduction

WPF (Windows Presentation Foundation) introduced a number of very interesting concepts which, from my point of view, are bigger than WPF itself and can be used in non-visual programming and even non .NET languages, e.g. Java or JavaScript.

These new concepts include

  • Attached Properties
  • Bindings
  • Recursive Tree Structures (Logical and Visual Trees)
  • Templates (Data and Control Templates can be re-used for creating and modifying such tree structures)
  • Routed Events (events that propagate up and down the tree structures)

I strongly believe, that WPF paradigms are a qualitative leap in programming theory comparable in magnitude to the OOP breakthroughs after decades of procedural programming.

In this article I discuss the recursive tree structures and Routed Attached Events propagated on such trees.

This article does not require WPF knowledge and is also independent of the two previous articles which I published on the subject of WPF concepts implemented outside of WPF - Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2)

A bit of History

WPF introduced Logical and Visual trees. Logical trees closely match the XAML tag hierarchy. Visual trees represent the hierarchy of visual objects e.g. A Grid panel can contain a number of buttons and text objects. The buttons in turn can contain panels containing some more fine grain objects like icons and texts and tool tips.

WPF also has a concept of an Attached Routed Event. Just like an attached property such event can be defined outside of the object that invokes it (non-invasiveness principle). Also the handlers for such events can be placed up the visual tree with the order of handlers invoked being determined by the type of the routed event (bubbling or tunneling). This bubbling and tunneling event propagation will be discussed in greater detail below.

Microsoft's LINQ to XML library System.Xml.Linq provides functionality for LINQ functional queries on an XML document (see e.g. LINQ to XML Overview).

An example of LINQ to XML query (stated in English) is "find me all XML nodes descendent from a tag 'Item' that have an attribute 'Price' smaller than '10'".

Based on LINQ to XML, Colin Eberhardt in LINQ to Tree - A Generic Technique for Querying Tree-like Structures and in Linq to Visual Tree came up with a generic Tree to XML technique allowing to query any tree structure with LINQ as long as such a structure implements a very simple ILinqToTree<T> interface.

Based on this generic approach, Colin Eberhardt created ILinqToTree<T> adapter for WPF visual trees, Windows Forms and Folder/File hierarchies. His LINQ to WPF Visual Trees functionality is virtually a must use for any WPF developer.

One downside to Colin's approach is the requirement that the tree nodes should implement (or should be adapted to implement) the ILinqToTree<T> interface.

In this article, I introduce a more generic LINQ to Tree functionality which does not require any interface implementation. Instead, the parent-child relationship is set by two delegates. One of them specifies how to obtain a parent from a child; the other specifies how obtain children from a parent. Many of the LINQ methods require only one of these delegates while some require both.

I also show how to create REvents that propagate on such generic trees (similar to Attached Routed Events propagating on WPF Visual Trees). The REvent name came by analogy with AProps (non-visual attached properties) defined and presented in Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings.

Generic Trees

Tree graph is a connected graph without cycles where each node has no more than one parent and 0 or more children:

The nodes without children are called the leaves of the tree. There is only one node that does not have a parent and it is called the root of the tree. On the graph pictures of the trees, the roots are usually drawn at the top and the children point down from their parents.

Because of the above, we will call navigation from a child to a parent - up navigation and navigation from parent to children - down navigation.

Many structures in software languages can be represented as trees - XML, WPF Visual Trees, WPF Logical Trees, JSON, Object Structure etc.

Some trees have only up navigational ability (from child to parent), others - down - from parent to children and many trees have both navigational capabilities.

Now, let us look at the tree navigation in terms of C# functionality.

Up navigation means we move from a tree node to its parent. It can be represented by a delegate taking tree node as an argument and producing a parent tree node: Func<Node, Node> toParent.

Down navigation means we move from a tree node to a collection of its children. The corresponding delegate takes a node as an argument and returns a collection of children: Func<Node, IEnumerable<Node>> toChildren.

Using these two delegates one can navigate up and down arbitrary trees.

Generic Tree to LINQ Samples

The best way to demonstrate the tree to LINQ functionality is through samples, so I am going to start with them.

Non-Visual Organization Tree Sample

The first sample is located under OrganizationTreeTest project. It deals with a very small organization presented as a tree:

The nodes of the tree are of the type OrgPerson. OrgPerson contains the Name and Position properties characterizing the name of the person and his/her position within the organization. Property Boss refers to another OrgPerson who is the boss of the current person within the organization. There is a property ManagedPeople representing a collection of OrgPersons that the current person is managing.

OrgPerson also contains a constructor that allows creating an object by name and position. It also has two utility functions simplifying assembling the organization:

  1. void AddManaged(OrgPerson child) adds the passed OrgPerson object to the ManagedPeople collection of the current OrgPerson object, while at the same time setting the Boss property on the passed object to the current object.
  2. OrgPerson AddAndCreateManaged(string name, Position position) - creates an org person with the passed name and position, and adds it to the ManagedPeople collection of the current object (while at the same time setting its Boss property to the current object). It returns the created child object to the caller.
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<orgperson> ManagedPeople { get; set; }

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

    // 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();
    }
}
</orgperson>

ThePosition property of OrgPerson is of very simple enum type Position:

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

The usage example is located within Program.Main method.

First, we assemble the organization:

#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  

As you can see, Tom is the CEO of the company. He has two people reporting to him - John, who manages the developers (developmentManager) and Greg, who manages the accountants (accountingManager).

John has two developers reporting to him - Nick and Rick (dev1 and dev2) and Greg has two accountants reporting to him Jill and Jane (acct1 and acct2).

Then, we define the up and down tree navigation delegates:

#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, orgperson="">> toChildren = (orgPerson) => orgPerson.ManagedPeople;
#endregion CREATE THE Up and Down TREE NAVIGATION Delegates
</orgperson,>

Now we employ the Tree to LINQ functionality.

First, we print all the nodes within the organization by using SelfAndDescendantsWithLevelInfo(...) extension method:

#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(...) method returns a collection of objects of TreeNodeInfo<OrgPerson> type corresponding to the current node and all its descendants.

NP.Paradigms.TreeNodeInfo<OrgPerson> is a very simple class consisting of the tree node itself and integer Level. Level is used to specify the depth (distance) of the current node from the original node (most often from the root node of the tree):

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; }
}  

In order to print each TreeNodeInfo<OrgPerson> I employ an extension function OrgPersonExtensions.ToPrintString<OrgPerson>(this TreeNodeInfo<OrgPerson> treeNodeInfo) defined within OrgPerson.cs file:

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

This extension method prints the node information shifted to the right by treeNodeInfo.Level tabs, so that the lower the person is within the hierarchy, the more to the right he will be.

Here is what we get when we print the resulting collection returned by ceo.SelfAndDescendantsWithLevelInfo(toChildren):

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

Next we demonstrate SelfAndAncestors(...) method:

#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(...) will return a collection consisting of the calling node and its ancestors ordered from the node to the root node. In our sample, it will print Nick, then, Nick's manager John, then, John's manager - Tom - the CEO:

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

Extension method Desendants(...) will return all the descendants of the passed node (but not the node itself):

#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  

The code above will show all the accountants (descendants of the accountingManager node):

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

The methods we presented above, required only one delegate (either up tree delegate toParent or down tree delegate toChildren). Now I will show a couple of methods that require both of them.

AncestorsAndDescendantsFromTop(...) extension method, returns a collection of all ancestors of the current node, starting from the root node, the current node itself and all its descendants:

#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  

As promised, the above functionality will print Greg's (the accountant manager's) seniors, himself and his reports:

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

Finally method AllButAncestorsAndDescendants will return a collection containing every node except for the current node, its ancestors and descendants:

#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  

will print:

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

The extension methods are defined within well documented NP.Paradigms.TreeUtils static class.

Visual and Logical WPF Trees to LINQ

We can use TreeUtils extension methods demo'ed in the last section also to perform LINQ operations on WPF visual and logical trees. VisualTreeTests project under TESTS folder shows such example.

Note that the project depends not only on NP.Paradigms project, but also on NP.Paradigms.Windows project. The latter project depends on Microsoft's visual libraries: WindowBase.dll and PresentationCore.dll (while the former project does not require them). The purpose of NP.Paradigms.Windows project is to adapt the plain C# implementations of WPF concepts to be used in WPF and XAML.

There are two static classes within the HP.Paradigms.Windows project that we are interested in - VisualTreeUtils and LogicalTreeUtils. In general, they are wrappers around the NP.Paradigms.TreeUtils methods, only we use different prefixes (in order to distinguish between them) and the Up and Down tree delegates are defined by methods that navigate up or down the visual or logical tree correspondingly.

Let us look at NP.Paradigms.Windows.VisualTreeUtils static class. Here is how it defines the Up and Down tree delegates:

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;
    };  

We use VisualTreeHelper for the delegate's implementations.

Now, every extension method within the VisualTreeUtils class is a wrapper around the corresponding method within NP.Paradigms.TreeUtils class with the appropriate delegate plugged in as one of the arguments. The name of the method is the same as that of TreeUtils only prefixed with 'Visual'. For example here is the implementation of the VisualAncestors method:

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

VisualTreeUtils.VisualAncestors method wraps TreeUtils.Ancestors method with toParent delegatepassed to it as the argument.

Another example is the VisualDescendantsWithLevelInfo(...) method:

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

Now, take a look at NP.Paradigms.Windows.LogicalTreeUtils class. Here is how we define Up and Down tree delegates in it:

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>();
    };  

The wrapper methods have prefix Logical instead of Visual e.g.:

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

Also, those who work with WPF know that logical nodes do not have to be of FrameworkElement type, they can be of simple types, e.g. string. Therefore, we are forced to assume that the nodes are objects, not FrameworkElements as in the case of the visual tree.

Now, let us run the VisualTreeTests project - here is what we get:

We can see the visual and logical tree nodes of the grid panel containing text "Element To Display" and the button hierarchy.

To display a single tree node in the WPF window, we use the class VisualTreeTests.TreeNodeDisplayer.

It has one property string StringToDisplay that contains the string that should be displayed. It also has two constructors allowing to create the TreeNodeDisplayer for a visual tree node and for a logical tree node. Each of the constructors sets the StringToDisplay property of the object, to show the information about the node (its type and its name).

MainWindow class constains two dependency properties VisualTreeCollection and LogicalTreeCollection - both are collections of TreeNodeDisplayer objects corresponding to the visual and logical hierarchies. These collections are set within MainWindow_Loaded event handler:

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 file contains the element we want to display - it is a grid with a text and a button in it:

<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> 

It also contains two ItemsControl elements - one for displaying the visual tree information and one for displaying the logical tree information:

<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>  

The ItemsSource properties of these ItemsControl objects are bound to the VisualTreeCollection and LogicalTreeCollection dependency properties of the MainWindow.

REvents on Trees

WPF Routed Events on Visual Trees

There is a concept of Attached Routed Events in WPF. Unlike the usual C# events they

  1. Can be defined outside of the class that fires them and 'Attached' to the objects.
  2. Can propagate up and down the WPF visual tree - in the sense that the event can be fired by one tree node and handled on another tree node (one of the firing node's ancestors).

There are 3 different modes of propagation for the routed events:

  1. Direct - this means that the event can only be handled on the same visual tree node that fires it.
  2. Bubbling - the event travels from the current node (the node that raises the event) to the root of the visual tree and can be handled anywhere on the way.
  3. Tunneling - the event travels from the root node of the visual tree to the current node (the node that raises the event).

The following pictures depict bubbling and tunneling event propagation:

Note that both bubbling and tunneling events operate on the node and its ancestors (only in the opposite directions). There is no Routed Event propagation mode that would spread from a node to its descendants.

If a bubbling or tunneling Routed Event is handled at some node, it will still propagate further to the very end, and one can set a Routed Event handler beyond the first handling point.

REvents on Arbitrary Trees

Here I describe implementation of a paradigm very similar to the Routed Events but more powerful and generic. I call this paradigm REvents (just as in the first article in the series I called generic non-WPF implementation of Attached Properties - AProps - see Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings).

REvents are more powerful than Routed Events because:

  1. They can operate on arbitrary trees - not only on WPF Visual Trees. The trees that they can operate on should be defined by the Up and Down tree delegates described above.
  2. In addition to bubbling and tunneling modes, there is also a special mode for propagating from a node that raises the event down to the node's descendants.
  3. The Routed Event propagation mode can only be determined at Routed Event creation, while the mode of REvent can be set during its invocation, so the same REvent can be used with different propagation modes.

We call this extra REvent propagation mode - PropagateToDescendants mode. It allows the REvent handling to happen on the descendants of the node (both bubbling and tunneling propagation happen on the nodes ancestors, only in different order).

Important Note: the picture above and the sample below show event propagation from the root node of the tree to its descendants. In fact the REvent functionality supports propagation from any tree node to its descendants (the node that raises the REvent does not have to be the root node).

The usage sample for REvents is located under TESTS/REventsTest solution.

In this project we show examples of event propagation on the same tree as in the first sample - tree representing a small organization:

All the code demonstrating the REvent usage is located within Program.Main() method.

First we assemble organization, just as we did above:

#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

Then we create the up and down tree navigation delegates:

#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  

Next we create an REvent approveExpenseEvent that takes string expenseName and double dollarAmount as arguments:

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

Now, we create a handler for this event. The handler will approve any expense under $10000 (printing the corresponding message detailing the expense name and amount). The expenses above $10000 will be rejected and further message propagation will be stopped:

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());            
        }
    };  

Note that the first argument passed to an REvent handler is always of type REventInfo<NodeType> and it contains the

  1. SenderNode - specifying the node that raised the event
  2. HandlerNode - specifying the node that handles the event
  3. IsCanceled - flag that allows to cancel the event propagation
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; }
}  

Next we add this event handler to ceo and developmentManager nodes:

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

Now we raise the bubble event on dev1 node with expenseName being "Travel Expense" and the expense amount being $1000. This amount is smaller than the $10000 threshold, so the event should bubble from dev1 through developmentManager to the ceo and both the development manager and the CEO will approve the expense:

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

This will result in the following text printed:

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

Note that since the event bubbles - it will be handled by developmentManager first and by the ceo after that.

Next, we'll demonstrate event cancellation, assuming dev2 raises the same event, with amount $20000 (which is higher than $10000 threshold). In this case it will go first to developmentManager John, he will reject it and it will never make it to the CEO:

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

will result in the following printout:

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

Now let us demo event tunneling. Let us assume that the company has a non-standard procedure where any expense is first being sent for approval to the CEO who (after approving it) sends it down to the corresponding lower level managers. If the CEO (or a manager) rejects it, the event is not propagated further down.

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

This will result in the following printout:

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

Note that we start with the root of the organization tree - Tom, the CEO, and move down towards the node that raised the tunneling event.

Now we demo tunneling with cancellation:

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

will result in:

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

Finally, we are going to demo the event propagation from a tree node to all of its descendants - something that WPF Routed Events cannot do.

We create a new REvent readMemoEvent that takes one string argument corresponding to the memo name:

 REvent<orgperson, string=""> readMemoEvent = new REvent<orgperson, string="">();  
</orgperson,>

This corresponds to a memo that is being sent down from the CEO to each member of his organization.

Now, we specify the memo event handler:

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

It prints that the corresponding person within the organization read the memo.

We set this handler on every node within the tree:

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

Now we call RaiseEventPropagateToDescendants(...) method on the ceo node to execute the event on the node itself and propagate it to all the descendants of this node:

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

This will result in the following printout:

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'  

Any node can cancel the 'to descendants' propagation just like in case of bubbling and tunneling (even though we do not show it in our demo).

REvent Implementation Notes

NP.Paradigms.REvent class is defined within REvent.cs file under NP.Paradigms project:

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 stores the mapping between a tree node and an EventWrapper object. EventWrapper is a private class defined in the same file.

There are several versions of AddHandler(...) method defined within REvent class - for adding 5, 4, 3, 2 and 1 argument handlers. The first argument of the handler is always of type REventInfo<NodeType> described above. The next 4, 3, 2 or 1 arguments can be of any generic type. Here is an example of a 5 argument AddHandler method:

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;
}  

To get the event wrapper for the node, we use the GetOrCreateEventWrapper(...) method:

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 is a private class that allows adding and removing event handlers. It has a field _event of List<Action<REventInfo<NodeType>, T1, T2, T3, T4>> that allows accumulating the event handlers and calling them. It also has an event TheEvent for adding and removing the handlers to and from the _event field:

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;
    }
}  

The reason we basically re-implemented the MS event is that if there are multiple handlers, and one of them canceled the processing, we want to skip executing the rest of the handlers:

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;
}  

Note that various AddHandler(...) methods of REventWrapper<NodeType, T1, T2, T3, T4> class allow to add handlers with different number of arguments (5, 4, 3, 2 and 1). Just like in the case of REvent, the first argument is always of type REventInfo<NodeType> while the rest of the arguments' types' can be arbitrarily chosen.

Here are a couple of examples:

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);
}  

adds a 5 argument handler, while

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);
}  

adds a two argument handler.

Since TheEvent is always of type Action<REventInfo<NodeType>, T1, T2, T3, T4>, we use delegate expressions to convert the handler with smaller number of arguments to the type of TheEvent. For example, the code that does it for the two argument delegate is

// 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);

Method AddFunctionFromDelegate adds the 5 argument delegate to TheEvent, but also stores the mapping between the original action and the 5 argument delegate within _delegateToFuncMap dictionary:

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;
}  

This is done in order to enable removal of the 5 argument delegate by action, when method RemoveActionHandler is called:

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;
}  

There is also a method RemoveAllHandlers that clears the REventWrapper<NodeType> object of all event handlers:

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

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

Now, let us go back to REvent<NodeType, T1, T2, T3, T4> class. As we mentioned above, it contains an AProp _aProperty that enables creating an REventWrapper object for accumulating the event handlers for a node on the tree. Such an object is created or found by GetOrCreateEventWrapper(...) private helper method:

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;
}  

As mentioned above, we have multiple AddHandler methods allowing to add the handlers with a different number of arguments. E.g. here is the AddHandler method for adding a 4 argument handler:

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);
}  

In order to remove a handler from a node, we can use the RemoveHandler(...) method:

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);
    }
}  

There is also a method RemoveAllHandlers(NodeType node) that removes all REvent handlers from a tree node:

public void RemoveAllHandlers(NodeType node)
{
    REventWrapper<nodetype, t4=""> 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);
}
</nodetype,>

We have several methods for raising the REvent.

Method RaiseEvent results in the event (possibly) being handled on the same node as it is raised on, corresponding to the Direct mode for Routed Events:

// 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);
}  

Private method RaiseEventIterateThroughAncestors is used for bubbling and tunneling the 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;
}  

There are two methods RaiseBubbleEvent(...) and RaiseTunnelEvent(...) for raising bubbling and tunneling REvent correspondingly and they both use the utility method RaiseEventIterateThroughAncestors(...) described above:

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);
}  

Finally, there is a method RaiseEventPropagateToDescendants(...) that propagates the event to the descendants - down the tree:

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;
}  

Summary

This article continues the topic of implementing WPF concepts outside of WPF that I started in Plain C# implementation of WPF Concepts - Part 1 AProps and Introduction to Bindings and WPF-less Property Bindings in Depth - One Way Property Bindings (WPF Concepts Outside of WPF - Part 2)

Here I discuss implementing a generic tree structure by using toParent and toChildren delegates and implementing REvent concepts - events that propagate on the generic trees and are similar, but more powerful and more generic than WPF's Routed Events.

Acknowledgement

I want to thank the best, the brightest and the most prolific on the codeproject and outside of it who inspire me to look for new ways in developing software and sharing them with others, including, but not limited to Sacha Barber, Colin Eberhardt, Paulo Zemek, Florian Rappl, Dr. ABell, Alex Volynsky.

I also would like to thank my dear 7 year old daughter, who asked me to place this acknowledgement because she wanted to be acknowledged just like her older sister was several years ago, see the end of Prism for Silverlight/MEF in Easy Samples. Part 3 - Communication between the Modules article.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here