Saturday, March 01, 2008

Event Hub

My previous post was about the CAB to Prism migration path in general. This post is about a specific service that can help you on that journey: the event hub.

First up, let me give a shout out to both Jeremy Miller and Glenn Block for their posts here and here respectively on the subject of pub/sub event aggregators. My work is very much based on theirs. And a special thanks to Jeremy for taking some time out with me to discuss my implementation.

So what is this event hub beast I speak of? It's a service that can be used instead of CAB's event broker to provide a loosely-coupled, many-to-many pub/sub event system. Using such a service instead of CAB's EB will reduce your coupling to CAB and reduce the friction of migrating beyond CAB. Moreover, the event hub provides some features and functionality on top of CAB's event broker that will make your code more readable and yield better performance.

Let's take this one step at a time. Suppose we have a simple Customer class in our domain:

public class Customer
{
    public string Name { get; set; }

    public Customer(string name)
    {
        Name = name;
    }
}

Now suppose we want an event to be published whenever a Customer is selected. We start by defining a type that holds the event data. In this case, we just need to store the Customer that was selected:

public class CustomerSelected
{
    public Customer Customer { get; private set; }

    public CustomerSelected(Customer customer)
    {
        Customer = customer;
    }
}

The CustomerSelected class is known as the event subject, or simply subject. Notice that there is no requirement that this class be an EventArgs subclass, or that it be serializable, or even that it be a reference type. It can be any CLR type.

Now that we have our CustomerSelected class, we can publish an event very easily using the event hub service:

eventHub.Publish(new CustomerSelected(SelectedCustomer));

It's as simple as that. The event hub service will take care of finding the subscribers for this particular event subject, and will notify them by calling their Receive() method. Here is what a subscriber looks like for the CustomerSelected subject:

public partial class CustomerDetails : ISubscriber<CustomerSelected>
{
    void ISubscriber<CustomerSelected>.Receive(CustomerSelected subject)
    {
        //handle the event here        //subject.Customer gives us the Customer that was selected
    }
}

Now that we've seen some basic usage, here is a type diagram for the event hub interfaces:

image

A subscriber simply needs to implement a Receive() method that accepts the event subject as a parameter, and the event hub service provides methods for publishing events, subscribing to events, and unsubscribing from events. I'll talk a little more about these methods below.

The obvious advantage of the event hub over CAB's EB is the strong typing. We're no longer associating publishers and subscribers via strings but instead via a generic type T, and any discrepancies between publisher and subscriber signatures will be caught at compile-time, not runtime.

Another difference is the way subscribers are registered with the event hub. In CAB, the event publishers and subscribers are typically discovered via reflection by searching for attributed events and methods. With the event hub, I decided I'd prefer explicit subscription and un-subscription (although I also did a quick POC to do auto-discovery and subscription). I think explicit subscription and un-subscription in the code makes it very easy to determine what events a component expects and handles:

public CustomerDetails()
{
    InitializeComponent();

    eventHub.Subscribe<CustomerSelected>(this);

    Unloaded += (s, e) => { eventHub.Unsubscribe<CustomerSelected>(this); };
}

It also provides a better debugging experience, and allows you to unit test the wiring code. And of course, since we're no longer reflectively discovering publishers and subscribers, we get a nice performance boost to boot.

The event hub provides some more advanced features, too. Both publishers and subscribers have the option of filtering events. Publishers can provide a predicate when they publish an event, and this predicate can decide which subscribers receive the event. Similarly, subscribers can provide a predicate when they register interest in a subject type, and this predicate will be used to dynamically determine whether a specific event subject should be passed onto the subscriber. For the CustomerSelected event, a Venn diagram looks something like this:

image

The event hub starts by choosing only those subscribers that have subscribed to the CustomerSelected event. It then allows the publisher to filter out unwanted subscribers. Finally, it allows each of those subscribers to opt out of receiving the event via their own filter. We end up with those subscribers represented in blue above. These are the only subscribers that will be notified of the event. Of course, this set could change dynamically since the filter code might be using dynamic information to opt in or out of notifications. Filters are entirely optional, and the entire set of CustomerSelected subscribers would receive the event if there were no filters to apply.

Another feature provided by the event hub is automatic marshalling to a SynchronizationContext. This is particularly useful in UI development to ensure that subscribers receive event notifications on the UI thread, thus alleviating you of the burden of manual marshalling. CAB's event broker did a similar thing via a ThreadOption.UserInterface enumeration member in the subscription attribute, but the event hub allows you to pass any SynchronizationContext, so it is equally applicable in other environments, or even with your own custom SynchronizationContext implementation.

One feature my event hub does not provide is a scope mechanism. This was a conscious decision. Unless real-world usage forces me to add such a mechanism, I am going to avoid the added complexity. I suspect that most use cases can be solved by including scope information in the event subject type, and using the filtering mechanisms provided by the event hub. Time will tell.

Finally, I'd like to point out something to watch out for when using the event hub. When you have a hierarchy of subject types and you allow the compiler to resolve the generic type T in the Publish<T>() method, you may not get the result you expected. Consider:

public class SomethingHappened { }

public class SomethingElseHappened : SomethingHappened { }

SomethingHappened somethingHappened = new SomethingElseHappened();
//this will publish the SomethingHappened event,//NOT the SomethingElseHappened event!
_eventHub.Publish(somethingHappened);

Because we're relying on the compiler here to resolve the generic type T for the Publish<T>() method, it has used the static type SomethingHappened rather than the dynamic type SomethingElseHappened. This is easy enough to work around, but I'm leaning on the side of caution and always specifying generic type parameters for the event hub methods. It seems to make the code more readable too.

Well, that's about it for the event hub. I'm planning on employing this little guy in earnest within a few CAB projects in the coming months. Any feedback you have is welcome.

5 comments:

Anonymous said...

I think its great that you are providing help for porting from CAB to Prism, but your post reads as though this Event Hub design was your own when in fact it's based on Prism's Event Aggregator pattern implementation. This might not have been your intention, but it reads a bit like "look what I came up with."

Kent Boogaart said...

Um, firstly I wrote this before Prism's event aggregator came to be. Secondly, if you read the second paragraph I thank both Jeremy Miller (a fellow member of the Prism advisory board) and Glenn Block (who heads up the Prism team) for their input. How exactly is that taking credit, AC?

Anonymous said...

Great article. Don't listen to the first poster. This was very helpful. I'm working on coming up with an event system with a custom lightweight MVC framework for ASP.NET for controllers to communicate with one another. Something like this on a per-request basis should work great.

Anonymous said...

Thanks, good info here. I just read about Prism2 Event Aggregator and was searching for a similar framework for use in my webforms and mvc projects. Any suggestions? -rogdev

Kent Boogaart said...

@Anon: the prism event aggregator isn't specific to WPF/SL, so depending on your exact requirements you might be able to just rip it out and use it.