AddThis Social Bookmark Button

Print

Using Delegates to Implement Event Handling
Pages: 1, 2, 3

Raising the event will invoke whatever methods have been registered with the Clock class through the delegate. We'll examine this in a moment.



Once the event is raised, you update the state of the Clock class:

this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;

All that is left is to create classes that can subscribe to this event. You'll create two; your first will be the DisplayClock class. The job of DisplayClock is not to keep track of time, but rather to display the current time to the console.

The example simplifies this class down to two methods. The first is a helper method named Subscribe. Subscribe's job is to subscribe to the clock's OnSecondChange delegate. The second method is the event handler TimeHasChanged:

public class DisplayClock
{
    public void Subscribe(Clock theClock)
    {
        theClock.OnSecondChange +=
            new Clock.SecondChangeHandler(TimeHasChanged);
    }

    public void TimeHasChanged(
        object theClock, TimeInfoEventArgs ti)
    {
            Console.WriteLine("Current Time: {0}:{1}:{2}",
                ti.hour.ToString(), 
                ti.minute.ToString(), 
                ti.second.ToString());
   }
}

When the first method, Subscribe, is invoked, it creates a new SecondChangeHandler delegate, passing in its event handler method TimeHasChanged. It then registers that delegate with the OnSecondChange event of Clock.

You will create a second class that will also respond to this event: LogCurrentTime. This class would normally log the event to a file, but for our demonstration purposes, it will log to the standard console:

public class LogCurrentTime
{
    public void Subscribe(Clock theClock)
    {
        theClock.OnSecondChange +=
            new Clock.SecondChangeHandler(WriteLogEntry);
    }

    // this method should write to a file
    // we write to the console to see the effect
    // this object keeps no state
    public void WriteLogEntry(
        object theClock, TimeInfoEventArgs ti)
    {
        Console.WriteLine("Logging to file: {0}:{1}:{2}",
            ti.hour.ToString(), 
            ti.minute.ToString(), 
            ti.second.ToString());
    }
}

Although in this example these two classes are very similar, in a production program, any number of disparate classes might subscribe to an event.

All that remains is to create a Clock class, create the DisplayClock class, and tell it to subscribe to the event. You then will create a LogCurrentTime class and tell it to subscribe as well. Finally, you'll tell the Clock to run. All of this is shown in the following complete example:

Implementing Events with Delegates

namespace Programming_CSharp
{
  using System;
  using System.Threading;

  // a class to hold the information about the event
  // in this case it will hold only information 
  // available in the clock class, but could hold
  // additional state information 
  public class TimeInfoEventArgs : EventArgs
  {
    public TimeInfoEventArgs(int hour, int minute, int second)
    {
      this.hour = hour;
      this.minute = minute;
      this.second = second;
    }
    public readonly int hour;
    public readonly int minute;
    public readonly int second;
  }

  // our subject -- it is this class that other classes
  // will observe. This class publishes one delegate: 
  // OnSecondChange.
  public class Clock
  {
    // the delegate the subscribers must implement
    public delegate void SecondChangeHandler
      (
         object clock, 
         TimeInfoEventArgs timeInformation
      );

    // an instance of the delegate 
    public SecondChangeHandler OnSecondChange;

    // set the clock running
    // it will raise an event for each new second
    public void Run()
    {
      
      for(;;)
      {
        // sleep 10 milliseconds
        Thread.Sleep(10);
                
        // get the current time
        System.DateTime dt = System.DateTime.Now;

        // if the second has changed
        // notify the subscribers
        if (dt.Second != second)
        {
          // create the TimeInfoEventArgs object
          // to pass to the subscriber
          TimeInfoEventArgs timeInformation = 
            new TimeInfoEventArgs(
            dt.Hour,dt.Minute,dt.Second);

          // if anyone has subscribed, notify them
          if (OnSecondChange != null)
          {
            OnSecondChange(
              this,timeInformation);
          }
        }

        // update the state
        this.second = dt.Second;
        this.minute = dt.Minute;
        this.hour = dt.Hour;

      }
    }
    private int hour;
    private int minute;
    private int second;
  }

