Understanding Events and Event Handlers

Team-Fly    

 
Visual Basic .NET Unleashed
By Paul Kimmel
Table of Contents
Chapter 8.  Adding Events

Understanding Events and Event Handlers

Windows is a message-based, event-driven operating system. Events are simply occurrences that can be almost anything; basic occurrences are things like someone moving a window on the desktop. The movement of a form is an occurrence after which other forms need to be notified, so they can repaint their visible client regions . We refer to such occurrences as events.

At the OS level, an event is usually caused by an interrupt request. When you hit a key on your keyboard, special subprograms called interrupts (in the case of the keyboard, interrupts 9 and 16) are fired internally. The Windows OS detects the interrupt and packages up all the information necessary to describe the interrupt. The package describing what occurred is a message. At the Windows level, messages are generic structures that contain information. In the case of the keyboard interrupt, the message would contain a type identifier and keyboard state information, including Shift and Ctrl key states and which other keys were pressed. In early Windows programming, programmers had to write message crackers that deciphered all the generic message information and figured out what to do with it. Raw messages were a little hard to work with because the data was very generic.

In a program, the receipt of a message is an event. At another level of abstraction, Windows receives messages and does the unpacking of each message, converting generic detail to message-type specific detail. For example, a keystroke could be converted to a well-named constant instead of a raw value. A program can pass the address of a procedure to the Windows OS that can be called in response to a particular action. Procedure addresses when used in this manner are referred to as callbacks. Procedure addresses used to respond to Windows events are called event handlers.

Passing procedure pointers is still a pretty low level of abstraction. What we want in software development is increasingly higher levels of abstraction, allowing us to work at general, high levels, rather than arcane low levels.

