Understanding Events in C#

Tags: csharp dotnet technical

Events in CSharp

Events in C# follow the pub-sub model where the publisher class holds a list of subscribers which let the publisher know that they are waiting for something interesting to happen and the publisher should make them aware if the interesting thing does happen.

Event handling in C# requires one to understand delegates: which are boxes which can hold methods of a particular shape and can call them at runtime. Since this article is not about delegates, I am skipping them.

Anyhow, the publiser has to do two things:

  • Expose an event.
  • Raise the event when the interesting thing happens.

The subscribers too have to do two things:

  • Show their intent to the publisher and subscribe to the event
  • When the interesting thing has happened, and these have been made aware, then do something (subscribers may or may not receive extra data from the publishers).

Event Structure:

At a very high level, an event can be considered an object with a list of subscriber delegates, each of which is called when the interesting thing happens. Since this is a list of delegates, each one has to have the same “shape”.

For instance, the simplest event contains the delegates of type EventHandler and is defined like:

// EventName contains a list of delegates of type EventHandlers
public event EventHandler EventName;

where EventHandler has the following definition:

public delegate void EventHandler(object sender, EventArgs e);

so EventHandler has return void and should take two parameters of type object and EventArgs respectively. The first param is the sender and EventArgs is the class which contains event data.

Diving in Code:

Publisher Code:

The publisher has two options when it raises the event: it can either send some extra data to the subscribers, or it can simply let them know that something interesting has happened at its end. Let’s take the latter case first:

Consider a publisher class which reads names from console and tries to find a particular name. When it does find one, it thinks this is an interesting thing to have happened and wants to tell the same to its subscribers.

namespace EventExamples
{
    public class NameDetector
    {
        private string _name;
        public event EventHandler NameDetected;

        public NameDetector (string name)
        {
            _name = name;
        }


        public void Detect(string name) {
            // Interesting thing happened, raise event
            // via a helper method
            if (String.Equals(name, _name)) {
                OnNameDetected ();
            }
        }

        public void OnNameDetected() {
            // If NameDetected is null, then the event has no
            // subscribers, don't call it.
            if (NameDetected != null) {
                NameDetected (this, EventArgs.Empty);
            }
        }
    }
}

At this point, the publisher has done it’s job of exposing an event to the subscribers and having the necessary code in place to raise the event in case something interesting happens.

Subscriber’s code:

Now the subscriber has to do its two things: create a handler to handle the event published by the publisher and show the interest in the event by attaching the handler.

The event handler needs to have the same shape as listed in the publisher’s event. In this case it will be:

public static void SubscribersEventHandler(object sender, EventArgs e) {...}

Attaching the event handler is done by using += operator on publisher’s event:

publisherObject.Event += SubscribersEventHandler; // This is not a call

The entire code can be written as:

namespace EventExamples
{
    class MainClass
    {
        public static void Main (string[] args)
        {
            NameDetector detector = new NameDetector ("apple");
            detector.NameDetected += detector_NameDetected;

            while (true) {
                var name = Console.ReadLine ();
                detector.Detect (name);
            }

        }

        public static void detector_NameDetected(object sender, EventArgs e) {
            Console.WriteLine ("Name has been detected");
            Console.ReadKey ();
            Environment.Exit (0);
        }
    }
}

Output is:

aoesuth
aeosuth
ueaou
',.p',.p
apple
Name has been detected

Press any key to continue...

Sending and Receiving Data:

The Pub-Sub model can also be used to send data one-way from the publisher to the subscriber. In the code above, EventArgs class contains the generic event data, and we can extend it to send more data to the subscribers.

There are a few changes that have to be made to the publisher’s code:

  • Create a child class extending EventArgs, let’s call it MyEventArgs. This will hold the extra data coming from the publisher.
  • Instead of using EventHandler while creating event, use EventHandler<MyEventArgs>, which has the following definition:
public delegate void EventHandler<TEventArgs> (object sender, TEventArgs e);

The following code sends a DateTime object of when the correct name was detected.

namespace EventExamples
{
    public class NameDetector
    {
        private string _name;
        public event EventHandler<NameDetectorEventArgs> NameDetected;

        public NameDetector (string name)
        {
            _name = name;
        }


        public void Detect(string name) {
            // Interesting thing happened, raise event
            // via a helper method
            if (String.Equals(name, _name)) {
                OnNameDetected (DateTime.Now);
            }
        }

        public void OnNameDetected(DateTime detectionTime) {
            // If NameDetected is null, then the event has no
            // subscribers, don't call it.
            if (NameDetected != null) {
                var args = new NameDetectorEventArgs (detectionTime);
                NameDetected (this, args);
            }
        }
    }

    public class NameDetectorEventArgs : EventArgs {
        public DateTime DetectionTime {
            get;
            set;
        }

        public NameDetectorEventArgs (DateTime datetime)
        {
            DetectionTime = datetime;
        }

    }
}

The necessary changes have to be done in the subscriber too, so the event handler
will also use MyEventArgs instead of EventArgs:

namespace EventExamples
{
    class MainClass
    {
        public static void Main (string[] args)
        {
            NameDetector detector = new NameDetector ("apple");
            detector.NameDetected += detector_NameDetected;

            while (true) {
                var name = Console.ReadLine ();
                detector.Detect (name);
            }

        }

        public static void detector_NameDetected(object sender, NameDetectorEventArgs e) {
            Console.WriteLine ("Name has been detected at {0}", e.DetectionTime);
            Console.ReadKey ();
            Environment.Exit (0);
        }
    }
}

This is how the program will work:

aoesuth
aeosuth
ueaou
',.p',.p
apple
Name has been detected at 02-09-2015 16:49:09

Press any key to continue...