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 OrgPerson
s 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:
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.
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
{
public string Name { get; set; }
public Position ThePosition { get; set; }
public OrgPerson Boss { get; set; }
public List<orgperson> ManagedPeople { get; set; }
public OrgPerson()
{
Boss = null;
ManagedPeople = new List<orgperson>();
}
public OrgPerson(string name, Position postion) : this()
{
Name = name;
ThePosition = postion;
}
public void AddManaged(OrgPerson child)
{
child.Boss = this;
ManagedPeople.Add(child);
}
public OrgPerson AddAndCreateManaged(string name, Position position)
{
OrgPerson createdChildObj = new OrgPerson(name, position);
this.AddManaged(createdChildObj);
return createdChildObj;
}
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
Func<orgperson, orgperson=""> toParent = (orgPerson) => orgPerson.Boss;
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>
{
public NodeType TheNode;
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:
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 FrameworkElement
s 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)
{
IEnumerable<TreeNodeInfo<FrameworkElement>> visualTreeNodes = ElementToDisplay.VisualSelfAndDescendantsWithLevelInfo();
VisualTreeCollection = visualTreeNodes.Select((TreeNodeInfo) => new TreeNodeDisplayer(TreeNodeInfo)).ToList();
IEnumerable<TreeNodeInfo<object>> logicalTreeNodes = ElementToDisplay.LogicalSelfAndDescendantsWithLevelInfo();
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
- Can be defined outside of the class that fires them and 'Attached' to the objects.
- 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:
- Direct - this means that the event can only be handled on the same visual tree node that fires it.
- 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.
- 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:
- 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.
- 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.
- 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
Func<OrgPerson, OrgPerson> toParent = (orgPerson) => orgPerson.Boss;
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; }
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
SenderNode
- specifying the node that raised the event
HandlerNode
- specifying the node that handles the event
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 (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
Action<REventInfo<NodeType>, T1, T2, T3, T4> actionToAdd = (eventInfo, t1, t2, t3, t4) =>
{
action(eventInfo, t1);
};
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;
if (_delegateToFuncMap == null)
{
_delegateToFuncMap = new Dictionary<object, Action<REventInfo<NodeType>, T1, T2, T3, T4>>();
}
_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;
}
_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)
{
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)
{
REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = GetOrCreateEventWrapper(obj);
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)
{
REventWrapper<NodeType, T1, T2, T3, T4> eventWrapper = _aProperty.GetProperty(node);
if (eventWrapper == null)
return;
eventWrapper.RemoveActionHandler(handler);
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;
eventWrapper.RemoveAllHandlers();
_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:
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);
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.
private bool RaiseEventIterateThroughAncestors
(
NodeType node,
Func<NodeType, NodeType> toParentFunction,
bool shouldBubbleOrTunnel, T1 t1 = default(T1),
T2 t2 = default(T2),
T3 t3 = default(T3),
T4 t4 = default(T4)
)
{
IEnumerable<NodeType> ancestorIterator = node.SelfAndAncestors(toParentFunction);
if (!shouldBubbleOrTunnel) {
ancestorIterator = ancestorIterator.Reverse();
}
REventInfo<NodeType> eventInfo = new REventInfo<NodeType>(node, null);
foreach (NodeType currentNode in ancestorIterator)
{
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;
}
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 , 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 , 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.