Events


An event is a message or notification sent by a class to signal the occurrence of an action or a change in its state. The occurrence or change in state could be initiated by a user interface action, such as when a user clicks a button, or caused by some other program logic, such as when a method finishes reading records from a database. The class that raises the event (sends the notification) is called the event source or sender , and the class that receives the event is called the event sink or receiver . Event-based architecture uses the publish-subscribe model in which the source (or publisher) of an event allows its users (or subscribers) to specify logic that is executed when the event occurs.

In general, the event source does not know its subscribers ahead of time and does not know the logic that its subscribers would want to implement. Event-based programming thus requires an intermediary mechanism that connects the source and the receiver. In programming terms, event architecture needs some sort of callback mechanism. In C++, callbacks are implemented using function pointers; in Java, they are implemented using interfaces. The .NET Framework provides a new construct ”called a delegate ”to provide the functionality of a callback.

Delegates combine the best of the earlier paradigms . A delegate has the granularity of a function pointer and the type safety of an interface. In effect, a delegate is equivalent to a type-safe function pointer. And that is not all. A delegate also holds a linked list of other delegates, which is very useful for multicasting events (sending an event to multiple subscribers). Let's take at look at the delegate construct next and then examine how delegates are used in the event architecture of the .NET Framework.

Delegates

A delegate in the .NET Framework is a class that can hold a reference to a static or instance method of a class. The signature of the method must match the signature of the delegate. To understand this, let's begin with a declaration of a delegate class. The following line of code contains the declaration of a delegate named MyDelegate that can refer (bind) to only those methods that have a void return type and take one argument of type integer:

 publicdelegatevoidMyDelegate(intnumber); 

Before we see how to bind a delegate to a method, let's look at the delegate declaration more closely. A delegate declaration is similar to a method declaration, with the addition of the delegate keyword. However, a delegate is not a method ”it is a class with its own methods and other members . You do not provide the implementation for a delegate class; the delegate declaration causes the compiler to generate a class that derives from System.MulticastDelegate . For example, using our earlier declaration, the compiler generates a MyDelegate class that derives from System.MulticastDelegate . If you want to see the compiler-generated class, you can use the ILDASM disassembler tool that ships with the .NET Framework SDK. The tool is briefly described in the Introduction of this book.

System.MulticastDelegate is the base class for delegates in the .NET Framework. MulticastDelegate derives from the System.Delegate class. Delegate types in the .NET Framework class library do not derive directly from Delegate ” they derive from MulticastDelegate , which inherits from Delegate the functionality to bind a delegate to a method. MulticastDelegate also contains a linked list of delegates ”called the invocation list ”from which it can add or and remove delegates. When an instance of MulticastDelegate is invoked, it sequentially invokes the delegates in its invocation list.

You should not define a delegate by deriving from MulticastDelegate yourself. Only language compilers and the runtime are intended to generate types that derive from MulticastDelegate. To define a delegate in C# or Visual Basic, specify the delegate keyword and provide a class name and signature, as shown in our definition of MyDelegate .

To see delegates in action, let's look at an example. In the code, we'll define a delegate, a class that exposes a property of the delegate type, and a class that binds the delegate to a method. Let's look at the code first and then discuss what the sample does. The delegate- related code is boldface in Listing 3-1:

Listing 3-1 A delegate used as a callback
 usingSystem; namespaceMSPress.DelegateSample{  publicdelegatevoidPrintCallback(intnumber);  publicclassPrinter{ privatePrintCallback_print;  publicPrintCallbackPrintCallback  { get{ return_print; } set{ _print=value; } } } publicclassDriver{ privatevoidPrintInteger(intnumber){ Console.WriteLine("FromPrintInteger:Thenumberis{0}.",number); } staticvoidMain(string[]args){ Driverdriver=newDriver(); Printerprinter=newPrinter();  printer.PrintCallback=newPrintCallback(driver.PrintInteger); printer.PrintCallback(10); printer.PrintCallback(100);  Console.WriteLine("PressEntertoexit..."); Console.ReadLine(); } } } 

