Events
Windows-based applications are message-based.
This means that the application is communicating with Windows and
Windows is communicating with the application by using predefined
messages. These messages are structures that contain various pieces
of information that the application and Windows will use to
determine what to do next. Prior to libraries such as MFC
(Microsoft Foundation Classes) or to development environments such
as Visual Basic, the developer would have to handle the message
that Windows sends to the application. Visual Basic and now .NET
wrap some of these incoming messages as something called events. If
you need to react to a specific incoming message, you would handle
the corresponding event. A common example of this is when the user
clicks a button on a form. Windows is sending a WM_MOUSECLICK message to the button’s message
handler (sometimes referred to as the Windows Procedure or
WndProc). To the .NET developer this is exposed as the Click event of the button.
In developing object-based applications, another
form of communication between objects is required. When something
of interest happens in one of your objects, chances are that other
objects will want to be informed. Again, events come to the rescue.
Just as the .NET Framework wraps up Windows messages in events, you
can also utilize events as the communications medium between your
objects.
Delegates are used as the means of wiring up the
event when the message is received by the application. Believe it
or not, in the preceding section on delegates, you learned just
about everything you need to know to understand how events work.
However, one of the great things about how Microsoft has designed
C# events is that you don’t actually need to understand anything
about the underlying delegates in order to use them. So, this
section starts off with a short discussion of events from the point
of view of the client software. It focuses on what code you need to
write in order to receive notifications of events, without worrying
too much about what is happening behind the scenes - just so you
can see how easy handling events really is. After that, you write
an example that generates events, and as you do so, you should see
how the relationship between events and delegates works.
The discussion in this section will be of most use
to C++ developers because C++ does not have any concept similar to
events. C# events, on the other hand, are quite similar in concept
to Visual Basic events, although the syntax and the underlying
implementation are different in C#.
|
|
Tip |
In this context, the term event is used in
two different senses. First, as something interesting that happens,
and second, as a precisely defined object in the C# language - the
object that handles the notification process. When we mean the
latter, we will usually refer to it either as a C# event or, when
the meaning is obvious from the context, simply as an event.
|
The Receiver’s View of Events
The event receiver is any application,
object, or component that wants to be notified when something
happens. To go along with the receiver, there will of course be the
event sender. The sender’s job will be to raise the event. The
sender can be either another object or assembly in your
application, or in the case of system events such as mouse clicks
or keyboard entry, the sender will be the .NET runtime. It is
important to note that the sender of the event will not have any
knowledge of who or what the receiver is. This is what makes events
so useful.
Now, somewhere inside the event receiver will be a
method that is responsible for handling the event. This event
handler will be executed each time the event that it is registered
to is raised. This is where the delegate comes in. Because the
sender has no idea who the receiver(s) will be, there cannot be any
type of reference set between the two. So the delegate is used as
the intermediary. The sender defines the delegate that will be used
by the receiver. The receiver registers the event handler with the
event. The process of hooking up the event handler is known as
wiring up an event. A simple example of wiring up the Click event will help illustrate this process.
First, create a simple Windows Forms application.
Drag over a button control from the toolbox and place it on the
form. In the properties window rename the button to buttonOne. In the code editor, add the following
line of code in the Form1
constructor:
Now in Visual Studio you should have noticed that
after you typed in the += operator all you had to do was press the
Tab key a couple of times and the editor will do the rest of the
work for you. In most cases this is fine. However, in this example
the default handler name is not being used, so you should just
enter the text yourself.
What is happening is that you are telling the
runtime that when the Click event of
buttonOne is raised, that Button_Click method should be executed. EventHandler is the delegate that the event uses to
assign the handler (Button_Click) to the
event (Click). Notice that you used the
+= operator to add this new method to
the delegate list. This is just like the multicast example that you
looked at earlier in this chapter. This means that you can add more
than one handler for any event. Because this is a multicast
delegate, all of the rules about adding multiple methods apply;
however, there is no guarantee as to the order that the methods are
called. Go ahead and drag another button onto the form and rename
it to buttonTwo. Now connect the
buttonTwo Click event to the same
Button_Click method, as shown here:
With delegate inference you can also write the code
as follows where the compiler generates the same code as in the
previous version:
The EventHandler
delegate is defined for you in the .NET Framework. It is in the
System namespace, and all of the events
that are defined in the .NET Framework use it. As discussed
earlier, a delegate requires that all of the methods that are added
to the delegate list must have the same signature. This obviously
holds true for event delegates as well. Here is the Button_Click method defined:
A few things are important about this method.
First, it always returns void. Event
handlers cannot return a value. Next are the parameters. As long as
you use the EventHandler delegate, your
parameters will be object and
EventArgs. The first parameter is the
object that raised the event. In this example it is either
buttonOne or buttonTwo, depending on which button is clicked. By
sending a reference to the object that raised the event you can
assign the same event handler to more than one object. For example,
you can define one button click handler for several buttons and
then determine which button was clicked by asking the sender
parameter.
The second parameter, EventArgs, is an object that contains other
potentially useful information about the event. This parameter
could actually be any type as long as it is derived from
EventArgs. The MouseDown event uses the MouseDownEventArgs. It contains properties for which
button was used, the X and Y coordinates of the pointer, and other
info related to the event. Notice the naming pattern of ending the
type with EventArgs. Later in the
chapter, you see how to create and use a custom EventArgs-based object.
The name of the method should also be mentioned. As
a convention, event handlers follow a naming convention of
object_event. object is the object that is raising the event, and
event is the event being raised. There
is a convention and for readability’s sake it should be
followed.
The last thing to do in this example is to add some
code to actually do something in the handler. Now remember that two
buttons are using the same handler. So, first you have to determine
which button raises the
event, and then you can call the action that should be performed.
In this example, you can just output some text to a label control
on the form. Drag a label control from the toolbox onto the form
and name it labelInfo. Then write the
following code on the Button_Click
method:
Notice that because the sender parameter is sent as
object, you will have to cast it to whatever object is raising the
event, in this case Button. In this
example, you use the Name property to
determine what button raised the event; however, you can also use
another property. The Tag property is
handy to use in this scenario, because it can contain anything that
you want to place in it. To see how the multicast capability of the
event delegate works, add another method to the Click event of buttonTwo.
The constructor of the form should look something like this
now:
If you let Visual Studio create the stub for you,
you will have the following method at the end of the source file.
However, you have to add the call to the MessageBox.Show() function.
If you go back and make use of anonymous methods,
the methods Button_Click and
Button2_Click would not be needed. The
code for the events would like this:
When you run this example, clicking buttonOne will change the text in the label.
Clicking buttonTwo will not only change
the text but also display the MessageBox. Again the important thing to remember is
that there is no guarantee that the label text changes before the
MessageBox appears, so be careful not to
write dependent code in the handlers.
You might have had to learn a lot of concepts to
get this far, but the amount of coding you need to do in the
receiver is fairly trivial. Also bear in mind that you will find
yourself writing event receivers a lot more often than you write
event senders. At least in the field of the Windows user interface,
Microsoft has already written all the event senders you are likely
to need (these are in the .NET base classes, in the Windows.Forms namespace).
Generating Events
Receiving events and responding to them is
only one side of the story. For events to be really useful, you
need the ability to generate them and raise them in your code. The
example in this section looks at creating, raising, receiving, and
optionally canceling an event.
The example has a form raise an event that will be
listened to by another class. When the event is raised, the
receiving object will determine if the process should execute and
then cancel the event if the process cannot continue. The goal in
this case is to determine whether the number of seconds of the
current time is greater than or less than 30. If the number of
seconds is less than 30, a property is set with a string that
represents the current time; if the number of seconds is greater
than 30, the event is canceled and the time string is set to an
empty string.
The form used to generate the event has a button
and a label on it. In the example code to download the button is
named buttonRaise and the label is
labelInfo; however, you can use any name
you want for your labels. After you have created the form and added
the two controls, you will be able to create the event and the
corresponding delegate. Add the following code in the class
declaration section of the form class:
So, what exactly is going on with these two lines
of code? First, you are declaring a new delegate type of
ActionEventHandler. The reason that you
have to create a new one and not use one of the predefined
delegates in the .NET Framework is that there will be a custom
EventArgs class used. Remember the
method signature must match the delegate. So, you now have a
delegate to use; the next line actually defines the event. In this
case the Action event is defined, and
the syntax for defining the event requires that you specify the
delegate that will be associated with the event. You can also use a
delegate that is defined in the .NET Framework. Nearly 100 classes
are derived from the EventArgs class, so
you might find one that works for you. Again, because a custom
EventArgs class is used in this example,
a new delegate type has to be created that matches it.
The new EventArgs-based
class, ActionCancelEventArgs, is
actually derived from CancelEventArgs,
which is derived from EventArgs.
CancelEventArgs and adds the
Cancel property. Cancel is a Boolean that informs the sender object
that the receiver wants to cancel or stop the event processing. In
the ActionCancelEventArgs class a
Message property has been added. This is
a string property that will contain textual information on the
processing state of the event. Here is the code for the
ActionCancelEventArgs class:
You can see that all an EventArgs-based class does is carry information
about an event to and from the sender and receiver. Most times the
information used from the EventArgs
class will be used by the receiver object in the event handler.
However, sometimes the event handler can add information into the
EventArgs class and it will be available
to the sender. This is how the example uses the EventArgs class. Notice that a couple of
constructors are available in the EventArgs class. This extra flexibility adds to the
usability of the class by others.
At this point, an event has been declared, the
delegate has been defined, and the EventArgs class has been created. The next thing
that has to happen is that the event needs to be raised. The only
thing that you really need to do is make a call to the event with
the proper parameters as shown in this example:
Simple enough. Create the new ActionCancelEventArgs class and pass it in as one of
the parameters to the event. However, there is one small problem.
What if the event hasn’t been used anywhere yet? What if an event
handler has not yet been defined for the event? The Action event would actually be null. If you tried to
raise the event, you would get a null reference exception. If you
wanted to derive a new form class and use the form that has the
Action event defined as the base, you
would have to do something else whenever the Action event is raised. Currently, you would have to
enable another event handler in the derived form in order to get
access to it. To make this process a little easier and to catch the
null reference error you have to create a method with the name
OnEventName where EventName is the name of the event. The example has
a method named OnAction(). Here is the
complete code for the OnAction()
method:
Not much to it but it does accomplish what is
needed. By making the method protected, only derived classes have
access to it. You can also see that the event is tested against
null before it is raised. If you were to derive a new class that
contains this method and event, you would have to override the
OnAction method and then you would be
hooked into the event. To do this, you would have to call
base.OnAction() in the override.
Otherwise, the event would not be raised. This naming convention is
used throughout the .NET Framework and is documented in the .NET
SDK documentation.
Notice the two parameters that are passed into the
OnAction method. They should look
familiar to you because they are the same parameters that will need
to be passed to the event. If the event needed to be raised from
another object other than the one that the method is defined in,
you would need to make the accessor internal or public and not
protected. Sometimes it makes sense to have a class that consists
of nothing but event declarations and that these events are called
from other classes. You would still want to create the OnEventName methods. However, in that case they
might be static methods.
So, now that the event has been raised, something
needs to handle it. Create a new class in the project and call it
BusEntity. Remember that the goal of
this project is to check the seconds property of the current time,
and if it is less than 30, set a string value to the time, and if
it is greater than 30, set the string to :: and cancel the event. Here is the code:
In the constructor, the handler for the
Form1.Action
event is declared. Notice that the syntax is very similar to the
Click event that you registered earlier.
Because you used the same pattern for declaring the event, the
usage syntax stays consistent as well. Something else worth
mentioning at this point is how you were able to get a reference to
the Action event without having a
reference to Form1 in the BusEntity class. Remember that in the Form1 class the Action
event is declared static. This isn’t a requirement, but it does
make it easier to create the handler. You could have declared the
event public, but then an instance of Form1 would need to be referenced.
When you coded the event in the constructor, you
called the method that was added to the delegate list Form1_Action, in keeping with the naming standards.
In the handler a decision on whether or not to cancel the event
needs to be made. The DoActions method
returns a Boolean value based on the time criteria described
earlier. DoAction also sets the
time string to the proper value.
Next, the DoActions
return value is set to the ActionCancelEventArgs Cancel property. Remember that
EventArg classes generally do not do
anything other than carry values to and from the event senders and
receivers. If the event is canceled (e.Cancel =
true), the Message property is
also set with a string value that describes why the event was
canceled.
Now if you look at the code in the buttonRaise_Click event handler again you will be
able to see how the Cancel property is
used:
Note that the ActionCancelEventArgs object is created. Next, the
event Action is raised, passing in the
newly created ActionCancelEventArgs
object. When the OnAction method is
called and the event is raised, the code in the Action event handler in the BusEntity object is executed. If there were other
objects that had registered for the Action event, they too would execute. Something to
keep in mind is that if there were other objects handling this
event, they would all see the same ActionCancelEventArgs object. If you needed to keep
up with which object canceled the event and whether more than one
object canceled the event, you would need some type of list-based
data structure in the ActionCancelEventArgs class.
After the handlers that have been registered with
the event delegate have been executed, you can query the
ActionCancelEventArgs object to see if
it has been canceled. If it has been canceled, lblInfo will contain the Message property value. If the event has not been
canceled, the lblInfo will show the
current time.
This should give you a basic idea of how you
can utilize events and the EventArgs-based object in the event to pass
information around in your applications.