Event Cache Component:
This article introduces a reusable generic event cache that houses events with custom bubbling capabilities. The bubbled events can be defined during runtime in addition to being defined in the normal way as part of a class definition. The event bubbling can be "handled" and stopped by any recipient of the bubbled event. I'll describe the basic architecture of the component and describe how to consume it.
I recently was working on a project where my domain model was a grid structure that needed event bubbling capabilities. I also needed these bubbled events to be handled by any node in the grid so they would stop bubbling. Here was my solution…. A cache for events.
The idea is that objects needing to expose this functionality will have an instance of the event cache as a member component. They can then expose whatever functionality is required to perform the work they need to do. Let's first dig into the basic functionality of the event cache and then we'll look at how to implement it in your object model. Finally, I'll show a sample of how to wire up the event bubbling.
EventCache Features:
- On-the-fly event definition.
- Event bubbling
- Event handling so bubbling can be stopped.
- Contraints in place to ensure there is not a cyclic graph when bubbling events
Simple Samples:
The following sample demonstrates how the event cache can receive event definitions on-the-fly and also then fire them off. All the events in the event cache have a distinct name that is used to lookup the event.
private static void Method_Event_Subscription_Sample()
{
EventCache
cache = new EventCache();
const string
name = "Method_Event_Subscription() sample";
Object
sender = null;
// defining the event run-time
// we can get an event for subscription with a name
cache.GetEvent(name).Raised += Sample_Raised;
// this is how we can raise the event
cache.RaiseEvent(sender, name);
}
private static void Sample_Raised(Object sender, EventArgs args)
{
Console.WriteLine("Sample_Raised() fired");
} |
The subscription could also be done using a delegate with the following method:
private static void Method_Event_Subscription_Sample()
{
EventCache
cache = new EventCache();
const string
name = "Method_Event_Subscription() sample";
Object
sender = null;
// defining the event run-time
// Handler can be a method with the correct signature
cache.Subscribe(name, Sample_Raised);
// raise the event
cache.RaiseEvent(sender, name);
} |
Generic Message Object:
We might have need to pass a message object along with the event, so there is a generic BubbleEvent<T> to carry a message object:
public class BubbleEventArgs<TValue>:BubbleEventArgs
{
public BubbleEventArgs(String commandName, TValue value):base(commandName)
{
m_Value = value;
}
private readonly TValue
m_Value;
public TValue Value
{
get { return m_Value; }
}
} |
This sample demonstrates passing a Guid as a message.
private static void Generic_Transport_Object_Sample()
{
EventCache
cache = new EventCache();
const String
name = "Generic_Transport_Object_Sample";
Object
sender = null;
// we can call .GetEvent<> and define a type that will be
// used to transport data with the arguments
// (available in the .Value property)
cache
.GetEvent<Guid>(name)
.Raised += (snd, arg) =>
Console.WriteLine(
"Generic_Transport_Object_Sample() fired, with transport " +
arg.Value.ToString());
cache.RaiseEvent(sender, name, Guid.NewGuid());
} |
We may also want a strongly typed reference to the sender, so there is also a BubbleEvent<TValue, TSender> class.
public class BubbleEventArgs<TValue, TSource>:BubbleEventArgs<TValue>
{
public BubbleEventArgs(String commandName, TValue value, TSource source)
: base(commandName, value)
{
m_Source = source;
}
private readonly TSource
m_Source;
public TSource Source
{
get { return m_Source; }
}
} |
Using the EventCache:
Here is an example of how we would implement the EventCache() :
1) Set the EventCache as a member variable
2) 1) Instantiate the EventCache class (in constructor)
3) 2) Declare the event Handler with one of the following three BubbleEventArg types
a. public event EventHandler<BubbleEventArgs> EventRaised
b. public event EventHandler<BubbleEventArgs<TMessage>> EventRaised
c. public event EventHandler<BubbleEventArgs<TMessage, TSourceType>> EventRaised
4) 3) In the event declaration, wire up to the EventCache object as a backend store to house the event.
5) 4) [optional] Wrap the event firing mechanism in a method which will also use the EventCache object.
private class SuperWired
{
public SuperWired(String name)
{
m_EventCache = new EventCache();
m_Name = name;
}
private readonly EventCache
m_EventCache;
private readonly string
m_Name;
public string Name
{
get { return m_Name; }
}
public event EventHandler<BubbleEventArgs<Guid, SuperWired>> EventRaised
{
add { m_EventCache.Subscribe("EventRaised", value); }
remove { m_EventCache.Unsubscribe("EventRaised", value); }
}
private void RaiseEvent(Guid transport)
{
m_EventCache.RaiseEvent(this, "EventRaised", transport, this);
}
} |
Now we can consume the event just like any normal event
private static void Generic_Transport_Object_With_Strongly_Typed_Source_Sample()
{
// sometimes it would be nice to have a strongly typed source
// object to deal with instead of the Object refrence nomally
// passed in a event handler method.
SuperWired super = new SuperWired("Dude");
super.EventRaised += (snd, arg) =>
Console.WriteLine(
arg.Source.Name + // Source is the sender but now strongly typed
" fired the event with value " +
arg.Value.ToString()); // The data transport object is available
} |
Named Events On-The-Fly:
We can declare named events on-the-fly in our classes using the EventCache as well by exposing the functionality of the EventCache from a business object in the following way:
private class EventClass
{
public EventClass()
{
m_EventCache = new EventCache();
}
private readonly EventCache
m_EventCache;
public EventHost GetEvent(String commandName)
{
return m_EventCache.GetEvent(commandName);
}
public void FireEvent(String commandName)
{
m_EventCache.RaiseEvent(this, commandName);
}
} |
Now we can create new named events and fire them during run time:
EventClass sampleOne = new EventClass();
sampleOne
.GetEvent("DynamicEvent") // the event is created when it is first requested
.Raised += (snd, arg) =>
Console.WriteLine(arg.CommandName + " raised");
sampleOne.FireEvent("DynamicEvent"); |
We can also use the same technique to expose events with the generic message transport object:
private class GenericEventClass
{
public GenericEventClass()
{
m_EventCache = new EventCache();
}
private readonly EventCache
m_EventCache;
public EventHost<T> GetEvent<T>(String commandName)
{
return m_EventCache.GetEvent<T>(commandName);
}
public void FireEvent<T>(String commandName, T value)
{
m_EventCache.RaiseEvent(this, commandName, value);
}
} |
The class raising the event would be consumed in a very similar way
GenericEventClass sampleTwo = new GenericEventClass();
sampleTwo
.GetEvent<Guid>("DynamicEvent") // the event is created when first requested
.Raised += (snd, arg) =>
Console.WriteLine(
arg.CommandName +
" raised with " +
arg.Value.ToString());
sampleTwo.FireEvent("DynamicEvent", Guid.NewGuid()); |
Event Bubbling:
In order to take advantage of the event bubbling, we need to add the recipients that should receive the bubbled events using the EventCache.BubblesEventsTo() method. The BubbleEventArgs have a "Handled' property that will stop the event delegate from firing and stop the bubbling when set to "true'. There are constraints on wiring up the event bubbling in that you cannot have a cyclic graph because of the potential for endless recursive loops.
In this sample, we have a Person class that listens for a child to cry and will bubble the event to parents.
public class Person
{
/// <summary>
/// Initializes a new instance of the Person class.
/// </summary>
/// <param name="name"></param>
public Person(String name)
{
m_Name = name;
}
private readonly EventCache
m_BubbleManager = new EventCache();
private readonly String
m_Name;
public String Name
{
get { return m_Name; }
}
private readonly List<Person>
m_Children = new List<Person>();
private void ListenTo(Person child)
{
child.m_BubbleManager.BubbleEventsTo(m_BubbleManager);
}
public void SetChild(Person child)
{
ListenTo(child);
m_Children.Add(child);
}
public void RaiseCryEvent()
{
BubbleEventArgs<Person> arg = new BubbleEventArgs<Person>("Cry", this);
m_BubbleManager.RaiseEvent(this, arg);
if(! arg.Handled)
Console.WriteLine("Nobody cared");
}
public event EventHandler<BubbleEventArgs<Person>> BabyCry
{
add { m_BubbleManager.Subscribe("Cry", value); }
remove { m_BubbleManager.Unsubscribe("Cry", value); }
}
} |
To wire up the events in our Person object, we'll use the SetChild() method:
static void Main(string[] args)
{
Person baby = new Person("baby");
Person mom = new Person("mom");
Person dad = new Person("dad");
Person grandmaX = new Person("grandma X");
Person grandpaX = new Person("grandpa X");
Person grandmaY = new Person("grandma Y");
Person grandpaY = new Person("grandpa Y");
mom.SetChild(baby);
dad.SetChild(baby);
grandmaX.SetChild(mom);
grandpaX.SetChild(mom);
grandmaY.SetChild(dad);
grandpaY.SetChild(dad);
} |
Now we'll wire up the event handlers so we can see what is going on:
baby.BabyCry += (snd, arg) => Console.WriteLine("Baby cry");
mom.BabyCry += (snd, arg) => Console.WriteLine("Mom hears " + arg.Value.Name + " cry");
dad.BabyCry += (snd, arg) => Console.WriteLine("Dad hears " + arg.Value.Name + " cry");
grandmaX.BabyCry += (snd, arg) => Console.WriteLine("Grandma X hears " + arg.Value.Name + " cry");
grandpaX.BabyCry += (snd, arg) => Console.WriteLine("Grandpa X hears " + arg.Value.Name + " cry");
grandmaY.BabyCry += (snd, arg) => Console.WriteLine("Grandma Y hears " + arg.Value.Name + " cry");
grandpaY.BabyCry += (snd, arg) => Console.WriteLine("Grandpa Y hears " + arg.Value.Name + " cry"); |
So when we fire the event:
Console.WriteLine("\n\nStart");
baby.RaiseCryEvent();
Console.WriteLine("End"); |
We get the following output:
Start
Baby cry
Mom hears baby cry
Grandma X hears baby cry
Grandpa X hears baby cry
Dad hears baby cry
Grandma Y hears baby cry
Grandpa Y hears baby cry
Nobody cared
End |
If we were to add a method to set the "Handled' property to true we'll get different behavior:
Console.WriteLine("\n\nStart");
grandmaX.BabyCry += (snd, arg) => { arg.Handled = true; Console.WriteLine("handled by grandma"); };
baby.RaiseCryEvent();
Console.WriteLine("End"); |
We'll get different output:
Start
Baby cry
Mom hears baby cry
Grandma X hears baby cry
handled by grandma
End |
The code and more samples for this article are on CodePlex at: http://eventcache.codeplex.com. If you are interested in contributing, find some bugs or have some comments or suggestions please let me know. I hope you find this component as useful as I have.
Until next time,
Happy Coding