Quantcast
Channel: C# Help » Events
Viewing all articles
Browse latest Browse all 10

.NET Event Handling using the Template Method Design Pattern

$
0
0


 

Environment: .NET, C#, Visual Basic .NET, Windows 2000, Windows XP, Windows NT4 SP6a

Microsoft .NET event handling, in common with typical object-orientedframeworks, 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 .NETevent handling with the Template Method design pattern. The discussionand code snippets are inC# but the summary sample is implemented in both C# and Visual Basic .NET.

The article elaborates on ideas discussed by Tomas Restrepo inthe March 2002 issue of Visual Systems Journal,which builds on the recommended practice for event handling described by Microsoftin the MSDN Library .NET topic, DesignGuidelines for Class Library Developers. (See the sub-topic "Event UsageGuidelines.") 

The simplest strategy for event handling is just to raise an event and notcare about who consumes it, or whether different clients need to relate to it indifferent ways. 

Example – Simple Event Handling

Consider a class, Supplier, that raises an event whenever its name fieldis 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 there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}

private string name;
}

public class Client
{
public Client()
{
// Register for supplier event
supplier = new Supplier();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}

public void TestEvent()
{
// Set the name – which generates an event
supplier.Name = "Kevin McFarlane";
}

private void supplier_NameChanged(object sender, EventArgs e)
{
// Handle supplier event
}

private Supplier supplier;
}
<!– end the block of source code –>

Clients of an event can be both external and internal

An "external" client is one that consumes an event but is not related to theclass that raises the event. In other words, it is not part of the event class'sinheritance tree. The class, Client, above is an external client. 

An "internal" client can be the event-raising class itself, if it's handlingits own events, or a subclass of the event-raising class. In such cases, thesimple strategy outlined above is inadequate. Clients cannot easily change whathappens when the event is raised or what the default behaviour is when handlingthe event.

To tackle this problem, in the .NET DesignGuidelines for Class Library Developers, Microsoft recommends using aprotected virtual method for raising each event. This provides a way forsubclasses to handle the event using an override. So, in our example,OnNameChanged() should look like this:

protected virtual void OnNameChanged()
{
// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}

Microsoft then adds: "The derived class can choose not to call the baseclass during the processing of OnEventName. Be prepared for this by notincluding any processing in the OnEventName method that is required for the baseclass to work correctly."

Therein lies the problem. In general, OnNameChanged() may do some defaultprocessing before it raises the event. An OnNameChanged() override may want to dosomething different. But to ensure that external clients work properly it mustcall the base class version. If it doesn't call the base class version the eventwill not be raised for external clients. And it may forget to call the baseclass version. Forgetting to call the base class version, which raises theevent, violates the Liskov (polymorphic) substitution principle: methods that use referencesto base classes must be able to use objects of derived classes without knowingit. Fortunately, there is a way out of this problem.

The Template Method Design Pattern

The purpose of the Template Method designpattern is to define analgorithm as a fixed sequence of steps but have one or more of thesteps variable. In our example the algorithm can be considered toconsist of raisingthe event and responding to it. The part that needs to be variable istheresponse. So the trick is to separate this from the raising of theevent. Wesplit OnNameChanged() into two methods: InternalOnNameChanged() and OnNameChanged().InternalOnNameChanged() calls OnNameChanged() to perform default processing and thenraises the event. 

private void InternalOnNameChanged()
{
// Derived classes may override default behaviour
OnNameChanged();

// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}

protected virtual void OnNameChanged()
{
// Implement default behaviour here
}

The Name property is now altered to look like this:

get { return name; }
set { name = value; InternalOnNameChanged(); }

The advantages of this technique are: 

  1. 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.
  2. 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 ofthree classes, Supplier, ExternalClient and InternalClient. Supplier raises anevent. The two client classes each consume the event. InternalClient is a derived class of Supplier

ExternalClient contains an embedded Supplier reference. However, this isinitialised with an InternalClient reference. Thus when ExternalClient registersfor the Supplier event, this invokes InternalClient's OnNameChanged() override.Then the event is handled by InternalClient's NameChanged() and finallyExternalClient's NameChanged() handlers.

<
/p>

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();
}
}

