Basic Instincts

Static Event Binding Using WithEvents

Ted Pattison

Contents

Using the WithEvents Keyword
Static Event-binding Magic
Using WithEvents Fields in Inherited Classes
Summing it Up

This month's Basic Instincts column builds upon my last three columns in which I introduced and explained the fundamental concepts and syntax associated with delegates and events. Last month I showed you how to design and write a simple class that defines and raises events. You also saw how to dynamically bind an event handler to an event using the AddHandler keyword. This month I am going to discuss static event binding, an alternative technique for registering an event handler.

Using the WithEvents Keyword

Programmers with previous experience in Visual Basic® may find that static event binding is fairly easy because of its familiar syntax using the WithEvents keyword. Let me revisit the example of the BankAccount class that I used in last month's column. This class defines an event as a public member and raises the event from within a method definition, as shown here:

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

Objects created from the BankAccount class expose the LargeWithdraw event. You can see that a BankAccount object contains the logic to raise the LargeWithdraw event whenever a withdrawal is made for a value greater than $5,000.

Imagine that you want to create a new class named AccountAuditor1 to act as an event listener using static event binding. You can define a class as an event listener by adding one or more fields defined with the WithEvents keyword:

Class AccountAuditor1
  Private WithEvents account As BankAccount
  '*** other members omitted
End Class

You should note that a field defined using the WithEvents keyword must be based on a class that defines one or more instance events; otherwise it will cause a compile-time error. In this example, the field that is named account can be defined using the WithEvents keyword because the BankAccount class defines an instance event named LargeWithdraw.

The point of defining the account field with the WithEvents keyword is to allow methods defined within the AccountAuditor1 class to be registered as event handlers for events raised by a BankAccount object. Now that I've defined a WithEvents field, I'll create a method that will act as an event handler for the LargeWithdraw event.

When you define a method that's going to be an event handler, it must have the appropriate calling signature. For example, a method that's going to act as an event handler for the LargeWithdraw event must have a calling signature that matches LargeWithdrawHandler. Look at the following class definition:

Class AccountAuditor1
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler method implementation
  End Sub
End Class

In this example, I've added a new method named Handler1. As you can see, this handler method has a calling signature that matches the delegate type LargeWithdrawHandler. Also notice that the definition of the Handler1 method contains a Handles clause:

Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw

You should see that a Handles clause is based on a WithEvents field and an event. The Handles clause is important because it provides a hint to the Visual Basic .NET compiler. More specifically, the presence of a Handles clause triggers the Visual Basic .NET compiler to generate extra code that will create and register an event handler. In this particular case, the compiler generates code to create an event handler of type LargeWithdrawHandler that's bound to the Handler1 method. The compiler will then generate code to register it with the BankAccount object that has been assigned to the account field.

Note that the Handles keyword is new in Visual Basic .NET. Earlier versions of Visual Basic required you to use a specific naming convention for event handler methods. For example, Visual Basic 6.0 would require the handler method in the previous example to be defined using the name account_LargeWithdraw. However, in Visual Basic .NET the name of the handler method doesn't matter; what does matter is that the handler method is defined with a Handles clause.

The real magic is performed by the Visual Basic .NET compiler when you assign an event source object to a WithEvents field. I'm going to defer the discussion of how the compiler performs this magic until later. For now, I'm going to make one more addition to AccountAuditor1 so the class can be used as an event listener.

A listener object needs an event source object. For example, it doesn't make sense to create an AccountAuditor1 object unless you have a BankAccount object that's going to act as an event source. Therefore, I'm going to add a constructor so that every AccountAuditor1 object is initialized with a BankAccount object as its event source, like this:

Class AccountAuditor1
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler method implementation
  End Sub
  Sub New(ByVal SourceAccount As BankAccount)
    Me.account = SourceAccount '*** triggers binding of event handler
  End Sub
End Class

Note that this constructor assigns the SourceAccount parameter to the WithEvents field named account. This is the line of code where the automatic binding of the event handler takes place. Now the AccountAuditor1 class can be used to create a listener object. For example, imagine you wrote the following application:

'*** create event source
Dim account1 As New BankAccount()

'*** create listener object and bind event handler
Dim listener1 As New AccountAuditor1(account1)

'*** do something that triggers event
account1.Withdraw(5001)

When the constructor of the AccountAuditor1 class executes, the Visual Basic .NET compiler has generated the code to create an event handler object that's bound to the Handler1 method. The compiler also generates code to register the event handler with the BankAccount object by calling the registration method add_LargeWithdraw. Therefore, the Handler1 method will execute whenever the Withdraw method raises the LargeWithdraw event.

You have just seen the fundamentals of how to create a listener object using static event binding. Figure 1 shows a complete application that uses static event binding to implement a callback design that is similar to the other techniques you have seen over the last three Basic Instincts columns. You should note that static and dynamic event binding are two different approaches that can often be used to achieve similar goals.

Figure 1 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 AccountAuditor1
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler method implementation
    MsgBox("Handler1")
  End Sub
  Sub New(ByVal SourceAccount As BankAccount)
    Me.account = SourceAccount ''*** triggers binding of event handler
  End Sub
End Class

Class AccountAuditor2
  Private WithEvents account As BankAccount
  Sub Handler2(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler method implementation
    MsgBox("Handler2")
  End Sub
  Sub New(ByVal SourceAccount As BankAccount)
    Me.account = SourceAccount '*** triggers binding of event handler
  End Sub
End Class

Module MyApp
  Sub Main()
    '*** create bank account object
    Dim account1 As New BankAccount()
    '*** register event handlers
    Dim listener1 As New AccountAuditor1(account1)
    Dim listener2 As New AccountAuditor2(account1)
    '*** do something that triggers callback
    account1.Withdraw(5001)
  End Sub
End Module

Now that you have seen both static and dynamic event binding, you might be wondering which one you should use. Ideally, you should learn how to use both. The Visual Studio® .NET IDE uses static event binding when you ask it to generate skeleton definitions for your event handler methods. Therefore, your understanding of static event binding will be important when you are working with an event-driven application framework such as Windows® Forms or ASP.NET Web Forms.

However, it's also important that you know how to use dynamic event binding, which I showed last month, because of its flexibility. For example, static event binding can only be used in cases where the event source is an object. It cannot be used when the event source is a class. In other words, static event binding can be used with instance events but cannot be used with shared events. Dynamic event binding, on the other hand, will allow you to bind to either shared events or instance events.

There are circumstances when the Visual Studio .NET IDE is not able to generate all the event handling code you need for an application and you'll have to write the code to create and register event handlers by hand. In cases like these, you will find that dynamic event binding is more straightforward and easier to use. After all, it only takes a single AddHandler statement to create an event handler from a method and bind it to an event. The only requirement is that the handler method have the calling signature that's required for the event in question.

One more interesting thing to note is that static event binding is a special programming feature that is unique to Visual Basic .NET. Other managed languages, such as C#, support dynamic event binding but do not support the equivalent of static event binding. If you are going to switch between managed languages or you need to port code from C# to Visual Basic .NET, you should consider relying on dynamic event binding.

Static Event-binding Magic

Now I'd like to discuss the low-level details of how the Visual Basic .NET compiler supports static event binding. You can use static event binding without understanding all the details I am about to explain, but I'll provide the details about what the compiler is doing to satisfy any curiosity you may have about how things work behind the scenes.

Let's start by looking at what happens when you compile a class definition with a WithEvents field. Assume that you have written the following class definition:

Class AccountAuditor
  Private WithEvents account As BankAccount
  '*** other members omitted
End Class

What happens when you compile this class? The Visual Basic .NET compiler generates the class definition shown in ILDasm (see Figure 2). As you can see, the class definition generated by the compiler is very different from the one you wrote. After it has been compiled, there is no longer a field named account. Instead, there is a private field named _account and a read/write property named account. The compiler has also generated special implementations for the account property's Set and Get methods. All the magic is in the Set method implementation.

Figure 2 The Compiler's Class Definition

Figure 2** The Compiler's Class Definition **

So what really happens when you assign an event source object to a WithEvents field? For example, what happens when you assign a BankAccount object to the account field in the constructor of the AccountAuditor class?

Class AccountAuditor
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    *** handler method implementation
  End Sub
  Sub New(ByVal source As BankAccount)
    Me.account = source '*** triggers call to add_LargeWithdraw
  End Sub
End Class

It's important to see that you are really not assigning the event source object to a field. Instead, you are assigning the event source object to a property which causes the Set method named set_account to execute. The implementation for set_account is generated to create and register an event handler for each method defined with the Handles clause. Figure 3 shows how the code you write gets translated by the compiler.

Figure 3 Static Event Binding

'*******************************************
'**** the code you write looks like this ***
'*******************************************
Class AccountAuditor
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    *** handler method implementation
  End Sub
  Sub New(ByVal SourceAccount As BankAccount)
    Me.account = SourceAccount
  End Sub
End Class

'***************************************************
'*** the code that gets compiled looks like this ***
'***************************************************
Class AccountAuditor
  Private _account As BankAccount
  Private Property account() As BankAccount
    Get
      Return _account
    End Get
    '*** magic code starts here ***************************************
    Set(ByVal Value As BankAccount)
      If (Not _account Is Nothing) Then
        _account.remove_LargeWithdraw(AddressOf Me.Handler1)
      End If
      If (Not Value Is Nothing) Then
        _account = Value
        _account.add_LargeWithdraw(AddressOf Me.Handler1)
      End If
    End Set
    '*** magic code ends here *****************************************
  End Property
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler implementation
  End Sub
  Sub New(ByVal source As BankAccount)
    Me.account = source '*** triggers call Set method
  End Sub
End Class

Examine the code inside the Set method of the AccountAuditor class shown in Figure 3. This code checks to see if there is an existing event source object being referenced by the _account field. If there is, the code removes it by calling the unregistration method remove_LargeWithdraw. Next, the Set method checks the Value parameter to see whether the assigned value is a valid reference to an event source object or a value of Nothing. If the Value parameter holds a valid reference to an event source object, the Set method creates an event handler bound to the Handler1 method and registers it by calling add_LargeWithdraw.

So, there's really no magic involved. When you assign a valid object reference to a WithEvents field, the compiler generates code to create delegate objects and bind them to each method that has been defined with a Handles clause. The compiler also generates the code to register these delegate objects by calling the event registration methods defined by the notification source.

Note that you are not required to bind a listener object to an event source during initialization. You can assign a value to a WithEvents field any time during the lifetime of a listener object. Consider the class definition in Figure 4. With this new design, an AccountAuditor object will not be bound to an event source after it has been initialized. However, you can call StartListening at any time to bind a listener object to an event source object. Note however that a call to StartListening will disconnect the listener object from an existing event source so that it can connect to the new one. A call to StopListening will disconnect the listener object from an existing event source and, in addition, leave it in an unbound state.

Figure 4 Binding Listener Object Later

Class AccountAuditor
  Private WithEvents account As BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
    '*** handler method implementation
  End Sub
  Sub StartListening(ByVal source As BankAccount)
    Me.account = source '*** triggers call to add_LargeWithdraw
  End Sub
  Sub StopListening()
    Me.account = Nothing '*** triggers call to remove_LargeWithdraw
  End Sub
End Class

At this point it's fair to say that the Visual Basic .NET compiler is doing a good deal of work for you behind the scenes when you use static event binding. Furthermore, this example only involved a single handler method. In practice, you will often have many handler methods associated with a single WithEvents field.

Using WithEvents Fields in Inherited Classes

In the Microsoft® .NET Framework, it is common for a base class to raise events that are designed to be handled by derived classes. The idea is that a derived class author can customize behavior by adding event handlers to respond to events raised by a base class. This design technique provides an alternative to using overridable methods. It's also an important technique for you to understand because it is used extensively by application frameworks such as Windows Forms and ASP.NET.

Let's look at a simple example to see how this works. You have already seen that a Handles clause can be written in terms of a WithEvents field. You can also write a Handles clause using the MyBase keyword to handle an event defined within a base class, as shown in the following code:

Class CheckingAccount : Inherits BankAccount
  Sub Handler1(ByVal amount As Decimal) Handles MyBase.LargeWithdraw
    '*** handler method implementation
  End Sub
End Class

Once again, the Visual Basic .NET compiler generates the code that's needed to create an event handler and register it. However, the code for a base class event is generated in a slightly different fashion than the code for an event associated with a WithEvents field. In this example, the Visual Basic .NET compiler adds the code for binding to the LargeWithdraw method into the constructor of the CheckingAccount class.

There is one other thing you should keep in mind when working with a base class that exposes events. While a derived class can handle a base class event, it cannot raise a base class event. Take a look at the following example:

Class CheckingAccount : Inherits BankAccount
  Sub SomeOtherMethod()
    RaiseEvent LargeWithdraw(5001) '*** compile error
  End Sub
End Class

Since the implementation for an event involves a private field, the event can only be raised from within the class in which it is defined. Therefore, you will experience a compile-time error if you try to raise a base class event using a RaiseEvent statement or if you attempt to access the private field by name.

Summing it Up

I've shown you the two different techniques for registering event handlers for events: dynamic event binding and static event binding. To improve your knowledge of Visual Basic .NET, you must become comfortable with both techniques.

In the next installment of this column I will continue my discussion of events by exploring some of the most common delegate types and events in the Framework Class Libraries. In particular, I am going to focus on a delegate type named System.EventHandler and show how it provides a foundation for most of the events you will use in both Windows Forms and ASP.NET.

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