  // an observer. DisplayClock subscribes to the 
  // clock's events. The job of DisplayClock is 
  // to display the current time 
  public class DisplayClock
  {
    // given a clock, subscribe to 
    // its SecondChangeHandler event
    public void Subscribe(Clock theClock)
    {
      theClock.OnSecondChange +=
        new Clock.SecondChangeHandler(TimeHasChanged);
    }

    // the method that implements the 
    // delegated functionality
    public void TimeHasChanged(
      object theClock, TimeInfoEventArgs ti)
    {
      Console.WriteLine("Current Time: {0}:{1}:{2}",
        ti.hour.ToString(), 
        ti.minute.ToString(), 
        ti.second.ToString());
    }
  }

  // a second subscriber whose job is to write to a file
  public class LogCurrentTime
  {
    public void Subscribe(Clock theClock)
    {
      theClock.OnSecondChange +=
        new Clock.SecondChangeHandler(WriteLogEntry);
    }

    // this method should write to a file
    // we write to the console to see the effect
    // this object keeps no state
    public void WriteLogEntry(
      object theClock, TimeInfoEventArgs ti)
    {
      Console.WriteLine("Logging to file: {0}:{1}:{2}",
        ti.hour.ToString(), 
        ti.minute.ToString(), 
        ti.second.ToString());
    }
  }

  public class Test
  {
    public static void Main()
    {
      // create a new clock 
      Clock theClock = new Clock();

      // create the display and tell it to
      // subscribe to the clock just created
      DisplayClock dc = new DisplayClock();
      dc.Subscribe(theClock);

      // create a Log object and tell it
      // to subscribe to the clock 
      LogCurrentTime lct = new LogCurrentTime();
      lct.Subscribe(theClock);

      // Get the clock started
      theClock.Run();
    }
  }
}

Output:
Current Time: 14:53:56
Logging to file: 14:53:56
Current Time: 14:53:57
Logging to file: 14:53:57
Current Time: 14:53:58
Logging to file: 14:53:58
Current Time: 14:53:59
Logging to file: 14:53:59
Current Time: 14:54:0
Logging to file: 14:54:0

The net effect of this code is to create two classes, DisplayClock and LogCurrentTime, both of which subscribe to a third class' event (Clock.OnSecondChange).

OnSecondChange is a delegate. It starts out set to null. When the observer classes wish to be notified, they create an instance of the delegate and then add these delegates to OnSecondChange. For example, in DisplayClock's Subscribe method, you see this line of code:

theClock.OnSecondChange +=
  new Clock.SecondChangeHandler(TimeHasChanged);

It turns out that the LogCurrentTime class also wants to be notified. In its Subscribe method is very similar code:

public void Subscribe(Clock theClock)
{
  theClock.OnSecondChange +=
    new Clock.SecondChangeHandler(WriteLogEntry);
}

Problems with Delegates and Events

There is a problem with this example, however. What if the LogCurrentTime class was not so considerate, and it used the assignment operator (=) rather than the subscribe operator (+=)?

public void Subscribe(Clock theClock)
{
  theClock.OnSecondChange =
    new Clock.SecondChangeHandler(WriteLogEntry);
}

If you make that one tiny change to the example, you'll find that the Logger method is called but the DisplayClock method is not called. The assignment operator replaced the delegate held in the OnSecondChange multi-cast delegate. Not good.

A second problem is that other methods can call SecondChangeHandler directly. For example, you might add the following code to the Main() method of your Test class:

Console.WriteLine("Calling the method directly!");
System.DateTime dt = System.DateTime.Now.AddHours(2);

TimeInfoEventArgs timeInformation = 
  new TimeInfoEventArgs(
  dt.Hour,dt.Minute,dt.Second);

theClock.OnSecondChange(theClock, timeInformation);

Here, Main() has created its own TimeInfoEventArgs object and invoked OnSecondChange directly. This runs fine, even though it is not what the designer of the Clock class intended. Here is the output:

Calling the method directly!
Current Time: 18:36:7
Logging to file: 18:36:7
Current Time: 16:36:7
Logging to file: 16:36:7

The problem is that the designer of the Clock class intended the methods encapsulated by the delegate to be invoked only when the event is fired. Here Main() has gone around through the back door, and invoked those methods itself. What is more, it has passed in bogus data -- passing in a time construct set to two hours into the future!

Pages: 1, 2, 3

Next Pagearrow