Lesson 4 - Observer with .NET Events

Now we're at the final lesson. We've explored the Observer pattern and how it enables an object to send notifications to a number of other objects that are unknown at compile time. We explored how delegates in .NET can help implement the Observer pattern. Now it's time for me to reveal why I've typed three lessons so far without using an event at all.

What's a .NET Event?

In .NET, an event is a special construct that allows a type to send notifications to a number of objects that are unknown at compile time. Sound familiar? That's because .NET events abstract the details of the Observer pattern so you don't have to work hard to implement them! Let's take a brief look at defining an event and relate it to what we've learned. If you want a very in-depth discussion that actually scratches the surface of some of this lesson's concepts, check out Creating Your Own Events, a guide to events I wrote in July 2008. I'm going to assume you are at least familiar with the basics of events; if you aren't too sure you should probably give it a read. Let's start discussion with the simplest possible event declaration:

Event SomethingHappened As EventHandler

The Event keyword tells the compiler that it needs to create an event. SomethingHappened is the name of the event. As EventHandler tells the compiler that the event handler for this event must match the delegate EventHandler. Hmmm, a delegate? What's it look like?

Delegate Sub EventHandler(ByVal sender As Object, ByVal e As EventArgs)

There's an important fact here: when you define a .NET event, behind the scenes .NET creates a delegate and uses it as a multicast delegate. You use the AddHandler keyword to subscribe to an event; this is the same as using Delegate.Combine on a multicast delegate. To unsubscribe from an event you use RemoveHandler; this is the same as using Delegate.Remove to remove a delegate from a multicast delegate. You use the keyword RaiseEvent to raise an event; this is the same thing as calling a multicast delegate's Invoke method. So, to make a long story short, events are syntactic sugar for implementing the Observer pattern with multicast delegates. Let's look at the example project WeatherStationWithEvents.

Implementing Observer with .NET Events

First, take a look at ISensor. Notice that since the event manages the multicast delegate for us, there's no need for the AddCallback and RemoveCallback methods. We'll use AddHandler and RemoveHandler from now on. This removes the last evidence of IObservable from our code.

What's up with EventHandler(Of UpdateEventArgs)? Well, I decided that sensors should send their data reading along with the notification. To do this, I needed a way to send the data along with the event. EventHandler only lets me send EventArgs, which doesn't have any information. So, I used the generic delegate EventHandler(Of T) instead. It looks like this:

Delegate Sub EventHandler(Of T As EventArgs)(ByVal sender As Object, ByVal e As T)

That can look pretty mean if you aren't familiar with generics. This means that I have to specify a type T when I call the method, and the type must derive from EventArgs. The delegate will accept an object parameter and a parameter of type T. This is how you send extra data with your event. First, you create a class that derives from EventArgs to hold the data, then you use that class as T.

I defined the class UpdateEventArgs to be my event data type. It adds a property Data that represents the data reading when the event was raised. Typically, event data classes should be read-only but there are a few exceptions (see Form.FormClosing for a good example.)

With that data type implemented, I had to make sure the event would force event handlers to expect it, which is why I used the generic EventHandler(Of T). The following two event declarations are completely equivalent, but which would you rather have to write?

Public Event Update As EventHandler(Of UpdateEventArgs)

' -or-

Public Delegate Sub UpdateEventHandler(ByVal sender As Object, ByVal e As UpdateEventArgs)
Public Event Update As UpdateEventHandler

That covers the event. TemperatureSensor should be largely familiar by now. The only changes in this project are the declaration of the event, the lack of delegate-tracking data structures, and the use of RaiseEvent. Likewise, the only difference in MainForm is the use of AddHandler instead of AddCallback. The application is functionally equivalent to all of the others, but took less effort to implement.

Final Notes

So, now you know a lot about how events are implemented behind the scenes. You might have learned a new trick (using delegates to let users define behavior.) You should have a fairly good understanding of how events and delegates are related, and why you have to specify a delegate when you declare an event. Is this all you need to know about events?

I'm afraid not. If you haven't read my article Creating Your Own Events, you should probably at least skim it to make sure you understand the best practices for defining events on types. Once you're really familiar with events, you might want to take a look at my Advanced Events Tutorial. It starts with the implementation details I covered in this series of articles, but then discusses how .NET's default implementation can waste memory and cause application deadlock in types that define many events. There's a solution to these problems, but seeing as I've already written that article I see no need to add a Lesson 5 to this series. You can probably never look at that article and do just fine, but in particular if you are writing controls with many properties I urge you to have a look.

Side Note

Just as with multicast delegates, you should be aware that if any event handler throws an exception then any event handlers that come after it will not be called. If you take a look at the advanced events tutorial I linked above, you'll find a way to treat the event just as if it were a multicast delegate. This still has the same pitfalls, and I still suggest you follow the guideline that event handlers should never throw exceptions.