Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
111
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

274 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

Figure 8-6. Passing the buck

Analyzing the Delegation Code

The Main() method begins by creating an instance of the Garage and ServiceDepartment types. Now, when you write the following:

// Wash all dirty cars.

g.ProcessCars(new Car.CarDelegate(sd.WashCar));

what you are effectively saying is “Add a pointer to the ServiceDepartment.WashCar() method to a Car.CarDelegate object, and pass this object to Garage.ProcessCars().” Like most real-world garages, the real work is delegated to the service department (which explains why a 30-minute oil change takes 2 hours). Given this, ProcessCars() can be understood as

// CarDelegate points to the ServiceDepartment.WashCar function. public void ProcessCars(Car.CarDelegate proc)

{

...

foreach(Car c in theCars)

proc(c); // proc(c) => ServiceDepartment.WashCar(c)

}

Likewise, if you say the following:

// Rotate the tires.

g.ProcessCars(new Car.CarDelegate(sd.RotateTires));

then ProcessCars() can be understood as

// CarDelegate points to the ServiceDepartment.RotateTires function: public void ProcessCars(Car.CarDelegate proc)

{

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

275

foreach(Car c in theCars)

proc(c); // proc(c) => ServiceDepartment.RotateTires(c)

...

}

Source Code The CarGarage project is located under the Chapter 8 subdirectory.

Understanding Delegate Covariance

Hopefully at this point in the game, you are more comfortable with the creation and use of delegate types. Before turning our attention to the C# event syntax, let’s examine a new delegate-centric feature provided by .NET 2.0 termed covariance. As you may have noticed, each of the delegates created thus far point to methods returning simple numerical data types (or void). However, assume you are designing a delegate that can point to methods returning a custom class type:

// Define a deletate pointing to targets returning Car types. public delegate Car ObtainCarDelegate();

Of course, you would be able to define a target for the delegate as expected:

class Program

{

public delegate Car ObtainCarDelegate();

public static Car GetBasicCar() { return new Car(); }

static void Main(string[] args)

{

ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar); Car c = targetA();

Console.ReadLine();

}

}

So far, so good. However, what if you were to derive a new class from the Car type named SportsCar and wish to create a delegate type that can point to methods returning this class type? Prior to .NET 2.0, you would be required to define an entirely new delegate to do so:

// A new deletate pointing to targets returning SportsCar types. public delegate SportsCar ObtainSportsCarDelegate();

As we now have two delegate types, we now must create an instance of each to obtain Car and

SportsCar types:

class Program

{

public delegate Car ObtainCarDelegate();

public delegate SportsCar ObtainSportsCarDelegate();

public static Car GetBasicCar() { return new Car(); }

public static SportsCar GetSportsCar() { return new SportsCar(); }

276 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

static void Main(string[] args)

{

ObtainCarDelegate targetA = new ObtainCarDelegate(GetBasicCar); Car c = targetA();

ObtainSportsCarDelegate targetB =

new ObtainSportsCarDelegate(GetSportsCar); SportsCar sc = targetB();

Console.ReadLine();

}

}

Given the laws of classic inheritance, it would be ideal to build a single delegate type that can point to methods returning either Car or SportsCar types (after all, a SportsCar “is-a” Car). Covariance allows for this very possibility. Simply put, covariance allows you to build a single delegate that can point to methods returning class types related by classical inheritance:

class Program

{

//Define a single deletate that may return a Car

//or SportsCar.

public delegate Car ObtainVehicalDelegate();

public static Car GetBasicCar() { return new Car(); }

public static SportsCar GetSportsCar() { return new SportsCar(); }

static void Main(string[] args)

{

Console.WriteLine("***** Delegate Covariance *****\n");

ObtainVehicalDelegate targetA = new ObtainVehicalDelegate(GetBasicCar); Car c = targetA();

// Covariance allows this target assignment.

ObtainVehicalDelegate targetB = new ObtainVehicalDelegate(GetSportsCar); SportsCar sc = (SportsCar)targetB();

Console.ReadLine();

}

}

Notice that the ObtainVehicalDelegate delegate type has been defined to point to methods returning a strongly typed Car type. Given covariance, however, we can point to methods returning derived types as well. To obtain the derived type, simply perform an explicit cast.