This is where VB6 comes in. Having all this complexity hidden from VB programmers was one of the things that made VB6 easy to program in. VB6 programmers simply clicked on a Command control and wrote some code. The specifics of working with procedure addresses, callbacks, events, interrupts, and messages were concealed from VB6 programmers. (Again, it's important to note that other languages, like C++ and Object Pascal, supported working at any of these levels of abstraction.) As a matter of fact, unless you programmed heavily with the Windows API, you might never have encountered a callback in VB6. This brings us to the present day.

Support for writing dynamic event handlers and callbacks has historically made other languages more expressive than Visual Basic 6. Hence, significantly more programmer support for writing event handlers was added to Visual Basic .NET. However, you will immediately notice a couple of things in Visual Basic .NET. You can still click on a button (equivalent to a VB6 Command control) and generate an event handler for that button, and you will notice that the code implementing this behavior has changed. The result is that event handling is still easy in Visual Basic .NET. What might not be readily apparent is that you can do a lot more than simply write event-handling code at design time.

A dynamic event handler is an event handler that is reassigned or managed programmatically while a program is running.

Visual Basic .NET supports writing static event handlers at design time, just like VB6. In addition to static event handlers, Visual Basic .NET supports writing and using procedural parameterscallbacksat runtime, assigning multiple object events to a single event handler, parameter passing to event handlers, multicasting events, defining procedural types, implementing Shared events, and declaring and raising events in structures, modules, and classes.

Most of the behavior described in the last paragraph is supported by the new, special class referred to as a delegate. We can use events in the same way we used them in VB6click and codeand we can use them in a more advanced way by understanding how delegates work. In this chapter, we will look at delegates simply as a means of writing responsive code. Because delegates introduce some new concepts, we will cover more advanced techniques supported by delegates in Chapter 9.

Before we begin working with Visual Basic .NET event handlers, let's take a moment to examine some of the basics.

Basic Delegate: EventHandler

The basic event handler now has the following signature:

 Sub EventHandler(ByVal sender as System.Object, ByVal e As System.EventArgs) 

This is the signature of the delegate named EventHandler.

When you encounter code like the following example of an event handler for a button-click event, you can now readily identify it as code compatible with the EventHandler delegate:

 Private Sub button1_Click(ByVal sender As System.Object, ByVal e As _   System.EventArgs) Handles button1.Click 

Notice that an EventHandler delegate is associated with subroutines, as opposed to functions. The name in this instance is button1_Click. As in VB6, the name by convention indicates the object name attached to the event name by an underscore (_). Also notice the two matching parameters, sender and e, and their respective data types. What's new about this code from the delegate definition is the Handles clause, which tells you very clearly what object and event this subroutine responds to. An event handler supports handling events for multiple objects. This is a departure from VB6 and is the reason the Handles clause exists. The Handles clause lists all the objects this event handles events for (the upcoming section "Connecting Multiple Events to a Single Event Handler" has more information on its use).

Basically, the event handler statement is designed to indicate its primary purpose. When Button1 is clicked, button1_Click is called. Keep in mind, too, that an event-handling subroutine can be called directly just like any other subroutine.

A departure from VB6, as mentioned, is the inclusion of event arguments. These arguments provide some extra capability if used correctly.

Event Handler Object Argument

The sender argument is the usual invoking object. As you might recall, all Visual Basic .NET objects are derivatives of the Object class. This means that any object can be passed as the value of the event handler's sender argument.

Using a root object makes the general purpose event handler very flexible. For example, if you click button1 (from the fragment) the value of sender will be button1. You might handily jump to the conclusion that this is no big deal. You created the handler using button1, so of course the sender is button1. When the event handler is invoked, you can simply refer to button1 directly. Well, that's what we used to do in VB6 because we had few alternatives. And, you can still refer to a known object when you write the code, but you will be missing an advantage of the handler's sender argument.

If we use the sender argument and cast it to the right type, our event handler doesn't need to be revised if, for example, the name of the button changes. Making an event handler generic takes a little extra work but is worth the effort. Consider the two event handlers shown in Listings 8.1 and 8.2.

Listing 8.1 Using an event handler in a VB6 way.
  1:  Private Sub button1_Click(ByVal sender As System.Object, _  2:  ByVal e As System.EventArgs) Handles button1.Click  3:   4:  MsgBox(button1.Text)  5:   6:  End Sub 

The first listing refers directly to button1 because the code assumes that button1 will always be the only object handled by this event. This is functional, but if button1 changes or the event handler needs to be flexible in any way, the code is too rigid. Furthermore, if button1 doesn't reside in the same code as the event handler, we would be breaching module or class boundaries. Breaching boundaries goes againstgood practices relative to supporting encapsulation. A preferable way to write the preceding code is demonstrated in Listing 8.2.

Listing 8.2 A flexible way of handling the event handler sender argument in Visual Basic .NET.
  1:  Private Sub button1_Click(ByVal sender As System.Object, _  2:  ByVal e As System.EventArgs) Handles button1.Click  3:   4:  If (TypeOf sender Is Button) Then  5:  MsgBox(CType(sender, Button).Text)  6:  End If  7:   8:  End Sub 

As mentioned, the code is slightly more complex. Line 4 uses the TypeOf operator to determine if sender is a button. If sender is a button, the CType procedure is used to safely cast the object sender as a Button. CType returns a reference to the first argument dynamically cast as a type of the second argument. That is, Object.ReferenceEquals(sender, CType(sender, Button)) is True. In the example, CType is returning a reference to sender cast as a Button. The .Text property returns the text property of the button.

Listing 8.2 works under more conditions than Listing 8.1. If sender isn't a button, the code is still correct. If sender is some other button, the code works correctly. If button1 is renamed , the code in Listing 8.2 still works correctly. Listing 8.1 only works under one precise set of circumstances, when button1 exists.

Because we are passing a generic base object and Visual Basic .NET supports dynamic type checking and casting, more kinds of classes can use the generic EventHandler effectively. EventHandler supports writing code related to a specific kind of object without knowing the actual name or location of the object. The sender argument represents what we know about the world from the inside of the button1_Click event handler. If we stick to using the sender argument rather than a specific known object, we are less likely to write brittle code.

Event Handler EventArgs Argument

In addition to passing the invoking object, there are specific kinds of events where it's useful to know more information about what was going on when the event was invoked. For example, it's enough to know that the sender was a button when the button1_Click event is invoked, but this might not be sufficient information if a mouse event occurred.

If the user clicked the mouse button, you might want to know the location of the mouse and which button was clicked. Clicking a mouse button raises an OnMouseDown event, and the actual event handler signature includes an Object and a MouseEventArgs parameter. Listing 8.3 demonstrates an event handler for MouseDown for a form.

Listing 8.3 MouseDown event handler.
  1:  Private Sub Form1_MouseDown(ByVal sender As Object, ByVal e As _  2:  System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown  3:   4:  MsgBox(String.Format("X={0:d} , Y={0:d} ", e.X, e.Y))  5:  MsgBox(TypeOf e Is System.EventArgs)  6:   7:  End Sub 

The type of the second argument, e, is MouseEventArgs. From line 5 you will learn that MouseEventArgs IsA EventArgs, that is, MouseEventArgs is subclassed from EventArgs. Line 4 demonstrates some of the parameters of MouseEventArgs, including the X and Y position of the mouse. IntelliSense will help you discover the parameters of a subclass of EventArgs, or you can reference the help whenever a new EventArgs objects is passed to your event handler.

Listings 8.1 through 8.3 aren't intended to suggest that all delegates will have the same signature, including Object and EventArgs arguments. It just happens to be convenient for event handlers to share a common signature, especially where controls are concerned because many controls will have similar parameter needs, like Paint, Keypress, Load, or MouseDown. It's easier to use a generic signature that works for many cases than it is to implement a new handler for each type of control. (A less general approach would require a lot of additional coding on Microsoft's part and ours.)

Chapter 9 will demonstrate other kinds of delegates with additional parameters and no parameters. Throughout this book we will use standard delegates where it makes sense to do so, and implement custom delegates otherwise . Listing 8.4 demonstrates how we might combine these basic events in an application.

Listing 8.4 The SimpleDraw.exe listing.
  1:  Public Class Form1  2:  Inherits System.Windows.Forms.Form  3:   4:  #Region " Windows Form Designer generated code "  5:   6:  Private APen As Pen  7:  Private OldX, OldY As Integer  8:   9:  Public Sub New()  10:  [...]  11:  APen = New Pen(Color.Black)  12:  End Sub  13:   14:  [...]  15:  #End Region  16:   17:  Private Sub DrawPoint(ByVal X As Integer, ByVal Y As Integer)  18:  CreateGraphics().DrawLine(APen, X, Y, OldX, OldY)  19:  End Sub  20:   21:  Private Sub ResetOffset(ByVal X As Integer, ByVal Y As Integer, _  22:  Optional ByVal Reset As Boolean = False)  23:   24:  OldX = X + CInt(Reset)  25:  OldY = Y + CInt(Reset)  26:   27:  End Sub  28:   29:  Private Sub Form1_MouseMove(ByVal sender As Object, _  30:  ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove  31:   32:  statusBar1().Text = String.Format("X:{0:d} , Y:{0:d} ", e.X, e.Y)  33:  If (e.Button = MouseButtons().Left) Then  34:  DrawPoint(e.X, e.Y)  35:  End If  36:   37:  ResetOffset(e.X, e.Y, e.Button = MouseButtons().None)  38:   39:  End Sub  40:   41:  Private Sub Form1_MouseDown(ByVal sender As Object, _  42:  ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown  43:   44:  DrawPoint(e.X, e.Y)  45:  End Sub  46:   47:  End Class 

The code listing defines a simple drawing program using a couple of mouse events and a few of the capabilities of GDI+. (For more on GDI+, refer to Chapter 17, "Programming with GDI+.") The application is trivial. It will allow basic line drawing as shown in Figure 8.1, but doesn't store the image nor does it handle repainting .

Figure 8.1. SimpleDraw.exe demonstrates using event handlers to respond to user inputs.

graphics/08fig01.jpg

Code outlining was used to hide text that was generated by the IDE. The hidden outlined selections are represented by the [...] portions of the code. To create the sample program, declare a Pen and OldX and OldY integers. The Pen is defined in System.Drawing and is used for many of the graphics methods . OldX and OldY are used to follow the mouse movement. To complete the SimpleDraw application, perform the following steps:

  1. Ensure that the System.Drawing namespace is imported into a blank Windows application.

  2. Declare the aforementioned Pen, OldX, and OldY integer fields at the top of the Form class.

  3. Define the subroutines ResetOffset and DrawPoint as shown in Listing 8.4.

  4. From the toolbox, add a status bar control to the bottom of Form1.

  5. To create the event handlers, MouseMove and MouseDown, switch to the Code designer view (if you aren't there already).

  6. In the Objects list (in the same position as VB6, the Object list is the combo box at the top left side of the code editor), select (Base Class Events) for Form1.

  7. From the Procedures list (also in the same position as VB6, the Procedures list is the combo box at the top right side of the code editor), select MouseDown and MouseMove in turn to generate the event handlers for these two events. (Refer to the section "Creating Event Handlers in the Windows Form Designer" for alternative ways to generate event handlers)

  8. Add the code between each event handler as shown in Listing 8.4.

  9. Change the background color of the form to White.

  10. Press F5 to test the application.

To create simple line drawings, click the mouse in the client region of the form and drag the mouse cursor around. You should see some rudimentary drawing.

I would like to point out a few details here. ResetOffset was written differently to begin with. ResetOffset is the result of a refactoring, explained in the next subsection. The Pen object is declared at the class level and created in the New constructor. This was done so that I would only have to create the Pen object one time. CreateGraphics() actually creates a graphics object. The constructor for a graphics object is protected, so the only way to get one is to call CreateGraphics(). CreateGraphics() is an example of Microsoft programmers using Replace Constructor with Factory Method (whether they thought about it or not). One reason to use factory methods is to consolidate some specific steps that must be taken to ensure property initialization of an object; MS programmers were kind enough to hide the complexity of initializing graphics objects from us. Notice that I am creating a graphics object each time DrawPoint is called. This was done because the graphics object clips to the current client region. If the graphics object were created one time when the form was constructed , the next time the form was resized, drawing wouldn't work outside of the client region of the original form size . Creating the graphics object each time seems to incur no additional performance penalties and works correctly when the form is resized.

Refactoring: "Extract Method"

MouseMove from lines 29 to 39 from Listing 8.4 was originally defined as follows :

 Private Sub Form1_MouseMove(ByVal sender As Object, _   ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove   statusBar1().Text = String.Format("X:{0:d} , Y:{0:d} ", e.X, e.Y)   If (e.Button = MouseButtons().Left) Then     DrawPoint(e.X, e.Y)     OldX = e.X     OldY = e.Y   Else     OldX = e.X + 1     OldY = e.Y + 1   End If End Sub 

Notice the If...Then...Else statement in the preceding listing. It's not immediately clear what the OldX and OldY code is doing and the listing is about 15 lines long, a bit too long. The motivation for refactoring this code is to shorten and clarify this method, make behaviors reusable as they will be in a separate method, and use a well-named method to articulate meaning.

What the code in the fragment does (as well as the same code in Listing 8.4) is update the status bar, draw the point if the left mouse button is down, and then adjust the OldX and OldY points.

The refactoring "Extract Method" is performed in the following prescribed manner:

  1. Create a method with a name indicating what the method will do.

  2. Extract the relevant code from the original method to the new method.

  3. Add parameters to the new method to pass data used in the old method to the new method.

  4. Pass the data necessary to ensure proper operation from the old method to the new method.

  5. Compile and test the changes.

For Step 1, I chose ResetOffset as the method name. Step 2 indicates that relevant code should be extracted from the old method. I removed the references to OldX and OldY from the MouseMove event handler and placed them into the ResetOffset method, sending the e.X and e.Y values as arguments to the new method. The last value I needed was a Boolean indicating when the offset values should be reset. For this implementation, when there was no button clicked, OldX and OldY values needed to be reset. The revised code is contained in Listing 8.4 from lines 21 through 39.

The complete mechanics and motivation for "Extract Method" can be found in Fowler, page 110. Other mechanicsto use Fowler's term might be involved to complete the "Extract Method" refactoring. If temporary variables are used with the extracted code, you will need to relocate them to the new method. You might also want to perform the "Replace Temporary with Query" refactoring (introduced in Chapter 3). Finally, if any variables in the original method are modified by the new, refactored method, you might need to return that value as a ByRef argument or make the new method a function.

"Extract Method" is an easy refactoring technique to employ . It's one that will most likely be commonly used. By extracting code and making methods from that code, your procedures will stay smaller, be self-documenting , and you will have more atomic procedures that will have a greater incidence of reuse.

Connecting Multiple Events to a Single Event Handler

Visual Basic 6 allowed you to write one handler procedure to handle a single event type for multiple controls, but the controls had to be in a control array. An event handler for multiple controls was implemented to accept an index into the control array of the actual control raising the event. This is a structured solution; it involves arrays and indexes instead of polymorphism. The following VB6 code fragment demonstrates a control array, Command1(), associated with one event handler:

 Private Sub Command1_Click(Index As Integer)   MsgBox Command1(Index).Caption End Sub 

In the VB6 solution, Command1 isn't even a control any more, it's an array of controls. When I created the Command1 control, I only created one Command button. To create the control array, I copied and pasted Command1. Instead of giving me similar properties and a separate control, VB6 changed the nature of the first Command control from a single control to an array of controls. Converting a control to an array of controls in order to assign a single event handler to multiple controls is not intuitive behavior. Controls and event relationships should work without an array (However, single event handlers for multiple controls were supported this way because VB6 doesn't support object-oriented inheritance.)

Intuitively, this isn't what you might expect: create a control, copy it, and you get a control array. What you might intuitively expect is a separate control with a very similar state. Visual Basic .NET supports copying the control. When you drop a control on a form, select the control. Press Ctrl+C to copy the control and Ctrl+V to paste the copy to the form. In Visual Basic .NET, you will get a second control with almost identical properties. The control name will be different and the actual object will be a separate object from the copied one: no control array.

To experiment with the revised copy-and-paste -control behavior in Visual Basic .NET, we'll use a button control. Using a button control, paint a button on a blank form. Copy the button and paste the copy to the form. Select both buttons button1 and button2, named by defaultand double-click to generate an event handler. This step will create two separate event handlers, one for each button. Listing 8.5 shows the procedure body generated for each event handler.

Listing 8.5 Two empty event handlers for two separate button controls.
  1:  Private Sub button2_Click(ByVal sender As System.Object, _  2:  ByVal e As System.EventArgs) Handles button2.Click  3:   4:  End Sub  5:   6:  Private Sub button1_Click(ByVal sender As System.Object, _  7:  ByVal e As System.EventArgs) Handles button1.Click  8:   9:  End Sub 

When you click button1, the button1_Click event handler is called. When you click button2, the button2_Click event handler is called. Note the Handles clause at the end of each event handler indicating to which control and event the event handler will respond. To have a single event handler respond to more than one control, add a comma-delimited list of objects and events at the end of the Handles clause:

 Private Sub button2_Click(ByVal sender As System.Object, _   ByVal e As System.EventArgs) Handles button2.Click, button1.Click 

With the revision (shown in the fragment), button2_Click also handles button1_Click events. You can type the Handles clause manually or use the designer to indicate multicontrol event handling. I will return to this topic after we look at some of the other ways of defining and using event handlers.

There are two other issues we need to take a moment to discuss. The first issue is why we would want one procedure to handle events for more than one control. The second issue is what happens in the preceding scenario where there are now two event handlers for one control, namely button1. Let's look at each issue in turn.

Multiple-Control Event Handlers

Event handlers most often respond to interaction from the user. It's common for multiple control metaphors to exist for a single operation. For example, in MS Word, I can click File, Open or the folder on the toolbar to open a file. The File, Open menu and the folder on the toolbar are both metaphors for the file open operation. Invoking this operation should run the same code, whether I use the toolbar button or menu command to invoke it. Because the operation is identical, the code also should be identical.

Allowing a single event to support multiple metaphors for invoking that behavior is supported by VB.NET. Additionally, because Visual Basic .NET supports object-oriented inheritance and polymorphism, we can use a single event handler and code to perform different operations based on different objects. Using the code from Listing 8.5, here is a demonstration in Listing 8.6.

Listing 8.6 Two event handlers for one control.
  1:  Private Sub WhoAmI(ByVal Value As Button)  2:  MsgBox("I am " & Value.Text)  3:  End Sub  4:   5:  Private Sub button2_Click(ByVal sender As System.Object, _  6:  ByVal e As System.EventArgs) Handles button2.Click, button1.Click  7:   8:  WhoAmI(CType(sender, Button))  9:   10:  End Sub 

The code on line 8 indicates who the sender was. (Notice the refactored WhoAmI procedure.) Yes, these are trivial examples, but the code demonstrates two techniques clearly: one handler and one set of code can respond to more than one control, and extracted methods with good names make code clearer.

Multiple Event Handlers for a Control

Visual Basic .NET supports assigning multiple procedures to one or more event handlers. This means that several procedures could each implement part of the solution. In Listing 8.5, if we modified line 2 to indicate that we wanted button2_click to handle the Click event for button1 and button2 and we left button1_Click intact, both the button1 and button2 handlers would be called when button1 is clicked.

Calling more than one procedure per event is an aspect of multicast delegates. Delegates keep a list of callbacks and invoke all procedures maintained in a list internal to the delegate. Multicast delegates are an interesting and powerful feature of Visual Basic .NET that we will explore more in Chapter 9.


Team-Fly    
Top
 


Visual BasicR. NET Unleashed
Visual BasicR. NET Unleashed
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 222

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net