// Generates an event when its property is set.
public class Supplier
{
public Supplier() {}

public event EventHandler NameChanged;

public string Name
{
get { return name; }
set { name = value; InternalOnNameChanged(); }
}

// Internal clients, i.e., derived classes, can override default behaviour.
protected virtual void OnNameChanged()
{
// Implement default behaviour here
Console.WriteLine("Supplier.OnNameChanged");
}

// If internal clients (derived classes) override the default behaviour in OnNameChanged
// then external clients will still receive the event.
private void InternalOnNameChanged()
{
// Derived classes may override default behaviour
OnNameChanged();

// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}

private string name;
}

// An "internal" client that handles the Supplier.NameChanged event
// but first overrides its default behaviour.
public class InternalClient : Supplier
{
public InternalClient()
{
NameChanged += new EventHandler(this.Supplier_NameChanged);
}

protected override void OnNameChanged()
{
// Override default behaviour of Supplier.NameChanged
Console.WriteLine("InternalClient.OnNameChanged");
}

private void Supplier_NameChanged(object sender, EventArgs e)
{
// Handle Supplier.NameChanged
Console.WriteLine("InternalClient.NameChanged");
}
}

// An "external" client that handles the Supplier.NameChanged event.
public class ExternalClient
{
public ExternalClient()
{
// Instantiate supplier as a reference to an InternalClient instance.
// This should trigger the InternalClient.OnNameChanged override
// when an event is raised.
supplier = new InternalClient();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}

public void TestSupplier()
{
// This should raise an event and it will be handled by
// the InternalClient and ExternalClient handlers.
supplier.Name = "Kevin McFarlane";
}

private void supplier_NameChanged(object sender, EventArgs e)
{
// Handle Supplier.NameChanged
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

' Generates an event when its property is set.
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

' Internal clients, i.e., derived classes, can override default behaviour.
Protected Overridable Sub OnNameChanged()
' Implement default behaviour here
Console.WriteLine("Supplier.OnNameChanged")
End Sub

Private Sub InternalOnNameChanged()
' Derived classes may override default behaviour
OnNameChanged()

' Raise event for clients
RaiseEvent NameChanged(Me, New EventArgs())
End Sub

Private mName As String
End Class

' An "internal" client that handles the Supplier.NameChanged event
' but first overrides its default behaviour.
Public Class InternalClient
Inherits Supplier

Sub New()
End Sub

Protected Overrides Sub OnNameChanged()
' Override default behaviour of Supplier.NameChanged
Console.WriteLine("InternalClient.OnNameChanged")

End Sub

Private Sub Supplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs) _
Handles MyBase.NameChanged

' Handle Supplier.NameChanged
Console.WriteLine("InternalClient.NameChanged")
End Sub
End Class

' An "external" client that handles the Supplier.NameChanged event.
Public Class ExternalClient
Sub New()
' Instantiate Supplier as a reference to an InternalClient instance.
' This should trigger the InternalClient.OnNameChanged override
' when an event is raised.
mSupplier = New InternalClient()

' Register for Supplier.NameChanged event
AddHandler mSupplier.NameChanged, AddressOf mSupplier_NameChanged
End Sub

Public Sub TestSupplier()
' This should raise an event and it will be handled by
' the InternalClient and ExternalClient handlers.
mSupplier.Name = "Kevin McFarlane"

End Sub

Private Sub mSupplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs)
' Handle Supplier.NameChanged
Console.WriteLine("ExternalClient.NameChanged")
End Sub

Private mSupplier As Supplier
End Class


Viewing all articles
Browse latest Browse all 10

Trending Articles