Understanding Delegates in Visual Basic .NET
- Using the EventHandler Delegate
- Reviewing Delegate Members
- Defining Delegates
- Passing Delegates as Arguments
- Multicasting Delegates
- Using Delegates Across Project Boundaries
Visual Basic 6 provided an opportunity for us to become familiar with events as a dynamic aspect of Windows programming. From Chapter 8, you know that an event is an occurrence in your program, and an event handler is a procedure defined to respond to that occurrence.
To review, the event-handling mechanism works because procedures are effectively addresses. If we know the arguments that are passed to a procedure and have a procedure's address, we can invoke a procedure because this is how procedure invocation works internally. VB6 allowed us to pass the address of a procedure to Windows for API calls that needed a callback address, but didn't support callbacks within VB6 itself. Visual Basic .NET supports procedural types through the Delegate class. Delegates maintain the addresses of procedures used as callback procedures. When referring to a procedural type, think of a variable declaration whose type happens to be the signature of a procedure. Procedural type is the generic term that has been in existence in other languages for years; in Visual Basic .NET, procedural types are specifically referred to as delegates.
Because delegates are classes in Visual Basic .NET, we have extended capabilities beyond one instance of a Delegateprocedural typecontaining a single address of one procedure. Delegates are implemented to support a list of addresses referred to as an invocation list. Delegates that contain multiple procedure addresses are referred to as multicast delegates. Multicast delegates support a single event having multiple respondents.
VB6 required a control array if you wanted one event handler to handle events for multiple controls. Visual Basic .NET introduces the Delegate class to keep track of event handlers. Visual Basic .NET supports multiple event handler respondents for a single control event and supports multiple controls being associated with a single event handler.
In Chapter 9, you will learn all about defining, declaring, and invoking delegates. Additionally, I will demonstrate how delegates can be used as procedure arguments to support dynamic behavior. We will begin coverage of delegates in this chapter by looking at one of the most common pre-existing delegates, EventHandler.
Using the EventHandler Delegate
The most common delegate is EventHandler. A delegate is defined by preceding the name and signature of a procedure with the keyword Delegate. Applying this to what we know about the EventHandler delegate, we see that in Visual Basic .NET we can write a statement similar to the following:
Delegate Sub EventHandler(sender As Object, e As System.EventArgs)
This statement identifies a type name EventHandler as a delegate that takes an Object and System.EventArgs parameters. (If you've written or seen a function pointer in C/C++ or defined a procedural type in Object Pascal, this syntax will appear similar to you.) The delegate EventHandler is a type. Variables of type EventHandler can be the AddressOf any subroutine that has the same signature as the EventHandler delegate; specifically, the address of any subroutine that takes an Object and System.EventArgs parameters, in that order, can be assigned to an instance of an EventHandler delegate. We will come back to defining delegates and declaring instances of delegates in upcoming sections. For now, because EventHandler is so prevalent, let's take a look at how we can employ its generic arguments.
Delegates can be initialized with subroutines or functions.
Using the EventHandler Object Argument
The generic signature of the EventHandler Delegate wasn't picked by accident. From other architectures, specifically Delphi, a common ancestry has proven to be effective in implementing event handlers for controls like buttons and forms.
Many controls, for example, support a click event. To respond to an event, it's often helpful to know the originator of the event. For example, when a button is clicked, it's often helpful to be able to use the button object itself. The same may be true for forms or pictureboxes; you may want to respond to a click event. Without a common ancestry, an event handler would have to be defined specifically for each of these controls. An event handler for a picturebox would take a PictureBox argument, a form handler a Form argument, and so on. All of these variations of event handlers would cause any implementation supporting dynamic event handlers to swell up and complicate using the event handlers.
Consider a better alternative. A click event really just needs the object that invoked the event. Assuming a common ancestrywhich is what we have in Visual Basic .NETwe can define one type of event handler and allow polymorphism to support specific behaviors for subclasses of Object.
This is exactly what we have in Visual Basic .NET. Object is the common ancestor for classes, ValueTypes (like Integer), and structures. Roughly, this means that anything can be passed to satisfy the Object argument and dynamic type-checking through the TypeOf operator can be used to determine the specific subclass passed to satisfy that argument.
Let's pause for a minute and examine the need for a generic object reference. It begs the question: If a generic object parameter is so important, why are we just now getting an implementation of event handling that supports it?
The direct answer is that previous versions of VB had some shortcomings. Delegates are one of the reasons Microsoft can market Visual Basic .NET as a first-class language. VB6 supported a weaker style of programming event handlers.
In VB6, we would implement an event handler and refer to the specific object in the event-handling code, for example:
Sub Command1_Click() MsgBox Command1.Name End Sub
Unfortunately, this style of programming tightly couples the event handler with a single control. In VB6, this worked moderately well, because in the absence of a control array only one control would be using this code. However, this code broke if you changed the name of the control. Visual Basic .NET supports the event handler as a property of the control. Consequently, if the control name changes, the property value doesn't, and the event handler still works correctly.
Additionally, Visual Basic .NET supports assigning multiple control events to a single handler. Thus the same handler may be invoked by many objects. Attempting the latter would break the VB6 model for event handlers. From the preceding fragment, the equivalent of Command1_Click may not have been invoked by the Command control.
Delegates and a stronger event programming model required that event handlers have arguments, and sanity justified a polymorphic means of implementing event handlers.
Another factor may be that Distinguished Architect Anders Hejlsberg was instrumental in implementing Delphi event handlers this way and Microsoft needed something that worked.
The benefit of a generically defined event-handling Delegate means that multiple controls, supporting semantically similar operations, can be assigned to exactly the same event handler without using control arrays or specific references to controls.
Multiple Event Respondents
Suppose we have a main form with two metaphors for closing the application. For argument's sake, suppose that a File, Exit menu closes the application by closing the main form, as does a button with the text Quit. Each metaphor for closing the application performs semantically the same operationto run the End statement. Clearly, one procedure should be able to handle this operation no matter how it is invoked.
The solution EventHandlerDelegate.sln on this book's companion web site contains the code for this example. The form is implemented by adding a MainMenu control from the Windows Forms tab of the toolbox and a Button to a Form. Add a File menu with an Exit submenu by clicking and typing in the menu designer on the form (see Figure 1), and modify the Text property of Button, adding the text Quit.
Figure 1 MainMenu control editing can be performed directly on the Form Designer as shown.
Double-click on the Exit submenu to generate the event handler procedure body (shown in Listing 1). Add the Quit button to the Handles clause. In this example, the default control names were maintained, so you'll see Handles MenuItem2.Click, Button1.Click in the listing.
Listing 1An Event Handler Responding to Similar Events for Two Separate Controls
Private Sub MenuItem2_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MenuItem2.Click, Button1.Click End End Sub
The code is simple. It calls End to terminate the application. What is important is that MenuItem2 and Button1 are two disparate controls with semantically identical events, allowing one event handler to respond to an event raised by either control. Handles MenuItem2.Click and the event handler were added by double-clicking on the Exit menu item. The Button1.Click predicate was added manually.
In Listing 1, the code doesn't make use of either the Object or System.EventArgs argument. Sometimes, as in the example, you won't need these arguments. However, if the response is dependent on the type of the argument, you will need to type-check the sender argument to determine which action to take.
Type-Checking the Object Argument
Suppose in the example described in the previous section you wanted to perform one of two slightly different operations based on which metaphor was used to invoke the operation. For example, Quit might prompt the user to make sure he or she wanted to exit not only the current context but the entire program, whereas the more deliberate File, Exit might suggest that the user is clear about his or her intentions and definitely wants to exit the application.
We could perform dynamic type-checking on the sender argument in this instance, still using the same event handler and performing similar operationsquittingbut the Quit menu would ask the user to verify his or her intentions. You can type-check the sender argument by using the TypeOf operator.
From the implementation statement, after the user chooses the Quit button, the application exits if the user responds affirmatively to a verification prompt; otherwise, the application does not end. Listing 2 demonstrates the revision to Listing 1.
Listing 2Revised Code from Listing 1, Which Dynamically Type-Checks the sender Argument
1: Private Sub MenuItem2_Click(ByVal sender As System.Object, _ 2: ByVal e As System.EventArgs) Handles MenuItem2.Click, Button1.Click 3: 4: If (TypeOf sender Is Button) Then 5: If (MsgBox("Are you sure?", MsgBoxStyle.YesNo, "Exit Application") _ 6: = MsgBoxResult.No) Then 7: 8: Exit Sub 9: 10: End If 11: End If 12: 13: End 14: 15: End Sub
The revised code is defined on lines 4 through 11. Lines 4 through 11 test the negative case. If sender is a button and the user answers No to the MsgBox prompt, the subroutine exits; otherwise, the application is terminated on line 13. The type of the sender argument is checked on line 4 with the TypeOf operator. In the next subsection, we will use this operator to determine the type of the sender argument and cast the Object type to a specific type.
Refactoring and Algorithmic Decomposition
Let's take a moment to address stylistic issues. Some programmers may object to multiple exit points, but other than some programmers finding the code a little confusing, there is no prohibition against multiple exit points. The alternative is to test the positive case:
If sender is button type then If user wants to quit is True then End else (user doesn't want to quit) Exit Sub else (sender is menu item) End
Testing the positive case results in needing the End statement to appear twice.
As a matter of taste, I would prefer one exit point and a simpler event handler. In this event handler, all I want to know is whether the application can terminate. How termination is decided adds too much complexity to the event handler. Essentially the algorithm is "If CanQuit is True Then End" and this reflects what the code should say. Listing 3 demonstrates the revision using the more precise version of the algorithm.
Listing 3A More Precise Implementation of the Dual-Metaphor Application Termination Event Handler
1: Private Sub MenuItem2_Click(ByVal sender As System.Object, _ 2: ByVal e As System.EventArgs) Handles MenuItem2.Click, Button1.Click 3: 4: If (CanQuit(sender)) Then End 5: 6: End Sub
At the point this event handler is run, and the code now only asks "Can I quit?" If the answer is yes, the application terminates. (This is the singular level of complexity I strive for in my code.) Now the only problem is to implement CanQuit. Just as the event handler is a singular procedure now, so will CanQuit be. This singular division of labor plays to the short-term memory and problem-solving capability of the human mind and is supported by the concept of refactoring.
To implement the revision to Listing 3 from Listing 2, the refactoring "Extract Method" (introduced in Chapter 8) can be used twice to factor the prompt "Are you sure?" and the test to determine whether sender is a button. Alternatively, you can decompose the problem as an algorithm and then implement each of the supporting pieces of the algorithm. CanQuit is decomposed as "sender is a button and prompt response is yes" or "sender isn't a button."
Choosing refactoring or algorithmic decomposition depends on where you are in development of the code. If the code is already written, use the refactoring; if you are writing the code for the first time, state the algorithm and decompose it into its supporting pieces. Refactoring implies revision after the fact, versus decomposition, which is revision before the fact. Because the long version was introduced first, I will demonstrate the Extract Method refactorings in the bulleted list that follows. (Use Listing 4 to follow along with the bulleted list of steps.)
Factor out the MsgBox statement to a Function Quit that displays the message box prompt. Quit returns True if the user clicks Yes in response to the message box.
Replace the literal call to MsgBox in Listing 2 with the call to Quit() testing for False; that is, Quit() = False.
Replace the dynamic type-check of sender with a function IsButton taking a sender As Object argument. (I am performing this refactoring for clarity here, but probably wouldn't in production code.)
Replace the dynamic type-check with the call to IsButton, passing sender in Listing 2. If codified, this change would yield the following:
If( IsButton(sender)) Then If( Quit() = False ) Then Exit Sub End If End If End
Define a Function CanQuit(), which returns a Boolean. Perform the positive test in CanQuit to return a Boolean True if sender is a button and the response to Quit is True or sender isn't a button. CanQuit is implemented using the IsButton and the Quit methods defined thus far. CanQuit() is shown in Listing 4.
Listing 4Revision of the Code from Listing 3 Using the Refactoring Extract Method
1: Private Function Quit() As Boolean 2: Const Prompt As String = "Are you sure?" 3: Return MsgBox(Prompt, MsgBoxStyle.YesNo, 4: "Exit Application") = MsgBoxResult.Yes 5: End Function 6: 7: Private Function IsButton(ByVal sender As Object) As Boolean 8: Return TypeOf sender Is Button 9: End Function 10: 11: Private Function CanQuit(ByVal sender As Object) As Boolean 12: Return (IsButton(sender) AndAlso Quit()) Or Not IsButton(sender) 13: End Function 14: 15: Private Sub MenuItem2_Click(ByVal sender As System.Object, _ 16: ByVal e As System.EventArgs) Handles MenuItem2.Click, Button1.Click 17: 18: If (CanQuit(sender)) Then End 19: 20: End Sub
Listing 4 is longer than Listing 2, the original implementation. In fact, refactoring may result in temporarily longer fragments of code but shorter, more reusable algorithms and fewer lines of code in an overall system. Each algorithm in Listing 4 is expressive and very easy to understand.
I have met many people who don't understand the style of code in Listing 4. Simplistically, it seems as if I have traded one longer procedure for many shorter ones. A counter argument on the benefit side is that whereas the reader had to remember one slightly longer procedure, now the reader has to remember several, although short, procedures.
In very short examples, the use of many short procedures to replace a few longer ones seems to make very little sense. Instead of remembering what lines do, you have to figure out what functions do. Keep in mind that this argument only makes sense in individual examples, not systems. Using the strategiesdecomposition or refactoringdiscussed in this section results in more legible code, the need for fewer comments, procedures that are easier to understand and debug, and a greater number of reusable procedures. Time and experience bear out these assertions and the adoption of refactoring as a methodology supports the argument for singular, factored procedures.
Admittedly none of the methods in Listing 4 can be used again, but it's the overall strategy of factoring code to make individual pieces very easy to understand and more likely to be reused that we are striving for. Further, we are unlikely to know what a candidate for reuse is at the moment we are implementing a particular procedure. Consequently, refactoring provides us with an avenue for extracting code when potential reuse is identified.
The preceding paragraph referring to reusable code is our justification for an architectural model. Without models it becomes increasingly difficult to realize optimal code reuse because developers lose track of available classes and procedures.
Perhaps the absence of models is the reason the industry is not realizing the full potential of object-oriented development. (The last statement is based on personal experience. Only one in 30 projects that I have worked on was actively building an architectural model prior to my participation.)
Typecasting the Object Argument
If you use the sender argument of an EventHandler Delegate as is, you will only be able to use the members of the Object class. To use members of the specific instance, you will need to determine the actual type of the sender argument and cast sender to that type. The preceding section demonstrated the TypeOf and Is operators. To cast a base class to a specific subclass, use the CType function.
Alan Cooper, in his book The Inmates Are Running the Asylum (Sams, 1999, ISBN 0-672-31649-8), addresses confirmation dialog boxes in the opening sentence of the chapter "Software Won't Take Responsibility." "Confirmation dialog boxes are one of the most ubiquitous examples of bad design; the ones that ask us 'are we sure' that we want to take some action." (p. 67) Cooper goes on to suggest that the "are you sure" dialog box was designed to absolve the programmer of responsibility. Instead of prompting "are you sure," Cooper suggests that the user should be presumed sure but able to change her mind later, and that it's the programmer's responsibility to make sure that the user can change her mind (that is, undo an action).
Alan Cooper is the original inventor of Visual Basic, although he hasn't been active in its implementation or design for many years. When I asked Mr. Cooper by email about his book Inmates, I suggested that he had some interesting ideas, and asked if he thought they would be generally adopted. He was kind enough to answer, stating something to the effect that he wanted to make money.
I understood this to mean that perhaps his software would be more people-friendly and would represent its own compelling selling proposition. Contrarily, I think that the WinTel model is ubiquitous and a tremendous upheaval would occur if software were to radically change.
For the example in this section, we will take the middle road. We will assume that the user doesn't want to be prompted to verify any action but the option is a user-configurable option. (Perhaps in tutorial mode, the verification prompts would be presented.)
A production system must replace the verification screens with an undo capability. For example, deleting a record from a database needs to be undoable, especially if the user isn't prompted simply because this represents a significant departure from many implementations.
Although Cooper condemns abdicating responsibility to the user, writing software that is smarterfor example, can undo a delete recordis significantly more challenging than writing software that displays a verification dialog box.
The metaphor used to implement the configurable behavior is represented by a menu option and a checkbox. In a production system, you might represent this behavior with an Options dialog box and persist the choice to a user options table of the Registry. (Keep in mind that the purpose of this example is to demonstrate dynamic typecasting.) To try the example, open DynamicTypeCast.sln from this book's companion web site or create a Windows application and add a MainMenu with a Tools, Prompt On Close menu item and a checkbox. Complete the following steps to re-create the example:
Create a Windows application.
Add a MainMenu with a Tools, Prompt On Close menu item.
Add a Checkbox control to the form.
Double-click on the Prompt On Close menu item to generate the Click event handler (shown in Listing 5).
Add CheckBox1.CheckedChanged to the Handles clause of the event handler.
Complete the numbered steps and add the code as shown in Listing 5. A synopsis of the code follows the listing.
Listing 5A Single Event Handler Maintaining the State of a User-Configurable Option
1: Private FClosePrompt As Boolean = False 2: 3: Property ClosePrompt() As Boolean 4: Get 5: Return FClosePrompt 6: End Get 7: Set(ByVal Value As Boolean) 8: FClosePrompt = Value 9: Changed(Value) 10: End Set 11: End Property 12: 13: Private Sub Changed(ByVal Checked As Boolean) 14: MenuItem4.Checked = Checked 15: CheckBox1.Checked = Checked 16: End Sub 17: 18: Private Sub MenuItem4_Click(ByVal sender As System.Object, _ 19: ByVal e As System.EventArgs) _ 20: Handles MenuItem4.Click, CheckBox1.CheckedChanged 21: Static semaphore As Boolean = False 22: If (semaphore) Then Exit Sub 23: semaphore = True 24: 25: Try 26: 27: If (TypeOf sender Is MenuItem) Then 28: ClosePrompt = Not CType(sender, MenuItem).Checked 29: Else 30: ClosePrompt = CType(sender, CheckBox).Checked 31: End If 32: 33: Finally 34: semaphore = False 35: End Try 36: 37: End Sub
Note that the Handles clause on line 19 indicates that MenuItem4_Click handles MenuItem4.Click and CheckBox1.CheckedChanged events. Because both events have the same Delegate type, the single handler can handle both types of events. The Private field FClosePrompt on line 1 maintains the state, and by default is initialized to False. The property ClosePrompt is defined on lines 3 through 11. Line 8 stores the new ClosePrompt state and line 9 calls the Changed method, which synchronizes both controls' Checked states. Lines 18 through 37 define the single event handler.
Line 21 defines a variable named semaphore. This variable is Static to ensure that the event handler maintains the state of the semaphore between calls. It will be apparent soon why the semaphore is used. Line 22 exits if semaphore is True, and line 23 sets semaphore to True. Line 25 starts a Try Finally block with the Finally setting semaphore to False.
The If condition on lines 27 through 31 sets the value of ClosePrompt depending on whether sender is the MenuItem or CheckBox. If sender is a MenuItem, the Checked property is toggled with a Not statement; otherwise, the Checked property of the CheckBox already has the correct state. Setting ClosePrompt takes care of synchronizing the controls.
What does the semaphore do? The MenuItem part of the If statement updates ClosePrompt, which effectively changes the value of the CheckBox.Checked state. Changing this state causes the event handler to be called recursively, halfway through. The semaphore prevents the code from being executed a second time unnecessarily by making the event handler behave like an empty subroutine until the handler has completely finished the first time.
IntelliSense can provide member information to typecast objects at design time.
It's important to note that the CType function takes an Object and a class. The Object is cast to the type of the class. If the Object doesn't represent the class, a System.InvalidCastException occurs. For example, line 28 casts sender as a MenuItem; however, if sender is actually a Checkbox, this line would cause a System.InvalidCastException. After sender is cast, the members of the cast type can be used.
Using the EventHandler System.EventArgs Argument
The EventHandler is defined to pass a second argument, System.EventArgs. For generic events like Click, the EventArgs argument does not play a big role. However, it does act as a placeholder for more advanced events, like Paint. Paint uses a subclass of EventArgs to pass an instance of the device context wrapped in the Graphics class to the paint event handler.