FREE BOOK

Chapter 5: Advanced C# Class Construction Techniques

Posted by Apress Free Book | C# Language January 13, 2009
This chapter rounds out your introduction to the core aspects of the C# language by examining a number of advanced (but extremely useful) syntactic constructs. To begin, you learn how to construct and use an indexer method.

Understanding (and Using) Events

Delegates are fairly interesting constructs because you can resolve the name of a function to call at runtime, rather than compile time. Admittedly, this syntactic orchestration can take a bit of getting used to. However, because the ability for one object to call back to another object is such a helpful construct, C# provides the "event" keyword to lessen the burden of using delegates in the raw.

The most prevalent use of the event keyword would be found in GUI-based applications, in which Button, TextBox, and Calendar widgets all report back to the containing Form when a given action (such as clicking a Button) has occurred. However, events are not limited to GUI-based applications. Indeed, they can be quite helpful when creating "non-GUI" based projects (as you will now see).

Recall that the current implementation of Car.SpeedUp() (see Chapter 3) throws an exception if the user attempts to increase the speed of an automobile that has already been destroyed. This is a rather brute force way to deal with the problem, given that the exception has the potential to halt the program's execution if the error is not handled in an elegant manner. A better design would be to simply inform the object user when the car has died using a custom event, and allow the caller to act accordingly.

Let's reconfigure the Car to send two events to those who happen to be listening. The first event (AboutToBlow) will be sent when the current speed is 10 miles below the maximum speed. The second event (Exploded) will be sent when the user attempts to speed up a car that is already dead. Establishing an event is a two-step process. First, you need to define a delegate, which as you recall represents a pointer to the method(s) to call when the event is sent. Next, you define the events themselves using the "event" keyword. Here is the updated Car class (also notice that I have added a private Boolean to represent the state of the car):

// This car can 'talk back' to the user.
public class Car
{
// Is the car alive or dead?
private bool dead;
// Holds the function(s) to call when the event occurs.
public delegate void EngineHandler(string msg);
// This car can send these events.
public static event EngineHandler Exploded Exploded;
public static event EngineHandler AboutToBlow AboutToBlow;
}

Firing an event (i.e., sending the event to those who happen to be listening) is as simple as specifying the event by name and sending out any specified parameters. To illustrate, update the previous implementation of SpeedUp() to send each event accordingly (and remove the previous exception logic):

// Fire the correct event based on our current state of affairs.
public void SpeedUp(int delta)
{
// If the car is dead, send exploded event.
if(dead)
{
if(Exploded != null)
Exploded Exploded("Sorry, this car is dead...");
}
else
{
currSpeed += delta;
// Almost dead? Send about to blow ev event. ent.
if(10 = = maxSpeed - currSpeed)
if(AboutToBlow != null)
AboutToBlow AboutToBlow("Careful, approaching terminal speed!");
// Still OK! Proceed as usual.
if(currSpeed >= maxSpeed)
dead = true;
else
Console.WriteLine("\tCurrSpeed = {0}", currSpeed);
}
}

With this, you have configured the car to send two custom events (under the correct conditions). You will see the usage of this new automobile in just a moment, but first, let's check the event architecture in a bit more detail.

Events Under the Hood

A given event actually expands into two hidden public functions, one having an "add_" prefix, the other having a "remove_" prefix. For example, the Exploded event expands to the following methods:

// The following event expands to:
// add_Exploded()
// remove_Exploded()
//
public static event EngineHandler Exploded Exploded;

In addition to defining hidden add_XXX() and remove_XXX() methods, each event also actually maps to a private static class, which associates the corresponding delegate to a given event. In this way, when an event is raised, each method maintained by the delegate will be called. This is a convenient way to allow an object to broadcast the event to multiple "event sinks."

To illustrate, check out Figure 5-8, a screenshot of the Car type as seen through the eyes of ILDasm.exe.



Figure 5-8. Events under the hood

As you can see, each event (Exploded and AboutToBlow) is internally represented as the following members:   

  • A private static class 

  • An add_XXX() method
  • A remove_XXX() method

If you were to check out the IL instructions behind add_AboutToBlow(), you would find the following (note the call to Delegate.Combine() is handled on your behalf):
 

.method public hidebysig specialname static
void
add_AboutToBlow(class CarEvents.Car/EngineHandler 'value') cil managed synchronized
{
// Code size 22 (0x16)
.maxstack 8
IL_0000: ldsfld class CarEvents.Car/EngineHandler
CarEvents.Car::AboutToBlow
IL_0005: ldarg.0
IL_0006: call class [mscorlib]System.Delegate
[mscorlib]System.Delegate::Combine Combine(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_000b: castclass CarEvents.Car/EngineHandler
IL_0010: stsfld class CarEvents.Car/EngineHandler
CarEvents.Car::AboutToBlow
IL_0015: ret
} // end of method Car::add_AboutToBlow

As you would expect, remove_AboutToBlow() will make the call to Delegate.Remove() automatically:

.method public hidebysig specialname static
void
remove_AboutToBlow AboutToBlow(class CarEvents.Car/EngineHandler 'value')cil managed
synchronized
{
// Code size 22 (0x16)
.maxstack 8
IL_0000: ldsfld class CarEvents.Car/EngineHandler
CarEvents.Car::AboutToBlow
IL_0005: ldarg.0
IL_0006: call class [mscorlib]System.Delegate
[mscorlib]System.Delegate::Remove Remove(class [mscorlib]System.Delegate,
class [mscorlib]System.Delegate)
IL_000b: castclass CarEvents.Car/EngineHandler
IL_0010: stsfld class CarEvents.Car/EngineHandler
CarEvents.Car::AboutToBlow
IL_0015: ret
} // end of method Car::remove_AboutToBlow

The IL instructions for the event itself make use of the [.addon] and [.removeon] tags to establish the correct add_XXX and remove_XXX methods (also note the static private class is mentioned by name):

.event nt CarEvents.Car/EngineHandler AboutToBlow
{
.addon void CarEvents.Car::add_AboutToBlow AboutToBlow(class CarEvents.Car/EngineHandler)
.removeon
void CarEvents.Car::remove_AboutToBlow AboutToBlow(class
CarEvents.Car/EngineHandler)
} // end of event Car::AboutToBlow
 
So, now that you understand how to build a class that can send events, the next big question is how you can configure an object to receive these events.

Total Pages : 11 678910

comments