Note In a similar vein, contravariance allows you to create a single delegate that can point to numerous methods that receive objects related by classical inheritance. Consult the .NET Framework 2.0 SDK Documentation for further details.

Source Code The DelegateCovariance project is located under the Chapter 8 subdirectory.

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

277

Understanding C# Events

Delegates are fairly interesting constructs in that they enable two objects in memory to engage in a two-way conversation. As you may agree, however, working with delegates in the raw does entail a good amount of boilerplate code (defining the delegate, declaring necessary member variables, and creating custom registration/unregistration methods).

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. When the compiler processes the event keyword, you are automatically provided with registration and unregistration methods as well as any necessary member variable for your delegate types. In this light, the event keyword is little more than syntactic sugar, which can be used to save you some typing time.

Note Even if you choose to leverage the C# event keyword, you are still required to manually define the related delegate types.

Defining an event is a two-step process. First, you need to define a delegate that contains the methods to be called when the event is fired. Next, you declare the events (using the C# event keyword) in terms of the related delegate. In a nutshell, defining a type that can send events entails the following pattern (shown in pseudo-code):

public class SenderOfEvents

{

public delegate retval AssociatedDelegate(args); public event AssociatedDelegate NameOfEvent;

...

}

The events of the Car type will take the same name as the previous delegates (AboutToBlow and Exploded). The new delegate to which the events are associated will be called CarEventHandler. Here are the initial updates to the Car type:

public class Car

{

//This delegate works in conjunction with the

//Car's events.

public delegate void CarEventHandler(string msg);

// This car can send these events. public event CarEventHandler Exploded; public event CarEventHandler AboutToBlow;

...

}

Sending an event to the caller is as simple as specifying the event by name as well as any required parameters as defined by the associated delegate. To ensure that the caller has indeed registered with event, you will want to check the event against a null value before invoking the delegate’s method set. These things being said, here is the new iteration of the Car’s Accelerate() method:

public void Accelerate(int delta)

{

// If the car is dead, fire Exploded event. if (carIsDead)

{

278 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

if (Exploded != null)

Exploded("Sorry, this car is dead...");

}

else

{

currSpeed += delta;

// Almost dead?

if (10 == maxSpeed - currSpeed && AboutToBlow != null)

{

AboutToBlow("Careful buddy! Gonna blow!");

}

// Still OK!

if (currSpeed >= maxSpeed) carIsDead = true;

else

Console.WriteLine("->CurrSpeed = {0}", currSpeed);

}

}

With this, you have configured the car to send two custom events without the need to define custom registration functions. 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 C# event actually expands into two hidden public methods, one having an add_ prefix; the other having a remove_ prefix. This prefix is followed by the name of the C# event. For example, the Exploded event results in two CIL methods named add_Exploded() and remove_Exploded(). In addition to the add_XXX() and remove_XXX() methods, the CIL-level event definition associates the correct delegate to a given event.

If you were to check out the CIL instructions behind add_AboutToBlow(), you would find code that looks just about identical to the OnAboutToBlow() helper method you wrote previously in the CarDelegate example (note the call to Delegate.Combine()):

.method public hidebysig specialname instance void add_AboutToBlow(class CarEvents.Car/CarEventHandler 'value') cil managed synchronized

{

.maxstack 8 ldarg.0 ldarg.0

ldfld class CarEvents.Car/CarEventHandler CarEvents.Car::AboutToBlow ldarg.1

call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Combine(

class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) castclass CarEvents.Car/CarEventHandler

stfld class CarEvents.Car/CarEventHandler CarEvents.Car::AboutToBlow ret

}

As you would expect, remove_AboutToBlow() will indirectly call Delegate.Remove() and is more or less identical to the previous RemoveAboutToBlow() helper method:

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

279

.method public hidebysig specialname instance void remove_AboutToBlow(class CarEvents.Car/CarEventHandler 'value') cil managed synchronized

{

.maxstack 8 ldarg.0 ldarg.0

ldfld class CarEvents.Car/CarEventHandler CarEvents.Car::AboutToBlow ldarg.1

call class [mscorlib]System.Delegate [mscorlib]System.Delegate::Remove(

class [mscorlib]System.Delegate, class [mscorlib]System.Delegate) castclass CarEvents.Car/CarEventHandler

stfld class CarEvents.Car/CarEventHandler CarEvents.Car::AboutToBlow ret

}

Finally, the CIL code representing the event itself makes use of the .addon and .removeon directives to map the names of the correct add_XXX() and remove_XXX() methods to invoke:

.event CarEvents.Car/EngineHandler AboutToBlow

{

.addon void CarEvents.Car::add_AboutToBlow (class CarEvents.Car/CarEngineHandler)

.removeon void CarEvents.Car::remove_AboutToBlow (class CarEvents.Car/CarEngineHandler)

}

Now that you understand how to build a class that can send C# events (and are aware that events are nothing more than a typing time-saver), the next big question is how to “listen to” the incoming events on the caller’s side.

Listening to Incoming Events

C# events also simplify the act of registering the caller-side event handlers. Rather than having to specify custom helper methods, the caller simply makes use of the += and -= operators directly (which triggers the correct add_XXX() or remove_XXX() method in the background). When you wish to register with an event, follow the pattern shown here:

//ObjectVariable.EventName +=

//new AssociatedDelegate(functionToCall);

Car.EngineHandler d = new Car.EngineHandler(CarExplodedEventHandler) myCar.Exploded += d;

When you wish to detach from a source of events, use the -= operator:

// ObjectVariable.EventName -= delegateObject; myCar.Exploded -= d;

Given these very predictable patterns, here is the refactored Main() method, now using the C# event registration syntax:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Events *****");

Car c1 = new Car("SlugBug", 100, 10);

280 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

// Register event handlers.

c1.AboutToBlow += new Car.CarEventHandler(CarIsAlmostDoomed); c1.AboutToBlow += new Car.CarEventHandler(CarAboutToBlow);

Car.CarEventHandler d = new Car.CarEventHandler(CarExploded); c1.Exploded += d;

Console.WriteLine("\n***** Speeding up *****");

for (int i = 0; i < 6; i++) c1.Accelerate(20);

//Remove CarExploded method

//from invocation list. c1.Exploded -= d;

Console.WriteLine("\n***** Speeding up *****");

for (int i = 0; i < 6; i++) c1.Accelerate(20);

Console.ReadLine();

}

