Events
Both methods and properties allow us to write code that interacts
with our objects by invoking specific functionality as needed. It is often
useful for our objects to provide notification as certain activities occur
during processing. We see examples of this all the time with controls, where a
button indicates it was clicked via a Click event, or a textbox indicates its contents have changed via the TextChanged event.
Our objects can raise events of their own providing a
powerful and easily implemented mechanism by which objects can notify our
client code of important activities or events. In VB.NET, events are provided
using the standard .NET mechanism of delegates.
We'll discuss delegates after we explore how to work with events in VB.
Handling Events
We are all used to seeing code in a form to handle the Click
event of a button code such as:
Private Sub button1_Click
(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles button1.Click
End
Sub
Typically we just write our code in this routine without
paying a lot of attention to the code created by the VS.NET IDE. However, let's
take a second look at that code, since there are a couple of important things
to note here.
First off, notice the use
of the Handles keyword. This keyword specifically indicates that this method will be
handling the Click event from the button1 control. Of course, a control is just an object
so what we're indicating here is that this method will be handling the Click event from the button1 object.
Also notice that the method accepts two parameters. The
Button control class defines these parameters. It turns out that any method
that accepts two parameters with these data types can be used to handle the Click
event. For instance, we could create a new method to handle the event:
Private Sub MyClickMethod(ByVal s As System.Object, _
ByVal args As System.EventArgs) Handles button1.Click
End
Sub
Even though we've changed the method name, and the names of
the parameters, we are still accepting parameters of the same data types and we
still have the Handles
clause to indicate that this method will handle the event.
Handling Multiple Events
The Handles
keyword offers even more flexibility. Not
only can the method name be anything we choose, but a single method can handle
multiple events if we desire. Again, the only requirement is that the method
and all the events being raised must have the same parameter list.
This explains why
all the standard events raised by the .NET system class library have exactly
two parameters the sender and an EventArgs
object. By being so
generic, it is possible to write very generic and powerful event handlers than
can accept virtually any event raised by the class library.
One common scenario where this is useful is where we have
multiple instances of an object that raises events, such as two buttons on a
form:
Private Sub MyClickMethod(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles button1.Click, button2.Click
End
Sub
Notice that we've modified the Handles
clause to have a comma-separated list of events to handle. Either event will
cause our method to run, giving us a central location to handle these events.
The WithEvents Keyword
The WithEvents
keyword tells VB that we want to handle any events
raised by the object within our code. For example:
Friend
WithEvents button1 As System.Windows.Forms.Button
The WithEvents
keyword makes any events
from an object available for our use, while the Handles
keyword is used to link specific events to our methods so we can receive and
handle them. This is true not only for controls on forms, but also for any
objects that we create.
The WithEvents
keyword cannot be used to
declare a variable of a type that doesn't raise events. In other words, if the Button
class didn't contain code to raise events, we'd get a syntax error when we
attempted to declare the variable using the WithEvents
keyword.
The compiler can tell which
classes will and won't raise events by examining their interface. Any class
that will be raising an event will have that event declared as part of its
interface. In VB.NET, this means that we will have used the Event keyword to declare at least one event as part of
the interface for our class.
Raising Events
Our objects can raise events just like a control, and the code
using our object can receive these events by using the WithEvents
and Handles
keywords. Before we can raise an event from our object, however, we need to
declare the event within our
class by using the Event keyword.
In our Person
class, for instance, we may
want to raise an event any time the Walk
method is called. If we call
this event Walked,
we can add the following declaration to our Person
class:
Public Class Person
Private mstrName As String
Private mdtBirthDate As Date
Private mintTotalDistance As Integer
Private colPhones As New Hashtable()
Private mintAllergens As Integer
Public Event Walked()
Our events can also have parameters values that are
provided to the code receiving the event. A typical button's Click
event receives two parameters, for instance. In our Walked
method, perhaps we want to also indicate the distance that was walked. We can
do this by changing the event declaration:
Public Event Walked(ByVal Distance As Integer)
Now that our event is declared, we can raise that event
within our code where appropriate. In this case, we'll raise it within the Walk
method so any time that a Person
object is instructed to walk,
it will fire an event indicating the distance walked. Make the following change
to the Walk
method:
Public Sub Walk(ByVal Distance As Integer)
mintTotalDistance += Distance
RaiseEvent Walked(Distance)
End
Sub
The RaiseEvent
keyword is used to raise the actual
event. Since our event requires a parameter, that value is passed within
parentheses and will be delivered to any recipient that handles the event.
In fact, the RaiseEvent
statement will cause the
event to be delivered to all code that has our object declared using the WithEvents
keyword with a Handles
clause for this event, or any code that
has used the AddHandler
method.
If more than one method will be receiving the event, the
event will be delivered to each recipient one at a time. The order of delivery
is not defined meaning that we can't predict the order in which the
recipients will receive the event but the event will be delivered to all
handlers. Note that this is a serial, synchronous process. The event is
delivered to one handler at a time, and it is not delivered to the next handler
until the current handler is complete. Once we call the RaiseEvent
method, the event will be delivered to all listeners one after another until it
is complete there is no way for us to intervene and stop the process in the
middle.
Receiving Events with WithEvents
Now that we've implemented an event within our Person
class, we can write client code to declare an object using the WithEvents
keyword. For instance, in our project's Form1
code module, we can write the
following:
Public Class Form1
Inherits System.Windows.Forms.Form
Private WithEvents mobjPerson As Person
By declaring the variable WithEvents,
we are indicating that we
want to receive any events raised by this object.
We can also choose to declare the variable without the WithEvents
keyword, though, in that case, we would not receive events from the object as
described here. Instead we would use the AddHandler
method, which we'll
discuss after we cover the use of WithEvents.
We can then create an instance of the object, as the form is
created, by adding the following code:
Private Sub Form1_Load(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
mobjPerson = New Person()
End
Sub
At this point, we've declared the object variable using WithEvents,
and have created an instance of the Person
class so we actually have an
object with which to work. We can now proceed to write a method to handle the Walked
event from the object by adding the following code to the form. We can name
this method anything we like it is the Handles
clause that is important as
it links the event from the object directly to this method, so it is invoked
when the event is raised:
Private Sub OnWalk(ByVal Distance As Integer) Handles mobjPerson.Walked
MsgBox("Person walked " & Distance)
End
Sub
We're using
the Handles
keyword to indicate which event should be handled by this method. We're also
receiving an Integer
parameter. If the parameter list of our method doesn't match the list for the
event, we'll get a compiler error indicating the mismatch.
Finally, we need to call the Walk
method on our Person
object. Add a button to the form and write the following code for its Click
event:
Private Sub Button1_Click(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles button1.Click
mobjPerson.Walk(42)
End
Sub
When the button is clicked,
we'll simply call the Walk
method, passing an Integer value. This will cause the code in our class to be run including the RaiseEvent statement. The result will be an event firing back
into our form, since we declared the mobjPerson variable using the WithEvents keyword. Our OnWalk method will be run to handle the event, since it has the Handles clause linking it to the event.
The following diagram illustrates the flow of control:
The diagram illustrates how the code in the button's click
event calls the Walk method, causing it to add to the total distance
walked and then to raise its event. The RaiseEvent
causes the OnWalk
method in the form to be invoked and, once it is done, control returns to the Walk
method in the object. Since we have no code in the Walk
method after we call RaiseEvent, the control returns to the Click
event back in the form, and then we're all done.
Many people have
the misconception that events use multiple threads to do their work. This is
not the case. Only one thread is involved in this process. Raising an event is
much like making a method call, in that our existing thread is used to run the
code in the event handler. This means our application's processing is suspended
until the event processing is complete.
Receiving Events with AddHandler
Now that we've seen how to receive and handle
events using the WithEvents and Handles
keywords, let's take a look at an alternative
approach. We can use the AddHandler
method to dynamically add
event handlers through our code.
WithEvents and the Handles
clause require that we
declare both the object variable and event handler as we build our code,
effectively creating a linkage that is compiled right into our code. AddHandler,
on the other hand, creates this linkage at runtime, which can provide us with
more flexibility. Before we get too deep into that however, let's see how AddHandler
works.
In Form1, we can change the way our code interacts with
the Person
object first eliminating the WithEvents
keyword:
Private mobjPerson As Person
and then also eliminating the Handles
clause:
Private Sub OnWalk(ByVal Distance As Integer)
MsgBox("Person
walked " &
Distance)
End Sub
With these changes, we've eliminated all event handling for
our object and so our form will no longer receive the event, even though the Person
object raises it.
Now we can change the code to dynamically add an event
handler at runtime by using the AddHandler
method. This method simply
links an object's event to a method that should be called to handle that event.
Any time after we've created our object, we can call AddHandler
to set up the linkage:
Private Sub Form1_Load(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
mobjPerson = New Person()
AddHandler mobjPerson.Walked, AddressOf OnWalk
End
Sub
This single line of code does the same thing as our earlier
use of WithEvents
and the Handles
clause causing the OnWalk method to be invoked when the Walked
event is raised from our Person
object.
However, this linkage is done at runtime, and so we have
more control over the process than we have otherwise. For instance, we could
have extra code to decide which event
handler to link up. Suppose we have another possible method to handle the event
in the case that a message box is not desirable. Add this code to Form1:
Private Sub LogOnWalk(ByVal Distance As Integer)
System.Diagnostics.Debug.WriteLine("Person walked " &
Distance)
End
Sub
Rather than popping up a message box, this version of the
handler logs the event to the Output
window in the IDE.
Now we can enhance our AddHandler
code to decide which handler should be used dynamically
at runtime:
Private Sub Form1_Load(ByVal
sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
mobjPerson = New Person()
If Microsoft.VisualBasic.Command = "nodisplay" Then
AddHandler mobjPerson.Walked, AddressOf LogOnWalk
Else
AddHandler mobjPerson.Walked, AddressOf OnWalk
End If
End
Sub
If the word nodisplay
is on the command line when
our application is run, the new version of the event handler will be used
otherwise we'll continue to use the message box handler.