Recipe 3.19 Adding a Notification Callback Using an Interface
Problem
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.
Solution
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
Discussion
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.
See Also
See the "interface" keyword,
"Base Class Usage Guidelines," and
"When to Use Interfaces" topics in
the MSDN documentation.
|