public static void CarAboutToBlow(string msg) { Console.WriteLine(msg); }

public static void CarIsAlmostDoomed(string msg)

{Console.WriteLine("Critical Message from Car: {0}", msg); } public static void CarExploded(string msg)

{Console.WriteLine(msg); }

}

Source Code The CarEvents project is located under the Chapter 8 subdirectory.

Simplifying Event Registration Using Visual Studio 2005

Visual Studio .NET 2003 and Visual Studio 2005 each offer assistance with the process of registering event handlers. When you apply the += syntax during the act of event registration, you will find an IntelliSense window is displayed inviting you to hit the Tab key to auto-fill the associated delegate instance (see Figure 8-7).

Figure 8-7. Delegate selection IntelliSense

Once you do hit the Tab key, you are then invited to enter the name of the event handler to be generated (or simply accept the default name) as shown in Figure 8-8.

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

281

Figure 8-8. Delegate target format IntelliSense

Once you hit the Tab key again, you will be provided with stub code in the correct format of the delegate target (note that this method has been declared static due to the fact that the event was registered within a static method):

static void c1_AboutToBlow(string msg)

{

// Add your code!

}

This IntelliSense feature is available to all .NET events in the base class libraries. This IDE feature is a massive timesaver, given that this removes you from the act of needing to search the .NET help system to figure out the correct delegate to use with a particular event as well as the format of the delegate target.

A “Prim-and-Proper” Event

Truth be told, there is one final enhancement we could make to the CarEvents example that mirrors Microsoft’s recommended event pattern. As you begin to explore the events sent by a given type in the base class libraries, you will find that the first parameter of the underlying delegate is a System.Object, while the second parameter is a type deriving from System.EventArgs.

