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