0% found this document useful (0 votes)
19 views

3 - Programming With Events

Uploaded by

serleb44
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
19 views

3 - Programming With Events

Uploaded by

serleb44
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Basic Instincts

Programming with Events Using .NET


Ted Pattison

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.

What Exactly is an Event?


An event is just a formalized software pattern in which a notification source makes callbacks to one or more
handler methods. Events are therefore similar to interfaces and delegates because they provide a means to
design applications that use callback methods. However, events add a valuable degree of productivity because
they are easier to use than interfaces or delegates. Events allow the compiler and the Visual Studio® .NET IDE
to do much of the work for you behind the scenes.
A design that involves events is based on an event source and one or more event handlers. An event source
can be either a class or an object. An event handler is a delegate object that's bound to a handler method.
Figure 1 shows a high-level view of an event source wired up to its handler methods.

Figure 1 Event Source and Handlers


Every event is defined in terms of a particular delegate type. For each event defined by an event source, there
is a private field that is based on the event's underlying delegate type. This field is used to track a multicast
delegate object. An event source also provides a public registration method that allows you to register as
many event handlers as you'd like.
When you create an event handler (a delegate object) and register it with an event source, the event source
simply appends the new event handler to the end of the list. An event source can then use the private field to
call Invoke on the multicast delegate which, in turn, will execute all the registered event handlers.
What's really nice about events is that much of the work to set them up is already done for you. As you will
soon see, the Visual Basic® .NET compiler assists you by automatically adding a private delegate field and a
public registration method whenever you define an event. You will also see that Visual Studio .NET provides
even more assistance with a code generator that can automatically emit the skeleton definitions for your
handler methods.

Programming with Events


Because events in .NET are built on top of delegates, their underlying plumbing details are very different from
the way things used to work in previous versions of Visual Basic. However, the language designers of Visual
Basic .NET did a good job in keeping the syntax for programming events consistent with earlier versions of
Visual Basic. In many cases, programming events involves the same old familiar syntax you're used to using.
For example, you will use keywords such as Event, RaiseEvent, and WithEvents, and they will behave almost
identically to the way they have behaved in previous versions of Visual Basic.
Let's start by creating a simple callback design based on an event. First, I need to define an event within a class
definition by using the Event keyword. Every event must be defined in terms of a specific delegate type. Here's
an example of defining both a custom delegate type and a class that uses it to define an event:

Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)

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.

Figure 2 Class Definition in ILDasm


When you define an event, the compiler generates four members inside the class definition. The first member
is a private field based on the delegate type. This field is used to track a reference to a delegate object. The
compiler generates the name for this private field by taking the name of the event itself and adding the suffix
"Event". This means that creating an event named LargeWithdraw results in the creation of a private field
named LargeWithdrawEvent.
The compiler also generates two methods to assist with the registration and unregistration of delegate objects
that are to serve as event handlers. These two methods are named using a standard naming convention. The
method for registering an event handler is named after the event along with a prefix of "add_". The method for
unregistering an event handler is named after the event along with a prefix of "remove_". Therefore, the two
methods created for the LargeWithdraw event are named add_LargeWithdraw and remove_LargeWithdraw.
The Visual Basic .NET compiler generates an implementation for add_LargeWithdraw that accepts a delegate
object as a parameter and adds it to the list of handlers by calling the Combine method of the Delegate class.
The compiler generates an implementation for remove_LargeWithdraw that removes a handler method from
the list by calling the Remove method in the Delegate class.
The fourth and final member that is added to the class definition is one that represents the event itself. You
should be able to locate the event member named LargeWithdraw in Figure 2. It is the member with an
upside-down triangle next to it. However, you should note that this event member isn't really a physical
member like the other three. Instead, it's a metadata-only member.
This metadata-only event member is valuable because it can inform compilers and other development tools
that the class supports the standard pattern for event registration in the .NET Framework. The event member
also contains the names of the registration method and the unregistration method. This allows compilers for
managed languages such as Visual Basic .NET and C# to discover the name of the registration method at
compile time.
Visual Studio .NET is another good example of a development tool that looks for this metadata-only event
member. When Visual Studio .NET sees that a class definition contains events, it automatically generates the
skeleton definitions for handler methods as well as the code to register them as event handlers.
Before I move on to a discussion of raising events, I'd like to cover a restriction involved with creating a
delegate type that's to be used for defining events. A delegate type used to define an event cannot have a
return value. You must define the delegate type using the Sub keyword instead of the Function keyword, as
shown here:

'*** can be used for events


Delegate Sub BaggageHandler()
Delegate Sub MailHandler(ItemID As Integer)

'*** cannot be used for events


Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String

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:

If (Not LargeWithdrawEvent Is Nothing) Then


LargeWithdrawEvent.Invoke(Amount)
End If

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:

'*** this code


RaiseEvent LargeWithdraw(Amount)

'*** is the same as this code


If (Not LargeWithdrawEvent Is Nothing) Then
LargeWithdrawEvent.Invoke(Amount)
End If

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.

Figure 3 Using the Private Delegate Field

Sub Withdraw(ByVal Amount As Decimal)


'*** send notifications if required
If (Amount > 5000) AndAslo (Not LargeWithdrawEvent Is Nothing) Then
Dim handler As LargeWithdrawHandler
For Each handler In LargeWithdrawEvent.GetInvocationList()
Try
handler.Invoke(Amount)
Catch ex As Exception
'*** deal with exceptions as they occur
End Try
Next
End If
'*** perform withdrawal
End Sub

Creating and Registering an Event Handler


Now that you've seen how to define and raise an event, it's time to discuss how to create an event handler and
register it with a given source. There are two different ways to accomplish this in Visual Basic .NET. The first
technique is known as dynamic event binding and involves the use of the AddHandler keyword. The second
technique is called static event binding and involves the use of the familiar Visual Basic keyword WithEvents. I
plan to cover static event binding in a future column. So for now, let's examine how dynamic event binding
works.
Remember that an event handler is a delegate object. Therefore, you create one by instantiating a delegate
object from the delegate type on which the event is based. When you create this delegate object, you must
bind it to a target handler method that you want to serve as an event handler.
Once you have created an event handler, you must register it with a specific event by calling the special
registration method on the event source. Recall that the registration method for the LargeWithdraw event is
named add_LargeWithdraw. When you call the add_LargeWithdraw method and pass a delegate object as a
parameter, the event source adds the delegate object to the list of event handlers that are to receive event
notifications.
What's confusing about event registration is that you never directly call a registration method such as
add_LargeWithdraw. In fact, the Visual Basic .NET compiler will raise a compile-time error if you try to access
an event registration method by name. Instead, you use an alternate syntax involving the AddHandler
statement. When you use the AddHandler statement, the Visual Basic .NET compiler generates the code to call
the event registration method for you.
Let's look at an example of wiring up a few event handlers using dynamic event registration. Imagine you have
written the following set of shared methods in the AccountHandlers 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

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:

Dim handler1 As LargeWithdrawHandler


handler1 = AddressOf AccountHandlers.LogWithdraw

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:

AddHandler <event>, <delegate object>

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:

'*** create bank account object


Dim account1 As New BankAccount()

'*** create and register event handler


Dim handler1 As LargeWithdrawHandler
handler1 = AddressOf AccountHandlers.LogWithdraw
AddHandler account1.LargeWithdraw, handler1

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:

'*** create bank account object


Dim account1 As New BankAccount()

'*** register event handlers


AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.GetApproval

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:

Dim account1 As New BankAccount()

'*** register event handler


AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw

'*** unregister event handler


RemoveHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw

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.

Figure 4 An Event-based Design for Callback Notifications

Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)

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.

You might also like