[ Team LiB ] |
Recipe 3.19 Adding a Notification Callback Using an InterfaceProblemYou need a flexible, well-performing callback mechanism that does not make use of a delegate because you need more than one callback method. So the relationship between the caller and the callee is more complex than can easily be represented through the one method signature that you get with a delegate. SolutionUse an interface to provide callback methods. The INotificationCallbacks interface contains two methods that will be used by a client as callback methods. The first method, FinishedProcessingSubGroup, is called when an amount specified in the amount parameter is reached. The second method, FinishedProcessingGroup, is called when all processing is complete: public interface INotificationCallbacks { void FinishedProcessingSubGroup(int amount); void FinishedProcessingGroup( ); } The NotifyClient class implements the INotificationCallbacks interface. This class contains the implementation details of each of the callback methods: public class NotifyClient : INotificationCallbacks { public void FinishedProcessingSubGroup(int amount) { Console.WriteLine("Finished processing " + amount + " items"); } public void FinishedProcessingGroup( ) { Console.WriteLine("Processing complete"); } } The Task class is the main class that implements its callbacks through the NotifyClient object. The Task class contains a field called notificationObj, which stores a reference to the NotifyClient object that is passed to it either through construction or through the AttachToCallback method. The UnAttachCallback method removes the NotifyClient reference from this object. The ProcessSomething method implements the callback methods: public class Task { public Task(NotifyClient notifyClient) { notificationObj = notifyClient; } NotifyClient notificationObj = null; public void AttachToCallback(NotifyClient notifyClient) { notificationObj = notifyClient; } public void UnAttachCallback( ) { notificationObj = null; } public void ProcessSomething( ) { // This method could be any type of processing for (int counter = 0; counter < 100; counter++) { if ((counter % 10) == 0) { if (notificationObj != null) { notificationObj.FinishedProcessingSubGroup(counter); } } } if (notificationObj != null) { notificationObj.FinishedProcessingGroup( ); } } } The CallBackThroughIFace method uses callback features of the Task class as follows: public void CallBackThroughIFace( ) { NotifyClient notificationObj = new NotifyClient( ); Task t = new Task(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback( ); t.ProcessSomething( ); Console.WriteLine( ); t.AttachToCallback(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback( ); t.ProcessSomething( ); } This method displays the following: Finished processing 0 items Finished processing 10 items Finished processing 20 items Finished processing 30 items Finished processing 40 items Finished processing 50 items Finished processing 60 items Finished processing 70 items Finished processing 80 items Finished processing 90 items Processing complete Finished processing 0 items Finished processing 10 items Finished processing 20 items Finished processing 30 items Finished processing 40 items Finished processing 50 items Finished processing 60 items Finished processing 70 items Finished processing 80 items Finished processing 90 items Processing complete DiscussionUsing an interface mechanism for callbacks is a simple but effective alternative to using delegates. The interface mechanism is only slightly faster than using a delegate since you are simply making a call through an interface. This interface mechanism requires a notification client (NotifyClient) to be created that implements a callback interface (INotificationCallbacks). This notification client is then passed to an object that is required to call back to this client. This object is then able to store a reference to the notification client and use it appropriately whenever its callback methods are used. When using the callback methods on the notificationObj, you should test to determine whether the notificationObj is null; if so, you should not use it or else a NullReferenceException will be thrown: if (notificationObj != null) { notificationObj.FinishedProcessingGroup( ); } Interface callbacks cannot always be used in place of delegates. The following list indicates where to use each type of callback:
The current Task class is designed to allow only a single notification client to be used; in many cases, this would be a severe limitation. The Task class could be modified to handle multiple callbacks, similar to a multicast delegate. The MultiTask class is a modification of the Task class to do just this: public class MultiTask { public MultiTask(NotifyClient notifyClient) { notificationObjs.Add(notifyClient); } ArrayList notificationObjs = new ArrayList( ); public void AttachToCallback(NotifyClient notifyClient) { notificationObjs.Add(notifyClient); } public void UnAttachCallback(NotifyClient notifyClient) { notificationObjs.Remove(notifyClient); } public void UnAttachAllCallbacks( ) { notificationObjs.Clear( ); } public void ProcessSomething( ) { // This method could be any type of processing for (int counter = 0; counter < 100; counter++) { if ((counter % 10) == 0) { foreach (NotifyClient callback in notificationObjs) { callback.FinishedProcessingSubGroup(counter); } } } foreach (NotifyClient callback in notificationObjs) { callback.FinishedProcessingGroup( ); } } } The MultiCallBackThroughIFace method uses callback features of the MultiTask class as follows: public void MultiCallBackThroughIFace( ) { NotifyClient notificationObj = new NotifyClient( ); MultiTask t = new MultiTask(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.AttachToCallback(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachAllCallbacks( ); t.ProcessSomething( ); } This method displays the following: Finished processing 0 items Finished processing 10 items Finished processing 20 items Finished processing 30 items Finished processing 40 items Finished processing 50 items Finished processing 60 items Finished processing 70 items Finished processing 80 items Finished processing 90 items Processing complete Finished processing 0 items Finished processing 0 items Finished processing 10 items Finished processing 10 items Finished processing 20 items Finished processing 20 items Finished processing 30 items Finished processing 30 items Finished processing 40 items Finished processing 40 items Finished processing 50 items Finished processing 50 items Finished processing 60 items Finished processing 60 items Finished processing 70 items Finished processing 70 items Finished processing 80 items Finished processing 80 items Finished processing 90 items Finished processing 90 items Processing complete Processing complete Finished processing 0 items Finished processing 10 items Finished processing 20 items Finished processing 30 items Finished processing 40 items Finished processing 50 items Finished processing 60 items Finished processing 70 items Finished processing 80 items Finished processing 90 items Processing complete Another shortcoming exists with both the Task and MultiTask classes. What if you need several types of client notification classes? For example, we already have the NotifyClient class, what if we added a second class NotifyClientType2 that also implements the INotificationCallbacks interface? This new class is shown here: public class NotifyClientType2 : INotificationCallbacks { public void FinishedProcessingSubGroup(int amount) { Console.WriteLine("[Type2] Finished processing " + amount + " items"); } public void FinishedProcessingGroup( ) { Console.WriteLine("[Type2] Processing complete"); } } The current code base cannot handle this new client notification type. To fix this problem, we can replace all occurrences of the type NotifyClient with the interface type INotificationCallbacks. This will allow us to use any type of notification client with our Task and MultiTask objects. The modifications to these classes are highlighted in the following code: public class Task { public Task(INotificationCallbacks notifyClient) { notificationObj = notifyClient; } INotificationCallbacks notificationObj = null; public void AttachToCallback(INotificationCallbacks notifyClient) { notificationObj = notifyClient; } ... } public class MultiTask { public MultiTask(INotificationCallbacks notifyClient) { notificationObjs.Add(notifyClient); } ArrayList notificationObjs = new ArrayList( ); public void AttachToCallback(INotificationCallbacks notifyClient) { notificationObjs.Add(notifyClient); } public void UnAttachCallback(INotificationCallbacks notifyClient) { notificationObjs.Remove(notifyClient); } ... public void ProcessSomething( ) { // This method could be any type of processing for (int counter = 0; counter < 100; counter++) { if ((counter % 10) == 0) { foreach (INotificationCallbacks callback in notificationObjs) { callback.FinishedProcessingSubGroup(counter); } } } foreach (INotificationCallbacks callback in notificationObjs) { callback.FinishedProcessingGroup( ); } } } Now we can use either of the client notification classes interchangeably. This is shown in the following modified methods MultiCallBackThroughIFace and CallBackThroughIFace: public void CallBackThroughIFace( ) { INotificationCallbacks notificationObj = new NotifyClient( ); Task t = new Task(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback( ); t.ProcessSomething( ); Console.WriteLine( ); INotificationCallbacks notificationObj2 = new NotifyClientType2( ); t.AttachToCallback(notificationObj2); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback( ); t.ProcessSomething( ); } public void MultiCallBackThroughIFace( ) { INotificationCallbacks notificationObj = new NotifyClient( ); MultiTask t = new MultiTask(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); INotificationCallbacks notificationObj2 = new NotifyClientType2( ); t.AttachToCallback(notificationObj2); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachCallback(notificationObj); t.ProcessSomething( ); Console.WriteLine( ); t.UnAttachAllCallbacks( ); t.ProcessSomething( ); } The highlighted code has been modified from the original code. See AlsoSee the "interface" keyword, "Base Class Usage Guidelines," and "When to Use Interfaces" topics in the MSDN documentation. |
[ Team LiB ] |