Search

Friday 6 May 2011

Delegates 101 - Part IV: Event Handling


Introduction

So, here we are at the final part of this series on delegates; and it wouldn't be complete without looking at one of the key uses of delegates in .NET: Event handling.

A Simple Timer

In this article, we are going to look at a simple timer example. This timer raises two events: one when the timer is started and another when the timer has elapsed. The complete code for the timer is as follows:

// C#
public delegate void TimerEventHandler(object sender, EventArgs eventArgs);

public class SimpleTimer
{
    private TimeSpan time;

    public event TimerEventHandler Started;
    public event TimerEventHandler Elapsed;

    public SimpleTimer(long milliseconds)
        : this(TimeSpan.FromMilliseconds(milliseconds))
    {
    }

    public SimpleTimer(TimeSpan time)
    {
        this.time = time;
    }

    public void Start()
    {
        Thread timerThread = new Thread
        (
            delegate()
            {
                OnStarted(new EventArgs());
                Thread.Sleep(time);
                OnElapsed(new EventArgs());
            }
        );
        timerThread.Start();
    }

    private void OnStarted(EventArgs eventArgs)
    {
        if (Started != null)
            Started(this, eventArgs);
    }

    private void OnElapsed(EventArgs eventArgs)
    {
        if (Elapsed != null)
            Elapsed(this, eventArgs);
    }
}
' Visual Basic
Public Delegate Sub TimerEventHandler(ByVal sender As Object, ByVal eventArgs As EventArgs)

Public Class SimpleTimer

    Private _time As TimeSpan

    Public Event Started As TimerEventHandler
    Public Event Elapsed As TimerEventHandler

    Public Sub New(ByVal milliseconds As Long)
        Me.New(TimeSpan.FromMilliseconds(milliseconds))
    End Sub

    Public Sub New(ByVal time As TimeSpan)
        _time = time
    End Sub

    Public Sub Start()
        Dim timerThread As Thread = New Thread _
        ( _
            Sub()
                OnStarted(New EventArgs())
                Thread.Sleep(_time)
                OnElapsed(New EventArgs())
            End Sub _
        )
        timerThread.Start()
    End Sub

    Private Sub OnStarted(ByVal eventArgs As EventArgs)
        RaiseEvent Started(Me, New EventArgs())
    End Sub

    Private Sub OnElapsed(ByVal eventArgs As EventArgs)
        RaiseEvent Elapsed(Me, New EventArgs())
    End Sub
End Class

Now let's look at this code a little more closely. Firstly, notice how we declare a delegate for any methods which can be used to handle our two events:

// C#
public delegate void TimerEventHandler(object sender, EventArgs eventArgs);
' Visual Basic
Public Delegate Sub TimerEventHandler(ByVal sender As Object, ByVal eventArgs As EventArgs)

By convention, event handlers accept two parameters:

ParameterDescription
senderThe object which raised the event.
eventArgsAn object of type EventArgs, or inherits from EventArgs, which contains encapsulates any optional data we wish to pass to our event handler

We also declare the two events which out timer is going to raise:

// C#
public event TimerEventHandler Started;
public event TimerEventHandler Elapsed;
' Visual Basic
Public Event Started As TimerEventHandler
Public Event Elapsed As TimerEventHandler

Notice how the type of each of these events is that of the delegate which is going to handle them, in this case TimerEventHandler.

We also have convenience methods for raising each of the events:

// C#
private void OnStarted(EventArgs eventArgs)
{
    if (Started != null)
        Started(this, eventArgs);
}

private void OnElapsed(EventArgs eventArgs)
{
    if (Elapsed != null)
        Elapsed(this, eventArgs);
}
' Visual Basic
Private Sub OnStarted(ByVal eventArgs As EventArgs)
    RaiseEvent Started(Me, New EventArgs())
End Sub

Private Sub OnElapsed(ByVal eventArgs As EventArgs)
    RaiseEvent Elapsed(Me, New EventArgs())
End Sub

Note how we fire the event in exactly the same way as we would call any other delegate. Additionally, in C#, we do a check to see if any handlers have been wired-up up to our event; as firing an event without any handlers will cause an exception to be thrown.

Finally, the code which actually runs our timer and raises the events. Note how the timer is executed in a separate thread and (simply because it is in-keeping with the theme of this series) is passed to the thread as an anonymous method:

// C#
public void Start()
{
    Thread timerThread = new Thread
    (
        delegate()
        {
            OnStarted(new EventArgs());
            Thread.Sleep(time);
            OnElapsed(new EventArgs());
        }
    );
    timerThread.Start();
}
' Visual Basic
Public Sub Start()
    Dim timerThread As Thread = New Thread _
    ( _
        Sub()
            OnStarted(New EventArgs())
            Thread.Sleep(_time)
            OnElapsed(New EventArgs())
        End Sub _
    )
    timerThread.Start()
End Sub

Using our Timer

The following code shows how to use our simple timer and hook up our own methods to handle the events it raises:

// C#
static void Main(string[] args)
{
    SimpleTimer timer = new SimpleTimer(5000);
    timer.Started += timer_Started;
    timer.Elapsed += timer_Elapsed;
    timer.Start();
    Console.Read();
}

static void timer_Started(object sender, EventArgs e)
{
    Console.WriteLine("Timer has started.");
}

static void timer_Elapsed(object sender, EventArgs e)
{
    Console.WriteLine("Timer has elapsed.");
}
' Visual Basic
Sub Main(ByVal args() As String)
    Dim timer As SimpleTimer = New SimpleTimer(5000)
    AddHandler timer.Started, AddressOf timer_Started
    AddHandler timer.Elapsed, AddressOf timer_Elapsed
    timer.Start()
    Console.Read()
End Sub

Private Sub timer_Started(sender As Object, eventArgs As EventArgs)
    Console.WriteLine("Timer has started.")
End Sub

Private Sub timer_Elapsed(sender As Object, eventArgs As EventArgs)
    Console.WriteLine("Timer has elapsed.")
End Sub

Note how the syntax for wiring up our event handlers to the events is slightly different from what we've seen previously. This is because events are a type of multicast delegate, and as such multiple handlers can be wired up the same event.

Summary

Handling of events is one of the key uses for delegates in .NET. All event handlers follow a specific convention in terms of their signature and multiple different handlers can be wired up to the same event.

2 comments:

  1. Would you please explain lines 5-6 in the last listing?

    timer.Started += timer_Started;
    timer.Elapsed += timer_Elapsed;

    I don't know C# well enough to understand this. It looks like events are being updated with the return values from functions which return void. Huh?

    ReplyDelete
  2. Yes, I agree the C# syntax is somewhat confusing. I guess the += operator was chosen to try and emphasise the fact that you are adding a handler to an event (which may already have other handlers wired up to it), rather than (re)assigning the handler.

    In that respect the VB.NET syntax is much more intuitive in my opinion.

    ReplyDelete