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

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

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.

Send your questions and comments for Ted to  instinct@microsoft.com.

Ted Pattisonis an instructor and researcher at DevelopMentor (https://www.develop.com), where he co-manages the Visual Basic curriculum. He is the author of Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000).