The System.Object argument represents a reference to the object that sent the event (such as the Car), while the second parameter represents information regarding the event at hand. The System.EventArgs base class represents an event that is not sending any custom information:

public class EventArgs

{

public static readonly System.EventArgs Empty; public EventArgs();

}

For simple events, you can pass an instance of EventArgs directly. However, when you wish to pass along custom data, you should build a suitable class deriving from EventArgs. For our example, assume we have a class named CarEventArgs, which maintains a string representing the message sent to the receiver:

public class CarEventArgs : EventArgs

{

public readonly string msg;

public CarEventArgs(string message)

{

msg = message;

}

}

282 C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

With this, we would now update the CarEventHandler delegate as follows (the events would be unchanged):

public class Car

{

public delegate void CarEventHandler(object sender, CarEventArgs e);

...

}

When firing our events from within the Accelerate() method, we would now need to supply a reference to the current Car (via the this keyword) and an instance of our CarEventArgs type:

public void Accelerate(int delta)

{

// If the car is dead, fire Exploded event. if (carIsDead)

{

if (Exploded != null)

Exploded(this, new CarEventArgs("Sorry, this car is dead..."));

}

else

{

...

AboutToBlow(this, new CarEventArgs("Careful buddy! Gonna blow!"));

}

...

}

On the caller’s side, all we would need to do is update our event handlers to receive the incoming parameters and obtain the message via our read-only field. For example:

public static void CarAboutToBlow(object sender, CarEventArgs e) { Console.WriteLine("{0} says: {1}", sender, e.msg); }

If the receiver wishes to interact with the object that sent the event, we can explicitly cast the System.Object. Thus, if we wish to power down the radio when the Car object is about to meet its maker, we could author an event handler looking something like the following:

public static void CarIsAlmostDoomed(object sender, CarEventArgs e)

{

//Just to be safe, perform a

//runtime check before casting. if (sender is Car)

{

Car c = (Car)sender; c.CrankTunes(false);

}

Console.WriteLine("Critical Message from {0}: {1}", sender, e.msg);

}

Source Code The PrimAndProperCarEvents project is located under the Chapter 8 subdirectory.

Understanding C# Anonymous Methods

To wrap up this chapter, let’s examine some final delegate-and-event-centric features of .NET 2.0 as seen through the eyes of C#. To begin, consider the fact that when a caller wishes to listen to incoming events, it must define a unique method that matches the signature of the associated delegate:

C H A P T E R 8 C A L L B A C K I N T E R FA C E S, D E L E G AT E S, A N D E V E N T S

283

class SomeCaller

{

static void Main(string[] args)

{

SomeType t = new SomeType();

t.SomeEvent += new SomeDelegate(MyEventHandler);

}

// Typically only called by the SomeDelegate object. public static void MyEventHandler()

{ ...}

}

When you think about it, however, methods such as MyEventHandler() are seldom intended to be called by any part of the program other than the invoking delegate. As far as productivity is concerned, it is a bit of a bother (though in no way a showstopper) to manually define a separate method to be called by the delegate object.

To address this point, it is now possible to associate a delegate directly to a block of code statements at the time of event registration. Formally, such code is termed an anonymous method. To illustrate the basic syntax, check out the following Main() method, which handles the events sent from the Car type using anonymous methods, rather than specifically named event handlers:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Anonymous Methods *****\n");

Car c1 = new Car("SlugBug", 100, 10);

// Register event handlers as anonymous methods. c1.AboutToBlow += delegate {

Console.WriteLine("Eek! Going too fast!");

};

c1.AboutToBlow += delegate(object sender, CarEventArgs e) { Console.WriteLine("Message from Car: {0}", e.msg);

};

c1.Exploded += delegate(object sender, CarEventArgs e) { Console.WriteLine("Fatal Message from Car: {0}", e.msg);

};

...

}

}

Note The final curly bracket of an anonymous method must be terminated by a semicolon. If you fail to do so, you are issued a compilation error.

Again, notice that the Program type no longer defines specific static event handlers such as CarAboutToBlow() or CarExploded(). Rather, the unnamed (aka anonymous) methods are defined inline at the time the caller is handling the event using the += syntax.

The basic syntax of an anonymous method matches the following pseudo-code: