Managed C++ and .NET Development: Visual Studio .NET 2003 Edition
Delegates and events are completely new concepts to the traditional C++ developer. Truth be told, both provide the same functionality, basically allowing functions to be manipulated as pointers. Because a pointer can be assigned to more than one value in its lifetime, it is possible to have functions executed based on whichever function address was last placed in the pointer.
For those of you with a C++ background, you might notice that this object-oriented approach is very similar to function pointers. Where they differ is that delegates and events are a class and not a pointer, and delegates and events only invoke member methods of managed (__gc) classes.
You might be wondering, If they all do basically the same thing, why introduce the new concepts? Remember that a key aspect of .NET is language independence. Unfortunately, function pointers are strictly a C++ language feature and are not easily implemented in other languages, especially languages where there are no pointers. Also, function pointers are far from easy to implement. Delegates and events were designed to overcome these problems.
Delegates
A delegate is a class that accepts and then invokes one or more methods that share the same signature from other classes that have methods with this same signature.
The .NET Framework supports two forms of delegates:
-
System::Delegate: A delegate that accepts and invokes only a single method.
-
System::MulticastDelegate: A delegate that accepts and invokes a chain of methods. A MulticastDelegate can perform something known as multicast chaining, which you can think of as a set of delegates linked together and then later, when called, executed in sequence.
Managed C++ only supports multicast delegates, but this really isn't a problem because there's nothing stopping a multicast delegate from accepting and invoking only one method.
The creating and implementing of delegates is a three-part process with an optional fourth part if multicast chaining is being implemented:
-
Create the delegate.
-
Create the method to be delegated.
-
Place the method on the delegate.
-
Combine or remove delegates from the multicast chain.
Creating a Delegate
The code involved in creating a delegate is extremely easy. In fact, it is just a method prototype prefixed with the keyword __delegate. By convention, a delegate is suffixed with "delegate" but this is not essential. For example:
__delegate void SayDelegate(String *name);
What happens in the background during the compilation process is a lot more complex. This statement actually gets converted to a class with a constructor to accept delegated methods and three member methods to invoke these methods. Figure 4-8 shows the effects of the resulting compilation by running the program ILDASM on Listing 4-17 shown later.
Listing 4-17: Delegates.exe: Programming Delegates
#using <mscorlib.dll> using namespace System; __delegate void SayDelegate(String *name); __gc class Talkative { public: static void SayHello(String *name) { Console::Write(S"Hello there "); Console::WriteLine(name); } void SayStuff(String *name) { Console::Write(S"Nice weather we are having. Right, "); Console::Write(name); Console::WriteLine(S"?"); } void SayBye(String *name) { Console::Write(S"Good-bye "); Console::WriteLine(name); } }; Int32 main(void) { SayDelegate *hello, *stuff, *bye; // Static member functions hello = new SayDelegate(0, &Talkative::SayHello); Talkative *computer = new Talkative(); // Non-static member functions stuff = new SayDelegate(computer, &Talkative::SayStuff); bye = new SayDelegate(computer, &Talkative::SayBye); // Multicast delegate combine SayDelegate *winded = dynamic_cast<SayDelegate*>(Delegate::Combine(hello, stuff)); winded->Invoke(S"Mr Fraser"); bye->Invoke(S"Stephen"); Console::WriteLine(S"-------------"); // Multicast delegate remove SayDelegate *gruff = dynamic_cast<SayDelegate*>(Delegate::Remove(winded, stuff)); gruff->Invoke(S"Mr Fraser"); bye->Invoke(S"Stephen"); return 0; }
Creating a Method to be Delegated
There is absolutely nothing special about creating a member method for delegating. The only criteria are that it has the same signature as the delegate and that it is a public member method of a managed (__gc) class. The method can be either a static member method:
__gc class Talkative { public: static void SayHello(String *name) { Console::Write(S"Hello there "); Console::WriteLine(name); } }
or a nonstatic member method:
__gc class Talkative { public: void SayStuff(String *name) { Console::Write(S"Nice weather we are having. Right, "); Console::Write(name); Console::WriteLine(S"?"); } }
Placing a Method on the Delegate
This is the least obvious part of the delegating process. The reason is that you need to implement the autogenerated constructor of the delegate class. If you were not aware that a delegate was a class, then the syntax would appear quite confusing. But, because you are, it should be quite obvious that all you are doing is creating a new instance of the delegate class for each method that you want to delegate.
There is only one constructor for a delegate, and it takes two parameters:
delegate-name (address-of-object, address-of-method);
The delegate-name is the same as what you specified when you created the delegate. The address-of-object has two possible values. For static member methods, the value should be set to 0. For nonstatic member methods, the value should be the address of a previously created object within which the member method can be found. The address-of-method should contain the fully referenced address of the method. For example, here are delegations of a static and a non-static member method:
// Static member functions SayDelegate *hello = new SayDelegate(0, &Talkative::SayHello); // Non-static member functions Talkative *computer = new Talkative(); SayDelegate *stuff = new SayDelegate(computer, &Talkative::SayStuff);
Combining and Removing Delegates From a Multicast Chain
These are the trickiest parts of the delegating process, which doesn't say much. The reason they're tricky is that they require the use of two static member methods of the Delegate class, aptly named Combine and Remove. Also, they require typecasting.
The syntax for both combining and removing is exactly the same, except for, of course, the name of the method being called:
// Multicast delegate combine SayDelegate *wind = dynamic_cast<SayDelegate*>(Delegate::Combine(hello, stuff)); // Multicast delegate remove SayDelegate *gruff = dynamic_cast<SayDelegate*>(Delegate::Remove(wind, stuff));
Basically, the Combine method takes the two delegates, chains them together, and then places them on a new delegate. Because the Combine method generates a generic Delegate instance, it needs to be typecast to that specific delegate class required.
The Remove method does the opposite of the Combine method. It removes the specified delegate from the delegate multicast chain and then places the new chain on a new delegate. The Remove method also generates a generic Delegate instance, so it too needs to typecast to the delegate class required.
Invoking a Delegate
Doing this is quite simple but not obvious if you were not aware that a delegate is a class. All you have to do is call the autogenerated member method Invoke with the parameter list that you specified when you created the delegate:
hello->Invoke(S"Mr Fraser"); wind->Invoke(S"Stephen");
There is no difference in the syntax, whether you invoke one method or a whole chain of methods. The Invoke method simply starts at the top of the chain and executes methods until it reaches the end. If there is only one method, then it only executes one.
Listing 4-17 is a complete example of creating, implementing, and invoking delegates. It uses the same methods found in the function point example, but this time they are member methods of a managed (__gc) class. The example simply creates three delegates, combines two, and then invokes a single delegate and a chained delegate. Then it removes one of the delegates from the chain and invokes a single delegate and a chained delegate, but this time the chain contains only one delegate.
Figure 4-9 shows the results of this little program.
Events
An event is a specific implementation of delegates. You'll see it used quite extensively when I start discussing Windows Forms in Chapter 9 and 10. For now, you'll explore what events are and how they work without worrying about the .NET Framework event model.
In simple terms, events allow one class to trigger the execution of methods found in other classes without knowing anything about these classes or even from which classes it is invoking the method. This allows a class to execute methods and not have to worry about how, or even if, they are implemented. Because events are implemented using multicast delegates, it is possible for a single class to call a chain of methods from multiple classes.
There are always at least two classes involved with events. The first is the source of the event. This class generates an event and then waits for some other class, which has delegated a method to handle the event, to process it. If there are no delegated methods to process the event, then the event is lost. The second and subsequent classes, as was hinted at previously, receive the event by delegating methods to handle the event. I guess, truthfully, only one class is needed to handle an event, as the class that created the event could also delegate a method to process the event. But why would you want to do this, when a direct call to the method could be used, avoiding the event altogether, and it would also be much more efficient?
Building Event Source Class
Before you create an event source class, you need to define a delegate class on which the event will process. The delegate syntax is exactly the same as was covered previously. In fact, there is no difference between a standard delegate and one that handles events. To differentiate between these two types of delegates, by convention delegates that handle events have a suffix of "Handler":
__delegate void SayHandler(String *name);
Once you have the delegate defined, you can then create an event source class. There are basically two pieces that you will find in all event source classes: the event and an event trigger method. Like delegates, events are easy to code but do a little magic in the background. To create an event, include within a managed (__gc) class in a public scope area a delegate class declaration prefixed by the keyword__event:
__gc class EventSource { public: __event SayHandler* OnSay; //... };
Simple enough, but when the compiler encounters this, it gets converted into three member methods:
-
add_<delegate-name>: A public member method that calls the Delegate::Combine method to add delegated receiver class methods. To simplify the syntax, you use the overloaded += operator instead of calling add_<delegate-name> directly.
-
remove_<delegate-name>: A public member method that calls the Delegate::Remove method to remove delegated receiver class methods. To simplify the syntax, you use the overloaded -= operator instead of calling remove_<delegate-name> directly.
-
raise_<delegate-name>: A protected member method that calls the Delegate::Invoke method to call all delegated receiver class methods. This method is protected so that client classes cannot call it. It can only be called through a managed internal process.
Figure 4-10 is an ILDASM snapshot that shows the methods that were created by the __event keyword within the event source class of Listing 4-18, which is shown later.
Listing 4-18: Events.exe: Programming Events
#using <mscorlib.dll> using namespace System; __delegate void SayHandler(String *name); __gc class EventSource { public: __event SayHandler* OnSay; void Say(String *name) { OnSay(name); } }; __gc class EventReceiver1 { EventSource *source; public: EventReceiver1(EventSource *src) { if (src == 0) throw new ArgumentNullException(S"Must pass an Event Source"); source = src; source->OnSay += new SayHandler(this, &EventReceiver1::SayHello); source->OnSay += new SayHandler(this, &EventReceiver1::SayStuff); } void RemoveStuff() { source->OnSay -= new SayHandler(this, &EventReceiver1::SayStuff); } void SayHello(String *name) { Console::Write(S"Hello there ") Console::WriteLine(name); } void SayStuff(String *name) { Console::Write(S"Nice weather we are having. Right, ") Console::Write(name); Console::WriteLine(S"?"); } }; __gc class EventReceiver2 { EventSource *source; public: EventReceiver2(EventSource *src) { if (src == 0) throw new ArgumentNullException(S"Must pass an Event Source"); source = src; source->OnSay += new SayHandler(this, &EventReceiver2::SayBye); } void SayBye(String *name) { Console::Write(S"Good-bye "); Console::WriteLine(name); } }; Int32 main(void) { EventSource *source = new EventSource(); EventReceiver1 *receiver1 = new EventReceiver1(source); EventReceiver2 *receiver2 = new EventReceiver2(source); source->Say(S"Mr Fraser"); Console::WriteLine(S"------------------"); receiver1->RemoveStuff(); source->Say(S"Stephen"); return 0; }
Finally, now that you have an event, you need a way to trigger it. The triggering event can be almost anything. In Web forms, the triggering event will be handled by things such as mouse clicks and keypresses. In this case, you will simply call the delegate directly:
__gc class EventSource { public: __event SayHandler* OnSay; void Say(String *name) { OnSay(name); } };
Building Event Receiver Class(es)
One or more classes can process an event. The process for delegating a member class to an event is identical for each class. Other than the simplified syntax, you will find that event handling and delegate processing is the same. First, you create the member method to delegate. Then you combine it on the event handler.
The first thing you will need to do is create a public managed (__gc) class member method to be delegated to the event handler. Nothing is new here:
_gc class EventReceiver { public: //... void SayBye(String *name) { Console::Write(S"Good-bye "); Console::WriteLine(name); } };
Then, to combine this method on the event handler, the event receiver class must know which event source class it will be associated with. The easiest way to do this is to pass it through the constructor. To avoid a null pointer error, check to make sure that the pointer was passed. I could have made more thorough validations, such as verifying the type of class, but this is enough to get the idea across.
Now that you have the event source class and a member method to place, it is simply a matter of creating a new instance of a delegate of the event's delegate type and combining it. Or, in this case, using the operator += to combine the new delegate to the event within the source event class:
__gc class EventReceiver { EventSource *source; public: EventReceiver(EventSource *src) { if (src == 0) throw new ArgumentNullException(S"Must pass an Event Source"); source = src; source->OnSay += new SayHandler(this, &EventReceiver::SayBye); } //... };
What if you have a delegated method that you no longer want to be handled by the event? You would remove it just as you would a standard delegate. The only difference is that you can now use the -= operator. With its simplified syntax, you don't have to worry about typecasting:
source->OnSay -= new SayHandler(this, &EventReceiver::SayStuff);
Implementing the Event
You now have both a source and a receiver class. All that you need to do is create instances of each and then call the event trigger method.
Int32 main(void) { EventSource *source = new EventSource(); EventReceiver *receiver = new EventReceiver(source); source->Say(S"Mr Fraser"); return 0; }
Listing 4-18 shows all of the code needed to handle an event. This time, the event source class has two event receiver classes. The event is triggered twice. The first time, all delegates are combined and executed. The second time, one of the delegates is removed. You might notice that the member methods are very familiar.
Figure 4-11 shows the results of this little program.
Категории