For a .NET 2.0 version, see Pete O'Hanlon's article.
Preface
The EventPool is part of the Model-View-Controller (MVC) technology roadmap series, which includes:
- EventPool (this article)
- IMemento (tbd)
- UndoBuffer (tbd)
- StateManager (tbd)
- XmlGuiGenerator
- Form2Xml (tbd)
Introduction
Event pools are useful for managing large sets of events that you may typically encounter when working with an MVC model. A rich model often often contains many properties and undergoes many different state changes. Declaring a unique event (and sometimes a delegate with a subclassed EventArgs
parameter) is time consuming and adds to the maintenance of the model.
The drawback to this approach is that subclassed EventArgs
, while supported, must be cast to the correct subclass in the event handler. This moves the detection of incorrectly constructed event handlers from compile time to run time.
The Problem
Take a simple model, such as a four register stack used by a simple RPN calculator:
protected string[] registerStack={"0.00", "0.00", "0.00", "0.00"};
What we'd like to do is to provide an event that is fired whenever one of the register values changes so that any associated views can update the user interface, ex:
Let's say that the model represents the events with four properties:
Typically, we would construct a delegate for the event signature:
public delegate void RegisterChangeDelegate(object sender, EventArgs args);
and our four events:
public event RegisterChangeDelegate RegisterXChangeEvent;
public event RegisterChangeDelegate RegisterYChangeEvent;
public event RegisterChangeDelegate RegisterR3ChangeEvent;
public event RegisterChangeDelegate RegisterR4ChangeEvent;
and invoke our events in the property setters. For example, the X register setter:
public string X
{
get {return registerStack[0];}
set
{
registerStack[0]=value;
if (RegisterXChangeEvent != null)
{
RegisterXChangeEvent(this, EventArgs.Empty);
}
}
}
The view informs the model of it's interest in change event using:
model.RegisterXChangeEvent+=new EventHandler(RegisterXChangeEventHandler);
This will allow our views of the model to wire up event handlers that are invoked when the model changes, and all is happy with world. There are three problems:
- We've created event definitions that are a maintenance liability (albeit a small one in this example).
- Unless the programmer is very good, he/she will forget to instrument the event, as I have done here.
- We have to remember to always check if the event handler has actually been assigned.
The last problem can be worked around with some defensive event publishing, courtesy of Juval Lowy's and Eric Gunnerson's C# Best Practices Power Point presentation, downloadable here:
public class EventsHelper
{
public static void Fire(Delegate del,params object[] args)
{
if(del == null)
{
return;
}
Delegate[] delegates = del.GetInvocationList();
foreach(Delegate sink in delegates)
{
try
{
sink.DynamicInvoke(args);
}
catch{}
}
}
}
This eliminates the "if" block and ensures that, even if the event isn't assigned, the setter will still work. For example, this is how the new "X" setter would appear:
public string X
{
get {return registerStack[0];}
set
{
registerStack[0]=value;
EventsHelper.Fire(RegisterXChangeEvent);
}
}
Much cleaner, but it still doesn't address the other two problems--lots of events and lack of instrumentation.
The Solution
The solution is an event pool. The event pool simplifies the management of many different delegates and their associated events and event handlers. Events are abstracted by associating the event handler with a tag. Events are invoked by referring to the tag (rather than the event delegate). The event pool also lends itself well to instrumentation so that the programmer doesn't have to remember to do this.
Given the above example, when we use an event pool, the following code is eliminated:
public delegate void RegisterChangeDelegate(object sender, EventArgs args);
public event RegisterChangeDelegate RegisterXChangeEvent;
public event RegisterChangeDelegate RegisterYChangeEvent;
public event RegisterChangeDelegate RegisterR3ChangeEvent;
public event RegisterChangeDelegate RegisterR4ChangeEvent;
The model fires any assigned events via the tag mechanism. For example:
public string X
{
get {return registerStack[0];}
set
{
registerStack[0]=value;
eventPool.FireEvent("X");
}
}
The view expresses its interest in the model's event in a slightly different manner:
model.EventPool.Subscribe("X",new EventHandler(RegisterXChangeEventHandler));
Loose Coupling
The danger, of course, is that the events to which you can subscribe are now very loosely associated with the events that published. How do you know if you are subscribing to an event that is still being published?
The answer is to tell the event pool what events you are publishing, via the Publish
method:
virtual public void Publish(params object[] tags)
{
foreach(object tag in tags)
{
eventTagToDelegateMap.Add(tag, null);
}
}
The model, for example, publishes the events that it fires:
eventPool.Publish("X", "Y", "R3", "R4", "EntryStateChanged");
Now, when the view subscribes to the model's events, the event pool will verify that the event has been published and emit a warning if not (or you can change the code and have it throw an exception). The Subscribe
method is overloaded so that a subscriber can optionally disable this checking, in the event (hahaha) that the publisher hasn't registered the events yet or is doing some dynamic event publication.
The Demonstration
The download includes an RPN calculator as a demonstration for the EventPool
class. This demo a very clean MVC model and has a lot of events flying around, which are managed by the EventPool
.
Documentation
Documentation for the EventPool
can be found on my website. This documentation will be extended to include the other technologies in the MVC model as the additional articles in this series are published.
The only interesting thing about the EventPool
is the Subscribe
method, which builds a multicast delegate:
virtual public void Subscribe(
object tag,
EventHandler eventHandler,
bool loose)
{
if (!loose)
{
if (!eventTagToDelegateMap.Contains(tag))
{
Trace.WriteLine("Event Pool does not contain an entry for: "+
tag.ToString());
}
}
MulticastDelegate newDelegate=new MulticastDelegate(eventHandler);
if (!eventTagToDelegateMap.Contains(tag))
{
eventTagToDelegateMap.Add(tag, newDelegate);
}
else
{
MulticastDelegate dlgt=(MulticastDelegate)eventTagToDelegateMap[tag];
if (dlgt != null)
{
dlgt=(MulticastDelegate)Delegate.Combine(dlgt, newDelegate);
eventTagToDelegateMap[tag]=dlgt;
}
else
{
eventTagToDelegateMap[tag]=newDelegate;
}
}
}
Conclusion
I personally find this approach more maintainable and easier to use when working with MVC models that publish many events.