3 - Programming With Events
3 - Programming With Events
Contents
What Exactly is an Event?
Programming with Events
Raising an Event
Creating and Registering an Event Handler
Conclusion
This month's Basic Instincts column builds upon my last two columns in which I talked about concepts and
programming techniques associated with delegates. I will assume you have read the last two installments of
this column and that you understand the role that delegates play within the Microsoft® .NET Framework. If
you haven't read the last two columns, see Implementing Callback Notifications Using Delegates and
Implementing Callbacks with a Multicast Delegate. You should also know how to design and write a simple
application that uses multicast delegates to send callback notifications to a set of handler methods.
While you have probably been programming with events for years, migrating to the .NET Framework requires
you to reexamine their inner workings because events in the .NET Framework are layered on top of delegates.
The more you know about delegates, the more power you can harness when you program events. Your
understanding of how events work at a lower level is critical when you start to work with one of the event-
driven frameworks of the common language runtime (CLR) such as Windows® Forms or ASP.NET. My goal this
month is to give you an understanding of how events work at a lower level.
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
'*** other members omitted
End Class
In this example, the LargeWithdraw event has been defined as an instance member. In this design, a
BankAccount object will act as the event source. If you want a class instead of an object to act as an event
source, you should define events as shared members using the Shared keyword.
When you program with events, it's important to acknowledge that the compiler is doing a good deal of extra
work for you behind the scenes. For example, what do you think the compiler does when you compile the
definition of the BankAccount class that I just showed you into an assembly? Figure 2 shows what the
resulting class definition would look like when inspected with ILDasm.exe, the intermediate language
disassembler. This view provides a revealing look at how much the Visual Basic .NET compiler is doing behind
the scenes to assist you.
There's a good reason for this restriction. It's far more difficult to work with return values in a case involving a
multicast delegate that's bound to several handler methods. A call to Invoke on a multicast delegate returns
the same value as the last handler method in the invocation list. However, capturing the return value of
handler methods that appear earlier in the list isn't so straightforward. Eliminating the need to capture
multiple return values simply makes events easier to use.
Raising an Event
Now let's modify the BankAccount class so that it's able to raise an event when a withdrawal is made for an
amount that exceeds a $5000 threshold. The easiest way to fire the LargeWithdraw event is to use the
RaiseEvent keyword within the implementation of a method, property, or constructor. This syntax is probably
familiar because it's similar to what you have used in earlier versions of Visual Basic. Here's an example of
firing the LargeWithdraw event from the Withdraw method:
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
RaiseEvent LargeWithdraw(Amount)
End If
'*** perform withdrawal
End Sub
End Class
While the syntax remains the same from previous versions of Visual Basic, what happens when you raise an
event is very different now. When you use the RaiseEvent keyword to fire an event, the Visual Basic .NET
compiler generates the code required to execute each and every event handler. For example, what do you
think happens when you compile the following code?
RaiseEvent LargeWithdraw(Amount)
The Visual Basic .NET compiler expands this expression to code that calls Invoke on the private field that holds
the multicast delegate object. In other words, using the RaiseEvent keyword has the very same effect as writing
the code in the following snippet:
Note that the code generated by the Visual Basic .NET compiler conducts a check to make sure that the
LargeWithdrawEvent field contains a valid reference to an object. That's because the LargeWithdrawEvent
field will have a value of Nothing until the first handler method is registered. Therefore, the generated code
doesn't attempt to call Invoke unless at least one handler method is currently registered.
You should be able to make an observation about raising an event. It usually doesn't matter whether you use
the RaiseEvent keyword or you program directly against the private LargeWithdrawEvent field that's
automatically generated by the compiler. Both approaches produce equivalent code:
In many cases, it's likely that you will prefer the syntax of the RaiseEvent keyword because it requires less
typing and it results in code that is more concise. However, in certain situations where you need more control,
it might make sense to explicitly program against the private LargeWithdrawEvent field. Let's look at an
example of when this would be the case.
Imagine a scenario in which a BankAccount object has three event handlers that have been registered to
receive notifications for the LargeWithdraw event. What would happen if you triggered the event using the
RaiseEvent keyword and the second event handler in the invocation list threw an exception? The line of code
containing the RaiseEvent statement would receive a runtime exception, but you would have no way to
determine which event handler threw it. Furthermore, there would be no way to handle the exception thrown
by the second event handler and to continue where the third event handler is executed as expected.
However, if you are willing to program in terms of the private LargeWithdrawEvent field, you can deal with an
exception thrown by an event handler in a more graceful manner. Examine the code in Figure 3. As you can
see, dropping down to a lower level and programming against the private delegate field provides an extra
degree of control. You can gracefully handle an exception and then go on to execute event handlers that
appear later in the list. This technique has obvious benefits over the RaiseEvent syntax in which an exception
thrown by an event handler prevents the execution of any event handlers that appear later in the invocation
list.
Class AccountHandlers
Shared Sub LogWithdraw(ByVal Amount As Decimal)
'*** write withdrawal info to log file
End Sub
What should you do if you'd like to employ these methods as event handlers for the LargeWithdraw event of
the BankAccount class? Let's start by creating an event handler that's bound to the handler LogWithdraw. First,
you must create the delegate object that's going to serve as an event handler:
Next, you must register this new delegate object with an event source using an AddHandler statement. When
you register an event handler using the AddHandler statement, you are required to pass two parameters, like
this:
The first parameter required by AddHandler is an expression that evaluates to an event of a class or object.
The second parameter is a reference to the delegate object that is going to be wired up as an event handler.
Here's an example of using an AddHandler statement to register an event handler with the LargeWithdraw
event of a BankAccount object:
When you use the AddHandler keyword to register an event handler for the LargeWithdraw event, the Visual
Basic .NET compiler expands this code to call the registration method add_LargeWithdraw. Once the code
containing the AddHandler statement has been executed, your event handler is in place and ready for
notifications. Therefore, the LogWithdraw method will execute whenever the BankAccount object raises a
LargeWithdraw event.
In the last example, I used a longer form of syntax to illustrate exactly what happens when you create and
register an event handler. However, once you understand how things work, you might appreciate using a
more concise syntax to accomplish the same goal, as shown here:
Since the AddHandler statement expects a reference to a delegate object as the second parameter, you can
use the shorthand syntax of the AddressOf operator followed by the name of the target handler method.
When it sees this, the Visual Basic .NET compiler then generates the extra code to create the delegate object
that is going to serve as the event handler.
The AddHandler statement of the Visual Basic .NET language is complemented by the RemoveHandler
statement. RemoveHandler requires the same two parameters as AddHandler, yet it has the opposite effect. It
removes the target handler method from the list of registered handlers by calling the remove_LargeWithdraw
method supplied by the event source:
Now you have seen all the steps required to implement a callback design using events. The code in Figure 4
shows a complete application in which two event handlers have been registered to receive callback
notifications from the LargeWithdraw event of a BankAccount object.
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
RaiseEvent LargeWithdraw(Amount)
End If
'*** perform withdrawal
End Sub
End Class
Class AccountHandlers
Shared Sub LogWithdraw(ByVal Amount As Decimal)
'*** write withdrawal info to log file
End Sub
Shared Sub GetApproval(ByVal Amount As Decimal)
'*** block until manager approval
End Sub
End Class
Module MyApp
Sub Main()
'*** create bank account object
Dim account1 As New BankAccount()
'*** register event handlers
AddHandler account1.LargeWithdraw, _
AddressOf AccountHandlers.LogWithdraw
AddHandler account1.LargeWithdraw, _
AddressOf AccountHandlers.GetApproval
'*** do something that triggers callback
account1.Withdraw(5001)
End Sub
End Module
Conclusion
While the motivation for using events and some of the syntax remains unchanged from previous versions of
Visual Basic, you have to admit that things are rather different now. As you can see, you have far more control
over how you respond to events than you've ever had before. This is especially true if you're willing to drop
down and program in terms of delegates.
In the next installment of the Basic Instincts column I plan to continue this discussion of events. I'll show you
how Visual Basic .NET supports static event binding through the familiar syntax of the WithEvents keyword,
and I'll discuss the Handles clause. In order to really master events, you must be comfortable with both
dynamic event registration and static event registration.