Basic Instincts
Programming Events of the Framework Class Libraries
Ted Pattison
Contents
The EventHandler Delegate
Customized Event Parameters
Parameterizing Custom Events
Wrap-up
This month's installment represents the final column in a series of three focusing on programming events. In the previous two columns, I showed you how to define and raise events (see Basic Instincts: Programming with Events Using .NET and Basic Instincts: Static Event Binding Using WithEvents). I also explained how to wire up event handlers using both dynamic and static event binding. This month I am going to conclude my coverage of events by showing some practical examples of handling some of the more commonly used events in the Microsoft® .NET Framework.
The EventHandler Delegate
When you build applications using Windows® Forms or ASP.NET, you'll observe that a significant percentage of the events you encounter are defined in terms of a generic delegate type named EventHandler. The EventHandler type exists in the System namespace and has the following definition:
Delegate Sub EventHandler(sender As Object, e As EventArgs)
The delegate type EventHandler defines two parameters in its calling signature. The first parameter, named sender, is based on the generic Object type. The sender parameter is used to pass a reference that points to the event source object. For example, a Button object acting as an event source will pass a reference to itself when it raises an event based on the EventHandler delegate type.
The second parameter defined by EventHandler is named e and is an object of type EventArgs. In many cases, an event source passes a parameter value equal to EventArgs.Empty, indicating there is no additional parameter information. If an event source wants to pass extra parameterized information in the e parameter, it should pass an object created from a class that derives from the EventArgs class.
Figure 1 shows an example involving two event handlers in a Windows Forms application that are wired up using static event binding. Both the Load event of the Form class and the Click event of the Button class are defined in terms of the delegate type EventHandler.
Figure 1 Using Static Event Binding
Imports System
Imports System.Windows.Forms
Public Class MyApp : Inherits Form
'*** static event handler for base class event
Private Sub Form1_Load(ByVal sender As Object, _
ByVal e As EventArgs)_
Handles MyBase.Load
'*** event handler code
End Sub
'*** button defined as WithEvents field
Friend WithEvents cmdDoTask As Button
'*** static event handler for button
Private Sub cmdDoTask_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles cmdDoTask.Click
'*** event handler code
End Sub
End Class
You should also note that the names and format of the two event handler methods in Figure 1 are consistent with what is generated for you by the Visual Studio® .NET IDE. For example, if you double-click on a form or command button while you are in design view, Visual Studio .NET will automatically create the skeleton of event handler methods that looks like this. All you are required to do is fill in the implementation for these methods to give your event handlers the desired behavior.
You might have noticed that the Visual Studio .NET IDE generates handler methods using the naming scheme that was required by Visual Basic® 6.0. However, you should remember that the names of handler methods don't really matter with static event binding in Visual Basic .NET. It's the Handles clause that matters. You should feel free to rename handler methods to anything you want.
It's possible to rewrite these two event handlers so that they are wired up using dynamic event binding instead of static event binding. For example, the Form-derived class in Figure 2 provides the exact same event binding behavior as the Form-derived class in Figure 1. The only difference is that the latter example uses dynamic event binding and does not require either the WithEvents keyword or the Handles keyword. In many cases, you will write implementations for handler methods based on the EventHandler delegate type without referencing either the sender parameter or the e parameters. For example, these parameter values are of no real use when you are writing a handler for the Load event of a Form-derived class. The sender doesn't provide any value because it simply passes your Me reference. The e parameter passes EventArgs.Empty:
Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'*** these tests are always true
Dim test1 As Boolean = sender Is Me
Dim test2 As Boolean = e Is EventArgs.Empty
End Sub
Figure 2 Using Dynamic Event Binding
Imports System
Imports System.Windows.Forms
Public Class MyApp : Inherits Form
Friend cmdDoTask As Button
Sub New()
'*** other initialization code omitted
AddHandler MyBase.Load, AddressOf Me.Handler1
AddHandler cmdDoTask.Click, AddressOf Me.Handler2
End Sub
Private Sub Handler1(ByVal sender As Object, ByVal e As EventArgs)
'*** event handler code
End Sub
Private Sub Handler2(ByVal sender As Object, _
ByVal e As System.EventArgs)
'*** event handler code
End Sub
End Class
You might wonder why the calling signature of the Load event isn't more customized for its needs. After all, it would be less confusing if the Load event didn't include any parameters at all. It's fairly easy to find other examples of events based on the EventHandler delegate type in which the sender parameter or the e parameter don't pass anything of value.
Ask yourself the following questions. Why do you think so many events have been modeled in terms of the EventHandler when this delegate type has such a generic calling signature? Why didn't the designers of the .NET Framework model each event in terms of a custom delegate with a calling signature that was fine-tuned for its needs? As it turns out, there was a design goal in the development of the .NET Framework to restrict the number of delegates used for event handling. A little more explanation is in order.
The first motivation for minimizing the number of delegate types has to do with a more efficient utilization of memory used by an application. Loading in more types means using up more memory. If every event defined by the classes within the Windows Forms framework were based on a custom delegate, hundreds of delegate types would have to be loaded into memory every time you ran a Windows Forms application. The Windows Forms framework can provide much better memory utilization by relying on a small handful of delegate types for the hundreds of events defined within the Form class and the various control classes.
A second motivation for minimizing the number of delegate types has to do with increasing the potential to achieve polymorphism with pluggable handler methods. When you write a handler method with a calling signature that matches the EventHandler delegate, you can bind it to the majority of the events raised by a form and its controls.
Let's look at a few examples of writing generic event handlers. I'll begin with an example in which you want to respond to the TextChanged event of several textboxes on a form by changing the user's input to uppercase. There's no need to create a separate event handler for each control. Instead, you can create a single event handler and bind it to the TextChanged event of several different textboxes (see Figure 3).
Figure 3 One Event Handler for Several Controls
Public Class MyApp : Inherits Form
Friend WithEvents TextBox1 As TextBox
Friend WithEvents TextBox2 As TextBox
Friend WithEvents TextBox3 As TextBox
'*** create event handler bound to several TextChanged events
Private Sub TextChangedHandler(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles TextBox1.TextChanged, _
TextBox2.TextChanged, _
TextBox3.TextChanged
'*** convert sender to TextBox
Dim txt As TextBox = CType(sender, TextBox)
txt.Text = txt.Text.ToUpper()
End Sub
End Class
The first thing you should note about this example is that a Handles clause isn't limited to a single event. You can include as many events as you'd like by using a comma-delimited list after the Handles keyword. In this example, the TextChangedHandler method is used to create three different event handlers. Therefore, this method is going to execute whenever the user changes the text in any of the three textboxes.
When the TextChangedHandler method executes, how do you know which TextBox object is raising the event? That's what the sender parameter is for. Remember that the sender parameter is passed in terms of the generic type Object. That means you must convert it to a more specific type before you can program against it. In the previous example, the sender parameter must be converted to a TextBox in order to access its Text property.
If you have experience building form-based applications with earlier versions of Visual Basic, you might be accustomed to using control arrays. A primary advantage to using control arrays in Visual Basic 6.0 is that this feature makes it possible to create a single handler method that responds to events raised by several different controls. Visual Basic .NET does not support control arrays. However, you should not be overly alarmed because, as you've just seen, Visual Basic .NET provides an alternate technique for binding a single handler method to several different events.
The event architecture of the .NET Framework also provides you with the ability to do things that have never been possible with control arrays. For example, you can create a single handler method to respond to events raised by several different types of controls. An example of a handler method that's bound to three different events on three different control types is shown in Figure 4.
Figure 4 Handler Method Bound to Different Events
Public Class MyApp : Inherits Form
Friend WithEvents TextBox1 As TextBox
Friend WithEvents CheckBox1 As CheckBox
Friend WithEvents ListBox1 As ListBox
'*** define form-wide dirty flag
Friend DirtyFlag As Boolean
'*** set dirty flag when various events are raised
Private Sub DirtyFlagHandler(ByVal sender As Object, _
ByVal e As EventArgs) _
Handles TextBox1.TextChanged, _
CheckBox1.CheckedChanged, _
ListBox1.SelectedIndexChanged
'*** set form-wide dirty flag
DirtyFlag = True
End Sub
End Class
As you can see, the scheme for binding handler methods to events is pretty flexible. The only requirement is that a handler method and the events it's bound to are based on the same delegate type. The fact that so many events in the .NET Framework are based on the EventHandler delegate type makes it easy to write generic handler methods.
When you write a generic handler method, it's sometimes necessary to write code to perform conditional operations that are only executed when the event source is a certain type of object. For example, your handler method can inspect the sender parameter using the TypeOf operator. This allows your handler method to execute one set of operations if the event source is a Button object and another set of operations if it's a CheckBox object, like this:
Sub GenericHandler1(sender As Object, e As EventArgs)
If (TypeOf sender Is Button) Then
Dim btn As Button = CType(sender, Button)
'*** program against btn
ElseIf (TypeOf sender Is CheckBox) Then
Dim chk As CheckBox = CType(sender, CheckBox)
'*** program against chk
End If
End Sub
Customized Event Parameters
An event notification based on the EventHandler delegate doesn't typically send any meaningful information in the e parameter. The e parameter is often useless because it contains either a value of EventArgs.Empty or a value of Nothing. However, the designers of the .NET Framework created a convention for passing parameterized information from an event source to its event handlers. The convention involves the creation of a custom event argument class and a custom delegate type.
The mouse events raised by the Form class provide a good example of how this convention should be used. Parameterized information about the mouse position and about which mouse button has been pressed are modeled in a class named MouseEventArgs. The MouseEventArgs class contains an X and a Y property to track the mouse position as well as a Button property to indicate which mouse button has been pressed. Note that by convention the MouseEventArgs class must inherit from the generic class EventArgs.
The convention for passing parameterized information in an event notification requires a custom delegate to complement the custom event argument class. Therefore, there is a delegate named MouseEventHandler to complement the class MouseEventArgs. The handler delegate has this definition:
Delegate Sub MouseEventHandler(sender As Object, e As MouseEventArgs)
Now let's say you'd like to respond to a mouse-related event such as the MouseDown event of the Form class. You can write a handler method that looks like the one shown in Figure 5.
Figure 5 MouseEventHandler
Private Sub Form1_MouseDown(ByVal sender As Object, _
ByVal e As MouseEventArgs) _
Handles MyBase.MouseDown
'*** capture mouse position
Dim x_position As Integer = e.X
Dim y_position As Integer = e.Y
'*** take action depending on which button was pressed
Select Case e.Button
Case MouseButtons.Left
'*** do something
Case MouseButtons.Right
'*** do something else
End Select
End Sub
Note that the e parameter is very useful in the implementation of this handler method. The e parameter is used to determine the mouse position as well as to determine which mouse button has been pressed. All this parameterized information has been made possible by the design of the MouseEventArgs class.
You can find other examples of this parameterization convention used in the Windows Forms framework. For example, there is a class named KeyPressEventArgs that is complemented by a delegated type named KeyPressEventHandler. In addition, the ItemChangedArgs class is complemented by a delegate type named ItemChangedHandler. You will likely encounter other events with parameterized information that follow this same convention.
Parameterizing Custom Events
As an exercise, let's design a custom event to follow this convention for parameterization. I am going to use an example similar to what I used in my last few columns involving a BankAccount class. Consider the following code snippet:
Class BankAccount
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
'*** raise event
End If
'*** perform withdrawal
End Sub
End Class
Assume you are required to raise an event whenever a BankAccount object experiences a withdrawal for an amount greater than $5,000. When you raise this event, you are required to pass all the registered event handlers the amount of the withdrawal as a parameter. First, you should create a new event argument class that inherits from the EventArgs class:
Public Class LargeWithdrawArgs : Inherits EventArgs
Public Amount As Decimal
Sub New(ByVal Amount As Decimal)
Me.Amount = Amount
End Sub
End Class
A custom event argument class should be designed to contain a public field for each parameterized value an event source needs to pass to its event handler. In this case, the LargeWithdrawArgs class has been designed with a Decimal field named Amount. Next, you must create a new delegate type to complement your new event argument class:
Delegate Sub LargeWithdrawHandler(ByVal sender As Object, _
ByVal e As LargeWithdrawArgs)
By convention, this delegate type has been defined with an Object parameter named sender as the first parameter. The second parameter, e, is based on the custom event argument class.
Now that you have created the custom event argument class and a complementary delegate type, you can put them to use. Examine the following class definition:
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
Dim args As New LargeWithdrawArgs(Amount)
RaiseEvent LargeWithdraw(Me, args)
End If
'*** perform withdrawal
End Sub
End Class
The LargeWithdraw event has been modified to use the standard convention in the .NET Framework for passing parameterized information in an event notification. When it's time to raise a LargeWithdraw event in the Withdraw method, it's necessary to create a new instance of the LargeWithdrawArgs class and pass it as a parameter. Since the BankAccount object is the one that is raising the event, the Me keyword can be used to pass the sender parameter, as shown here:
Dim args As New LargeWithdrawArgs(Amount)
RaiseEvent LargeWithdraw(Me, args)
Now that you have seen how to create the event source, let's turn our attention to creating a handler method for this event. A handler method will be able to retrieve the parameterized information it needs through the e parameter. In this case, a handler method will use the e parameter to retrieve the value of the Amount field:
Sub Handler1(sender As Object, e As LargeWithdrawArgs)
'*** retrieve parameterized information
Dim Amount As Decimal = e.Amount
End Sub
Figure 6 shows a complete app in which a BankAccount object sends out event notifications when a large withdrawal is made. Note that this app follows the standard common language runtime convention for passing parameterized information in an event.
Figure 6 Custom Parameterized Events
'*** custom event arguments class
Class LargeWithdrawArgs : Inherits EventArgs
Public Amount As Decimal
Sub New(ByVal Amount As Decimal)
Me.Amount = Amount
End Sub
End Class
'*** delegate to complement custom event arguments class
Delegate Sub LargeWithdrawHandler(ByVal sender As Object, _
ByVal e As LargeWithdrawArgs)
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
Dim args As New LargeWithdrawArgs(Amount)
RaiseEvent LargeWithdraw(Me, args)
End If
'*** perform withdrawal
End Sub
End Class
Class AccountAuditor
Private WithEvents account As BankAccount
Sub Handler1(ByVal sender As Object, _
ByVal e As LargeWithdrawArgs) _
Handles account.LargeWithdraw
'*** retrieve parameterized information
Dim Amount As Decimal = e.Amount
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 AccountAuditor(account1)
'*** do something that triggers callback
account1.Withdraw(5001)
End Sub
End Module
Wrap-up
This concludes my series on the fundamentals of programming with events using Visual Basic .NET. The first two columns showed the mechanics of raising and handling events. This month I concentrated on practical examples of programming with common events and delgates defined within the .NET Framework.
It is likely that the majority of events you will handle with Visual Basic .NET will be based on the EventHandler delegate. You have seen that it's possible to wire up several events to a single handler method. In cases like this, it's important that you know when and how to utilize the sender parameter. You have also seen other events that pass parameterized information using a custom argument class. All in all, you should now be prepared to work with an event-driven framework such as Windows Forms or ASP.NET.
Send your questions and comments for Ted to instinct@microsoft.com.
Ted Pattisonis an instructor and course writer at DevelopMentor (https://www.develop.com). He has written several books about Visual Basic and COM and is currently writing a book titled Building Applications and Components with Visual Basic .NET to be published by Addison-Wesley in 2003.