Introduction
Here, I continue a series of blog posts regarding implementing WPF concepts outside of WPF.
In Attached Properties outside of WPF, I introduced a notion of AProperty
– attached property implementation
outside of WPF. Unlike WPF attached property, such property can be attached to any object - not only to DependencyObject
.
Just like AProperty
can be added to an object without modifying the object’s code, we can come up with the REvent
concept similar to WPF’s attached event in order to add an event to an object without modifying the object’s code.
In Tree Structures and Navigation, we were talking about navigating a Tree
from node to node. The REvent
s can also be propagating from node to node on a tree in a manner similar to that of WPF’s RoutedEvent
s propagating on a visual tree.
Attached and Routed Events in WPF
Each event has 4 important actions associated with it:
- Defining an event
- Adding handlers to an Event
- Removing handlers from an Event
- Raising or Firing an Event from an object
In WPF and Silverlight, one can use Attached Events - events defined outside of an object in which they are being
raised. These Attached Events are also called Routed Events for the reason that we are going to explain shortly.
Here is how we define those 4 actions above for attached WPF events:
- Unlike plain C# events, the Attached Events should be defined outside of an object in a
static
class:
public static RoutedEvent MyEvent=EventManager.RegisterRoutedEvent(...)
The Routed Event is associated with an object when it is being raised from it.
- In order to add a handler to a Routed Event to an object, the following code is employed:
myObject.AddHandler(MyRoutedEvent, eventHandlerFunction, ...)
The myObject
should always be a FrameworkElement
in order to be able to detect and handle a Routed Event.
- Removing a Routed Event is done in a similar fashion:
myObject.RemoveHandler(MyRoutedEvent, eventHandlerFunction)
- We can raise a Routed Event on a
FrameworkElement
object by using its RaiseEvent
method:
myObject.RaiseEvent(routedEventArgs)
routedEventArgs
should contain a reference to the static
Routed Event object defined (registered)
as MyEvent
, above.
These WPF Attached Events are also called Routed Events because the handler for such event does not have to be defined on the same object on which the event is raised or not even on an object that has a reference to an object on which such event is raised.
In fact, Routed Event can propagate through the ancestor of object that raised such event within the Visual Tree that the object belongs to.
The Routed Events propagating up the Visual Tree (from the raising object to its ancestors) are called Bubbling events.
The Routed Events can also visit the ancestors first, starting from the root node of the Visual Tree and ending the propagation at the node that raised the Routed Event. Such events are called Tunneling events. Routed Events can also be neither Bubbling nor Tunneling – in that case, they can only be handled on the object that raised them. Such events are called Direct event.
The routing behavior of an event (whether it is Bubbling, Tunneling or Direct is determined at the stage when the event is defined (registered).
What We Are Trying to Achieve
Here, we are implementing Routed Event WPF concept outside of WPF. Such event can Bubble and Tunnel
and
also can propagate to Tree Node‘s descendants on any Tree defined by its up and down propagation functions, not only the Visual Tree. Such events can also be attached to any objects, not only to FrameworkElement
s. The routing behavior of such an event is determined at the time when it is raised, not at the time when it is defined. The event can have up to 4 arguments specified by generic types.
Usage Example
We are going to show how the API is being used first, and only after that describe the implementation.
The test code is located within Program.cs file under AttachedRoutedEventTest
project. We build the Tree in exactly the same fashion as it was done in Tree Structures and Navigation:
#region Start building tree out of TestTreeNodes objects
TestTreeNode topNode = new TestTreeNode { NodeInfo = "TopNode" };
TestTreeNode level2Node1 = topNode.AddChild("Level2Node_1");
TestTreeNode level2Node2 = topNode.AddChild("Level2Node_2");
TestTreeNode level3Node1 = level2Node1.AddChild("Level3Node_1");
TestTreeNode level3Node2 = level2Node1.AddChild("Level3Node_2");
TestTreeNode level3Node3 = level2Node2.AddChild("Level3Node_3");
TestTreeNode level3Node4 = level2Node2.AddChild("Level3Node_4");
#endregion End tree building
Here is how we define the toParentFunction
and toChildrenFunction
for the tree:
Func toParentFn =
(treeNode) => treeNode.Parent;
Func<TestTreeNode, IEnumerable> toChildrenFn =
(treeNode) => treeNode.Children;
First, we print all the nodes of the tree shifted to the right in proportion to their distance from the top node:
IEnumerable<TreeChildInfo<TestTreeNode>> allTreeNodes =
topNode.SelfAndDescendantsWithLevelInfo(toChildrenTreeFunction);
Console.WriteLine("\nPrint all nodes");
foreach (TreeChildInfo<TestTreeNode> treeNode in allTreeNodes)
{
string shiftToRight = new string('\t', treeNode.Level + 1);
Console.WriteLine(shiftToRight + treeNode.TheNode.NodeInfo);
}
Here is the result of printing:
TopNode
Level2Node_1
Level3Node_1
Level3Node_2
Level2Node_2
Level3Node_3
Level3Node_4
Here is how we create REvent
:
REvent<TestTreeNode, string> aTestEvent = new REvent();
By the type arguments, we specify that this REvent
will act on objects of the type TestTreeNode
and will be accepting objects of type string
as arguments – overall, we can specify from 0 to 4 arguments of different types for the REvent
objects.
Now, we can set our REvent
‘s handlers for all the nodes of the tree:
foreach (TreeChildInfo<TestTreeNode> treeNodeWithLevelInfo in allTreeNodes)
{
TestTreeNode currentNode = treeNodeWithLevelInfo.TheNode;
aTestEvent.AddHander
(
currentNode,
(str) =>
{
Console.WriteLine("Target Node: " + currentNode.NodeInfo + "\t\t\tSource Node: " + str);
}
);
}
The handler would print the current node’s name and the string
passed to the handler as the source node’s name (it is assumed that the RaiseEvent
function has the name of the raising tree node passed as an argument).
Now we raise different events (bubbling, tunneling, direct and propagating to children) and observe the results.
Bubbling Event
Console.WriteLine("\nTesting event bubbling:");
aTestEvent.RaiseBubbleEvent(level3Node3, toParentTreeFunction, level3Node3.NodeInfo);
Bubbling event raised from the bottom level node level3Node3
will print the node itself and all its ancestors printing first those who are closer to the originating node level3Node3
:
[__strong__]Testing event bubbling:
Target Node: Level3Node_3 Source Node: Level3Node_3
Target Node: Level2Node_2 Source Node: Level3Node_3
Target Node: TopNode Source Node: Level3Node_3
Tunneling Event
Console.WriteLine("\nTesting event tunneling:");
aTestEvent.RaiseTunnelEvent(level3Node3, toParentTreeFunction, level3Node3.NodeInfo);
Tunneling event will print the same nodes in the opposite order – starting from the top node and
ending with the originating node:
Testing event tunneling:
Target Node: TopNode Source Node: Level3Node_3
Target Node: Level2Node_2 Source Node: Level3Node_3
Target Node: Level3Node_3 Source Node: Level3Node_3
Direct Event
Console.WriteLine("\nTesting event Direct Event (without bubbling and tunneling):");
aTestEvent.RaiseEvent(level3Node3, level3Node3.NodeInfo);
Direct event will only print on the invoking node:
Testing event Direct Event (without bubbling and tunneling):
Target Node: Level3Node_3 Source Node: Level3Node_3
Event Propagating to Descendents
Console.WriteLine("\nTesting event propagation to descendents:");
aTestEvent.RaiseEventPropagateToDescendents(level2Node1, toChildrenTreeFunction, level2Node1.NodeInfo);
Event propagating to descendents fired from level2Node1
node located at the middle level, will print the node itself and its two descendents:
Testing event propagation to descendents:
Target Node: Level2Node_1 Source Node: Level2Node_1
Target Node: Level3Node_1 Source Node: Level2Node_1
Target Node: Level3Node_2 Source Node: Level2Node_1
Terminating Event Propagation
One can pass a Func
instead of an Action
to become an event handler for the REvent
. In that case, returning false
from that function would terminate the REvent
propagation – analogous to setting e.Cancel=true
for WPF’s routed event.
Below, we clear the event handler at level2Node2
node and reset to a Func
that always returns false
:
aTestEvent.RemoveAllHandlers(level2Node2);
aTestEvent.AddHander
(
level2Node2,
() =>
{
Console.WriteLine("Terminating event propagation at node " + level2Node2.NodeInfo);
return false;
});
After this, we re-run the bubbling, tunneling and propagating to children events:
Console.WriteLine("\nTesting event bubbling with event propagation termination:");
aTestEvent.RaiseBubbleEvent(level3Node3, toParentTreeFunction, level3Node3.NodeInfo);
Console.WriteLine("\nTesting event tunneling with event propagation termination:");
aTestEvent.RaiseTunnelEvent(level3Node3, toParentTreeFunction, level3Node3.NodeInfo);
Console.WriteLine("\nTesting event propagation to descendents with event propagation termination:");
aTestEvent.RaiseEventPropagateToDescendents(topNode, toChildrenTreeFunction, topNode.NodeInfo);
The results of these are shown below:
Testing event bubbling with event propagation termination:
Target Node: Level3Node_3 Source Node: Level3Node_3
Terminating event propagation at node Level2Node_2
Testing event tunneling with event propagation termination:
Target Node: TopNode Source Node: Level3Node_3
Terminating event propagation at node Level2Node_2
Testing event propagation from the TopNode to its descendents with event propagation termination:
Target Node: TopNode Source Node: TopNode
Target Node: Level2Node_1 Source Node: TopNode
Target Node: Level3Node_1 Source Node: TopNode
Target Node: Level3Node_2 Source Node: TopNode
Terminating event propagation at node Level2Node_2
You can see that the event propagation stops indeed at level2Node2
node.
Notes on Implementation
NP.Paradigms.REvent
implementation code is located within REvent.cs file under NP.Paradigms
project.
REvent
class defines AProperty _aProperty
. Its purpose is to provide a mapping between the objects on which some REvent
handlers were set and objects of type REventWrapper
that actually saves the REvent
handlers.
REventWrapper
has _event
member of List<Func<T1,T2,T3,T4,bool>>
type. It accumulates all of the event handlers associated with its object. It also has a bunch of functions that help to convert Action
s and Func
s with smaller amount of generic type arguments into Func<T1,T2,T3,T4, bool>
. There is also a map: Dictionary<object, Func> _delegateToFuncMap
that stores the mapping between the original Action
or Func
with smaller number of generic type arguments and the final Func<T1,T2,T3,T4>
. This is needed if we want to remove a handler – we’ll need to find the correct Func<T1,T2,T3,T4>
and remove it from the _event
list.
REvent
class has various functions for adding the event handlers to an object. It also has functions for raising event on an object so that it would propagate in a required fashion: bubbling, tunneling, direct or propagation to children – as they were presented in the usage example section.