Archive for December, 2013

Attached and Routed Events outside of WPF

December 1, 2013

Code Location

The code for this blog post can be downloaded from
AttachedRoutedEvents.zip.

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 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 REvents can also be propagating
from node to node on a tree in a manner similar to that of
WPF’s RoutedEvents propagating on a visual tree.

Attached and Routed Events in WPF

Each event has 4 important actions associated with it:

  1. Defining an event
  2. Adding handlers to an Event
  3. Removing handlers from an Event
  4. 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 action above for attached WPF events.

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

  2. 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 an Routed Event.

  3. Removing a Routed Event is done in a similar fashion:

            myObject.RemoveHandler(MyRoutedEvent, eventHandlerFunction)
          
  4. 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 they 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 anscestor 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 anscestors) are called Bubbling events.
The Routed Events can also visit the anscestors 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 (registerd).

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 FrameworkElements. 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:

// to parent function
Func toParentFn =
    (treeNode) => treeNode.Parent;

// to children function
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);

// print all the tree nodes
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:

// assign handlers for each of the 
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:

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 na 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 to
a Func that always returns false:

// stopping propagation by returning false from a handler
aTestEvent.RemoveAllHandlers(level2Node2);

aTestEvent.AddHander
(
    level2Node2, 
    () => 
    {
        Console.WriteLine("Terminating event propagation at node " + level2Node2.NodeInfo); 
        return false;
    }); // terminate event propagation at this node

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 Actions and Funcs 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 if 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.

Advertisements