If you compile and execute the sample, the output should look like this:

 FromPrintInteger:Thenumberis10. FromPrintInteger:Thenumberis100. PressEntertoexit... 

Here's what the sample does: It defines a delegate named PrintCallback that has a return type void and accepts a single parameter of type integer . The Printer class has a delegate property, PrintCallback , of type PrintCallback . The Driver class defines a PrintInteger method that has the same signature as PrintCallback . In its Main method, the Driver class binds the PrintCallback delegate of its Printer instance to its PrintInteger method. Now, whenever the PrintCallback delegate is invoked, the method it binds to ” PrintInteger ” is executed.

We would like to make one minor point about the delegate constructor. If you look at the constructor of the System.MulticastDelegate class, you will see that it takes two parameters. But we have supplied only one parameter in our delegate constructor: new PrintCallback(driver.PrintInteger) . The reason for this difference is that the other parameter is implicit and points to the object that contains the callback method. The C# and Visual Basic .NET compilers use the implicit parameter and the supplied parameter to create the two-parameter constructor the runtime needs. The two-parameter constructor is intended for compilers and other tools. You should always use the one-parameter constructor in your code, as we showed in the sample. If the method you are binding to is a static method (instead of an instance method), use the class name instead of the class instance name in the constructor. For example, if the PrintInteger method were a static method, you would instantiate the delegate as new PrintCallback(Driver.PrintInteger) .

That completes our quick tour of delegates. Delegates have more features than we have covered, but the background we've provided should be adequate for using them in event programming.

Event Delegates

Now that you have seen how delegates work, it is easy to understand how they are used in event programming. In essence, a class that wants to raise an event (send notifications) has a delegate member. A class that wants to receive the event provides a method that performs some logic in response to the event (an event-handler method). The receiver ”or some other class ”then binds the event handler to a delegate and adds that delegate to the invocation list of the event source. To raise an event, the event source invokes its delegate, which in turn invokes the delegates in its invocation list. Those delegates in turn invoke the handlers they are bound to, thus completing the event sequence. The .NET Framework event model is elegant, powerful, and easy to implement. We'll soon translate this high-level overview into concrete implementation details, but first let's take a look at event delegates, which are at the core of this event model.

Event delegates in the .NET Framework follow a certain convention for their signature and their naming scheme. This convention is relied upon by visual design tools and provides a consistent pattern for client code. To understand the convention, let's look at a commonly accessed event delegate in the .NET Framework, System.EventHandler :

 publicdelegatevoidEventHandler(objectsender,EventArgse); 

The signature of an event delegate in the .NET Framework is analogous to that of the EventHandler delegate. These are the specifics of the signature convention:

  • The return type of an event delegate is void .

  • An event delegate takes two arguments. The first argument ”of type Object ” represents the sender of the event. The second argument represents data for the event and is an instance of a class that derives from System.EventArgs .

To interoperate with the .NET Framework and for your component to work in a visual designer, you must follow the signature convention for any event delegates that you define. Notice that the signature convention is not limiting; you can include any data that you want to provide for your event in the event data class.

Classes that hold event data go hand in hand with event delegates. The base class EventArgs does not hold any event data. Its corresponding event delegate, EventHandler , is used for events that do not have associated data, such as the Click event of the System.Web.UI.WebControls.Button server control. An example of a class that holds event data is System.Web.UI.ImageClickEventArgs , which holds x and y coordinates for the Click event of the System.Web.UI.WebControls.ImageButton server control. Its associated event delegate is System.Web.UI.ImageClickEventHandler . Many namespaces in the .NET Framework contain event data classes and event delegates.

You should follow the .NET Framework convention for naming event data classes and event delegates. Event data classes are given the name of the event, appended by the suffix EventArgs , such as MonthChangedEventArgs . Event delegates are given names that consist of the event name, appended by the suffix EventHandler , such as MonthChangedEventHandler . Event delegates are named event handlers because they bind to methods that handle events.

Wiring Events

For completeness, we'll briefly describe the syntax for attaching an event handler to an event. (In this book, we'll focus on showing you how to raise events from your components and assume that you are familiar with handling events in ASP.NET pages or in other .NET applications.)

