Introduction
Microsoft .NET event handling, in common with typical object-oriented
frameworks, is implemented using the well-known Observer design pattern. (See the book, Design Patterns, by Gamma et al.,
Addison-Wesley, 1995, pp325-330). This article describes how to enhance .NET
event handling with the Template Method design pattern. The discussion and code snippets are in
C# but the summary sample is implemented in both C# and Visual Basic .NET.
The article elaborates on ideas discussed by Tomas Restrepo in
the March 2002 issue of Visual Systems Journal,
which builds on the recommended practice for event handling described by Microsoft
in the MSDN Library .NET topic, Design
Guidelines for Class Library Developers. (See the sub-topic "Event Usage
Guidelines.")
The simplest strategy for event handling is just to raise an event and not
care about who consumes it, or whether different clients need to relate to it in
different ways.
Example - Simple Event Handling
Consider a class, Supplier
, that raises an event whenever its name
field
is set and a class, Client
, that handles it.
public class Supplier
{
public Supplier() {}
public event EventHandler NameChanged;
public string Name
{
get { return name; }
set { name = value; OnNameChanged(); }
}
private void OnNameChanged()
{
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
private string name;
}
public class Client
{
public Client()
{
supplier = new Supplier();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}
public void TestEvent()
{
supplier.Name = "Kevin McFarlane";
}
private void supplier_NameChanged(object sender, EventArgs e)
{
}
private Supplier supplier;
}
Clients of an event can be both external and internal.
An "external" client is one that consumes an event but is not related to the
class that raises the event. In other words, it is not part of the event class's
inheritance tree. The class, Client
, above is an external client.
An "internal" client can be the event-raising class itself, if it's handling
its own events, or a subclass of the event-raising class. In such cases, the
simple strategy outlined above is inadequate. Clients cannot easily change what
happens when the event is raised or what the default behaviour is when handling
the event.
To tackle this problem, in the .NET Design
Guidelines for Class Library Developers, Microsoft recommends using a
protected virtual method for raising each event. This provides a way for
subclasses to handle the event using an override. So, in our example,
OnNameChanged()
should look like this:
protected virtual void OnNameChanged()
{
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
Microsoft then adds: "The derived class can choose not to call the base
class during the processing of OnEventName. Be prepared for this by not
including any processing in the OnEventName method that is required for the base
class to work correctly."
Therein lies the problem. In general, OnNameChanged()
may do some default
processing before it raises the event. An OnNameChanged()
override may want to do
something different. But to ensure that external clients work properly it must
call the base class version. If it doesn't call the base class version the event
will not be raised for external clients. And it may forget to call the base
class version. Forgetting to call the base class version, which raises the
event, violates the Liskov (polymorphic) substitution principle: methods that use references
to base classes must be able to use objects of derived classes without knowing
it. Fortunately, there is a way out of this problem.
The Template Method Design Pattern
The purpose of the Template Method design pattern is to define an
algorithm as a fixed sequence of steps but have one or more of the steps variable. In our example the algorithm can be considered to consist of raising
the event and responding to it. The part that needs to be variable is the
response. So the trick is to separate this from the raising of the event. We
split OnNameChanged()
into two methods: InternalOnNameChanged()
and OnNameChanged()
.
InternalOnNameChanged()
calls OnNameChanged()
to perform default processing and then
raises the event.
private void InternalOnNameChanged()
{
OnNameChanged();
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
protected virtual void OnNameChanged()
{
}
The Name
property is now altered to look like this:
get { return name; }
set { name = value; InternalOnNameChanged(); }
The advantages of this technique are:
- An essential step in the base class implementation, in this case raising the event, cannot be avoided by the derived class's failing to call the base class implementation. So external clients can be reliably serviced.
- The derived class can safely replace the base class's default behaviour in
OnNameChanged()
with no worries.
Example - Template Method Design Pattern Event Handling
Below is a complete example implemented in both C# and Visual Basic .NET. It consists of
three classes, Supplier
, ExternalClient
and InternalClient
. Supplier
raises an
event. The two client classes each consume the event. InternalClient
is a derived class of Supplier
.
ExternalClient
contains an embedded Supplier
reference. However, this is
initialised with an InternalClient
reference. Thus when ExternalClient
registers
for the Supplier
event, this invokes InternalClient
's OnNameChanged()
override.
Then the event is handled by InternalClient
's NameChanged()
and finally
ExternalClient
's NameChanged()
handlers.
So the output that is produced is:
InternalClient.OnNameChanged
InternalClient.NameChanged
ExternalClient.NameChanged
C# Implementation
using System;
class Test
{
static void Main(string[] args)
{
ExternalClient client = new ExternalClient();
client.TestSupplier();
}
}
public class Supplier
{
public Supplier() {}
public event EventHandler NameChanged;
public string Name
{
get { return name; }
set { name = value; InternalOnNameChanged(); }
}
protected virtual void OnNameChanged()
{
Console.WriteLine("Supplier.OnNameChanged");
}
private void InternalOnNameChanged()
{
OnNameChanged();
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
private string name;
}
public class InternalClient : Supplier
{
public InternalClient()
{
NameChanged += new EventHandler(this.Supplier_NameChanged);
}
protected override void OnNameChanged()
{
Console.WriteLine("InternalClient.OnNameChanged");
}
private void Supplier_NameChanged(object sender, EventArgs e)
{
Console.WriteLine("InternalClient.NameChanged");
}
}
public class ExternalClient
{
public ExternalClient()
{
supplier = new InternalClient();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}
public void TestSupplier()
{
supplier.Name = "Kevin McFarlane";
}
private void supplier_NameChanged(object sender, EventArgs e)
{
Console.WriteLine("ExternalClient.NameChanged");
}
private Supplier supplier;
}
Visual Basic .NET Implementation
Module Test
Sub Main()
Dim client As ExternalClient = New ExternalClient()
client.TestSupplier()
End Sub
End Module
Public Class Supplier
Sub New()
End Sub
Public Event NameChanged As EventHandler
Public Property Name() As String
Get
Return mName
End Get
Set(ByVal Value As String)
mName = Value
InternalOnNameChanged()
End Set
End Property
Protected Overridable Sub OnNameChanged()
Console.WriteLine("Supplier.OnNameChanged")
End Sub
Private Sub InternalOnNameChanged()
OnNameChanged()
RaiseEvent NameChanged(Me, New EventArgs())
End Sub
Private mName As String
End Class
Public Class InternalClient
Inherits Supplier
Sub New()
End Sub
Protected Overrides Sub OnNameChanged()
Console.WriteLine("InternalClient.OnNameChanged")
End Sub
Private Sub Supplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs) _
Handles MyBase.NameChanged
Console.WriteLine("InternalClient.NameChanged")
End Sub
End Class
Public Class ExternalClient
Sub New()
mSupplier = New InternalClient()
AddHandler mSupplier.NameChanged, AddressOf mSupplier_NameChanged
End Sub
Public Sub TestSupplier()
mSupplier.Name = "Kevin McFarlane"
End Sub
Private Sub mSupplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs)
Console.WriteLine("ExternalClient.NameChanged")
End Sub
Private mSupplier As Supplier
End Class