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

Attached and Routed Events Outside of WPF

0.00/5 (No votes)
1 Dec 2013 2  
A series of blog posts regarding implementing WPF concepts outside of WPF

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 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 actions 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 a 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 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 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:

[__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:

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

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