You 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.
Use 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
Using 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:
Use a delegate if you require ease of coding over performance.
Use the interface callback mechanism if you need potentially complex callbacks. An example of this could be adding a single callback interface method that will call back to an overloaded method. The number and types of parameters determine the method chosen.
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.