The process of associating an event handler with an event (adding a delegate to the invocation list) is called event wiring , while that of removing an event handler from an event (removing a delegate from the invocation list) is called event unwiring . In C#, the syntax for wiring and unwiring an event handler to an event looks like this:

 button.Click+=newEventHandler(this.Button_Clicked); button.Click-=newEventHandler(this.Button_Clicked); 

In the preceding code fragment, button is an instance of the Button control and is created in a class that has a Button_Clicked method that handles the button's Click event.

In Visual Basic .NET, event wiring and unwiring syntax looks like this:

 AddHandlerbutton.Click,AddressOfMe.Button_Clicked RemoveHandlerbutton.Click,AddressOfMe.Button_Clicked 

The declarative syntax of ASP.NET pages hides much of the event architecture from the page developer. However, the underlying event mechanism in ASP.NET is no different from the rest of the .NET Framework. We'll discuss events in ASP.NET server controls in detail in Chapter 9, "Control Life Cycle, Events, and Postback."

Raising an Event

In this section, we'll walk you through the .NET Framework design pattern for raising an event from your class. In the next section, we'll show a complete sample that demonstrates these steps. The sample files contain the sample in C# and in Visual Basic .NET.

To implement an event in your class, you need a class for event data, an event delegate, a delegate member in your class that holds the invocation list, and a method that sends the event notification. The actual implementation of an event is relatively straightforward ”the major effort is in understanding how the pieces fit together. Here's a high-level overview of the steps that you have to perform. We'll elaborate on these steps in the next section.

  1. If your class does not have any associated event data, use the Event ­Args class for event data. Or you can use another preexisting event data class if it matches your event. If a suitable event data class does not exist, define a class to hold event data. This class must derive from System.EventArgs and, by convention, its name should be the name of the event appended by EventArgs ” such as AdCreatedEvent ­Args , CommandEventArgs , or MonthChangedEventArgs . The following code fragment declares an event data class:

     publicclassLowChargeEventArgs:EventArgs{...} 
  2. If your event does not have associated data and you used EventArgs in step 1, use System.EventHandler as your event delegate. Or you can use another preexisting delegate if it matches your event. If a suitable event delegate does not exist, define an event delegate whose second argument has the type of the event data class from step 1. By convention, the name of the event delegate is the name of the event appended by EventHandler ” such as AdCreatedEvent ­Handler , CommandEventHandler , or MonthChangedEventHandler . The following code defines an event delegate:

     publicdelegatevoidLowChargeEventHandler(objectsender, LowChargeEventArgse); 
  3. In your class, define an event member using the event keyword. Give the event member the name of your event. The type of this member is the event delegate you used in step 2. Here's an example:

     publiceventLowChargeEventHandlerLowCharge; 

    The event member holds the list of delegates that subscribe to the event. When this member is invoked, it dispatches the event by invoking the delegates.

  4. In your class, define a virtual (overridable) method that invokes the event delegate ”after checking whether any event listeners exist. The name of this method contains the event name prefixed by On . Here's an example:

     protectedvirtualvoidOnLowCharge(LowChargeEventArgse){ if(LowCharge!=null){ LowCharge(this,e); } } 

    The purpose of the On<EventName> method is to allow classes that derive from your class to handle the event without attaching an event handler to themselves .

Note that the first two steps in the list describe classes that generally exist outside your class (defined by you, or in the .NET Framework or third party class library), while the next two steps are implemented in your class. Here's the skeleton of a class that implements the LowCharge event we just described:

 publicclassBattery{   publiceventLowChargeEventHandlerLowCharge; protectedvirtualvoidOnLowCharge(LowChargeEventArgse)  { if(LowCharge!=null){ LowCharge(this,e); } } } 

As you can see, you have to write very little code to implement an event. When the C# or Visual Basic .NET compiler sees a member marked with the event keyword, it automatically generates the three members shown in the following code. (You can see these members by using the ILDASM tool described in the Introduction of this book.)

  • A private field of the same type as the event delegate, such as the ­following:

     privateLowChargeEventHandlerLowCharge=null; 
  • A method that adds delegates to the event delegate, such as the ­following:

     //Theaccesslevelofthismethodisthesameasthatof //theeventmember. publicvoidAdd_LowCharge(LowChargeEventHandlerhandler){ LowCharge= (LowChargeEventHandler)Delegate.Combine(LowCharge,handler); } 
  • A method that removes delegates from the event delegate, such as the following:

     //Theaccesslevelofthismethodisthesameasthatof //theeventmember. publicvoidRemove_LowCharge(LowChargeEventHandlerhandler){ LowCharge= (LowChargeEventHandler)Delegate.Remove(LowCharge,handler); } 

The two methods generated by the compiler are event accessors that enable a user to attach or remove event handlers to or from your event. The access level of the compiler-generated methods is the same as the access level of the event member. However, even when the compiler-generated methods are public, they are not directly accessible from client code in C# and Visual Basic .NET. A user must wire and unwire handlers using the += and -= syntax in C# and the AddHandler and RemoveHandler syntax in Visual Basic .NET, as we showed earlier.

When a class raises a large number of events, it might not be efficient to allocate one delegate field per event, especially when only a few of the events are commonly handled. This is because each delegate field contributes memory overhead to the class instance, regardless of whether any event handlers are wired to the associated event. For such situations, the .NET Framework provides a utility class, System.ComponentModel.EventHandlerList , which provides a more optimal storage mechanism for event delegates. In Chapter 9, we will describe how to use this utility class to implement step 3 in the numbered list at the beginning of this section.

Event Sample

In this sample, we'll put together the steps we described in the preceding discussion. We'll define a Battery class that raises two events ” LowCharge and Depleted . Later we'll complete the sample with a LapTop class that contains a Battery instance and provides event-handling methods that are wired to the Battery instance.

Here are the event-related features of the Battery class:

  • The Battery class defines event members named LowCharge and Depleted .

  • The LowCharge event uses the LowChargeEventArgs class for event data and the LowChargeEventHandler class as the event delegate. The LowChargeEventArgs and LowChargeEventHandler classes are also defined in the sample. The event data in LowChargeEventArgs represents the charge level of the battery.

  • The Depleted event does not carry any data and uses the EventArgs class for event data and the EventHandler class as the event delegate.

  • The Battery class defines the OnLowCharge and OnDepleted methods, which raise the LowCharge and Depleted events.

The Battery class raises both its events in the set accessor of its CurrentLevel property by invoking the OnLowCharge and the OnDepleted methods.

Listings 3-2, 3-3, and 3-4 show the complete code for the sample. The event-related constructs are boldface in the code listings.

Listing 3-2 LowChargeEventArgs.cs
 usingSystem; usingSystem.ComponentModel; namespaceMSPress.EventSample{ //ClassthatholdsdatafortheLowChargeevent.  publicclassLowChargeEventArgs:EventArgs  { privatedouble_level; publicLowChargeEventArgs(doublebatteryLevel){ _level=batteryLevel; } publicdoubleLevel{ get{ return_level; } } } } 
Listing 3-3 LowChargeEventHandler.cs
 namespaceMSPress.EventSample{ //DelegatefortheLowChargeevent.  publicdelegatevoidLowChargeEventHandler(objectsender, LowChargeEventArgse);  } 
Listing 3-4 Battery.cs
 usingSystem; usingSystem.ComponentModel; namespaceMSPress.EventSample{ //Classthatraisesevents. publicclassBattery{ privateconstdoubleDepletedLevel=0.05; privatedouble_currentLevel; privatedouble_minimumLevel; //Eventmembers.  publiceventLowChargeEventHandlerLowCharge; publiceventEventHandlerDepleted;  publicBattery():this(1.0,0.15){} publicBattery(doublecurrentLevel,doubleminimumLevel){ _currentLevel=currentLevel; _minimumLevel=minimumLevel; } publicdoubleCurrentLevel{ get{ return_currentLevel; } set{ if(value<0.0value>1.0){ thrownewArgumentOutOfRangeException("value", value,  "CurrentLevelmustbebetween0.0and1.0."); } _currentLevel=value; if(_currentLevel>DepletedLevel&& _currentLevel<MinimumLevel){ LowChargeEventArgse= 
 newLowChargeEventArgs(_currentLevel);  OnLowCharge(e);  } elseif(_currentLevel>0.0&& _currentLevel<=DepletedLevel){  OnDepleted(EventArgs.Empty);  } } } publicdoubleMinimumLevel{ get{ return_minimumLevel; } set{ if(value<=DepletedLevelvalue>0.5){ thrownewArgumentOutOfRangeException("value", value,String.Format("MinimumLevelmustbe " +  "greaterthan{0:F2}andlessthan0.5.", DepletedLevel));} _minimumLevel=value; } } //RaisestheLowChargeevent.  protectedvirtualvoidOnLowCharge(LowChargeEventArgse)  { if(LowCharge!=null){ LowCharge(this,e); } } //RaisestheDepletedevent.  protectedvirtualvoidOnDepleted(EventArgse)  { if(Depleted!=null){ Depleted(this,e); } } } } 

The following example performs a simulation that drains the battery in a laptop. The battery raises events in response to changes in its charge level. Those events are handled by the laptop. The example defines a class named LapTop , which contains a Battery instance and defines handlers that perform logic in response to the events raised by the Battery class. LapTop wires its event handlers to the events of its Battery instance using event delegates. Notice that the Battery class (shown in Listing 3-4) does not implement any logic to handle its LowCharge or Depleted events. The logic to handle those events is supplied by the LapTop class in its event handlers: Battery_LowCharge and Battery_Depleted . The event handlers and the event wiring code are boldface in the LapTop source code, shown in Listing 3-5.

Listing 3-5 LapTop.cs
 usingSystem; usingSystem.ComponentModel; usingSystem.Threading; namespaceMSPress.EventSample{ publicclassLapTop{ privateBattery_battery; publicLapTop(){ _battery=newBattery();  _battery.LowCharge+= newLowChargeEventHandler(this.Battery_LowCharge); _battery.Depleted+= newEventHandler(this.Battery_Depleted);  }  privatevoidBattery_LowCharge(objectsender, LowChargeEventArgse)  { Console.WriteLine("Batteryislow,{0:F2}percentcharged.", e.Level*100.0); Console.WriteLine("Saveyourwork!!"); }  privatevoidBattery_Depleted(objectsender,EventArgse)  { Console.WriteLine("Batteryisoutofjuice!!!"); Console.WriteLine("Poweringdownin30seconds..."); } publicvoidStart(){ do{ doublenewLevel=_battery.CurrentLevel-0.05; if(newLevel<0.0) _battery.CurrentLevel=0.0; else _battery.CurrentLevel=newLevel; Thread.Sleep(50); }while(_battery.CurrentLevel>0.0); } 
 publicstaticvoidMain(){ LapToplapTop=newLapTop(); lapTop.Start(); Console.WriteLine("PressEntertoexit..."); Console.ReadLine(); } } } 

To build the sample, execute the following command from the directory containing the sample code:

 csc/out:EventSample.exe/r:System.dllBattery.csLapTop.cs LowChargeEventArgs.csLowChargeEventHandler.cs 

When you run EventSample.exe, you should see output similar to the following listing:

 Batteryislow,15.00percentcharged. Saveyourwork!! Batteryislow,10.00percentcharged. Saveyourwork!! Batteryisoutofjuice!!! Poweringdownin30seconds... PressEntertoexit... 

For this sample, we intentionally chose a scenario that does not involve ASP.NET controls or, for that matter, any user interface classes because we wanted to emphasize that the main elements of the .NET Framework event pattern are the same across the .NET Framework. In Chapter 9, where we describe events in ASP.NET server controls, you'll see that the underlying event pattern in server controls is the same as in the sample described here.



Developing Microsoft ASP. NET Server Controls and Components
Developing Microsoft ASP.NET Server Controls and Components (Pro-Developer)
ISBN: 0735615829
EAN: 2147483647
Year: 2005
Pages: 183

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