Monday, July 10, 2023

C# Event, Publisher and Subscriber classes

In this post, we will learn about Event. As the concept of event is closely related with delegate, the understanding of delegate is pre-requisite. Please go through Delegate post before going ahead this post.

What is an event?

An event is a way to send message from an object to another object to signal the occurrence of an action by the former object. In event, two objects sender and receiver of message or notification are involved. They are also known as publisher and subscriber objects.

We already know that delegate can be used to send the message from one object to another. Event is another way to do the same. Delegate works as a pipeline/medium to pass the message/data from sender to receiver. Delegate wraps the method which is to be executed when event is raised.

Before using event to send message from one object to another, we see how message can be propagated using delegate. Thereafter, we will see how the same can be done using Event.

Using Delegate Instead of Event for Notification

Create a console project in Visual Studio. Create Button class. Let’s suppose that we have Button class which has some events like click, double click etc.

Assume that for click event, we create a delegate so that when button is clicked, data will be sent from Button class to the subscriber class. It means that delegate will act as a medium or pipeline to pass data from Button class to Program class. In this example, Program class will be subscriber or receiver class.

Look at the following code in Button class.

public delegate void ClickDel(string message); //Step1
class Button
{
    public ClickDel click; // Step2
    public void ClickEventRaiser() // Step3
    {
        OnClick(); // Step4
    }

    private void OnClick()
    {
        if (click != null) // Step5
        {
            var data = "Some message from Button";
            click.Invoke(data); // Step6
        }
    }
}

Step1: We create a delegate which can be used to invoke any method which takes one string type parameter and returns void. This delegate will be used to send message from Button class to any other class. Publisher uses delegate to notify subscriber. In forthcoming paragraphs, we will see how instead of delegate event can be used for notification to subscriber classes.

Step2:  The click is delegate type reference variable. Note that delegate ClickDel is a type and so it is good convention to define it separately outside a class. So, we have defined it outside Button class. Please note that delegate is not initialized yet in Button class.

Step3: We have defined ClickEventRaiser method which will do some work and will raise event inside its body. When event (e.g. click) is raised - what will happen- its logic should be written in a separate method.

Step4: The logic of event (on click) is called. Note that on calling this method, event will be raised.

Step5: This step is crucial. It checks whether click delegate declared in Step2 is initialized or not. What it means? It means whether click delegate is subscribed in client Program class or not. This step will become clear when we look at Program class code. This step is linking publisher class to subscriber class. We will show how publisher class event (e.g. click) is subscribed by client class.

Step6: This step is also crucial. It shows that click delegate is invoked and message/data is sent using argument. We have initialized data inside OnClick method. We could have passed data from ClickEventRaiser method as well. I have avoided this to avoid confusion for beginners. We just have to remember that delegate is invoked inside the method which is called when event is raised.

Now, look at the following code in Program class. Program is client or subscriber class.


using System;
// Main program
class Program
{
    static void Main(string[] args)
    {
        Button button = new Button();
        //subscribe click event with Client_ClickHandler
        button.click += Client_ClickHandler;
        // call a method of Button which will in turn raise event
        button.ClickEventRaiser();
    }
    private static void Client_ClickHandler(string message)
    {
        if (message != null)
        {
            Console.WriteLine("Button is clicked " + message);
        }
    }
}

Step1: The Program class creates an object of Button class to subscribe/consume event(s) of Button.

Step2: The button object uses dot operator followed by event click. It implies that click event of button object is going to be subscribed by Program class. But to subscribe this event the client class must tell the method which will handle the event data when it will be received by Program object. In RHS operand of += we specify the event handler method. This event handler will receive event data. Note that the event handler method must have compatible signature to accept all the data sent from publisher Button class. As there is only one data sent which is string type, so, we have a compatible event handler in Program class called Client_ClickHandler. This method takes one parameter of string type.

Step3: Subscribing event in Step2 is not enough. The subscriber class must call a method of Button class which will raise event. The method ClickEventRaiser in Button class is therefore called.

Step4: When event will be raised, the data will be sent from Button using delegate. That data/message will be received by event handler called Client_ClickHandler.

The above is the simple example to explain click event which is implemented using delegate instead of Event.

Another Delegate for Double Click event

What if Button class has another event called dblclick implemented using another delegate called DblClickDel. In the following code, we modify the publisher (Button) and subscriber(Program) classes to add another delegate for double click event.

public delegate void ClickDel(string message);
public delegate void DblClickDel(string message);
class Button
{
    public ClickDel click;
    public DblClickDel dblclick;
    public void ClickEventRaiser()
    {
        OnClick("Inside ClickEventRaiser");
    }

    private void OnClick(string str)
    {
        if (click != null)
        {
            click.Invoke(str);
        }
    }
    public void DoubleClickEventRaiser()
    {
        OnDblClick("Inside ClickEventRaiser");
    }

    private void OnDblClick(string str)
    {
        if (dblclick != null)
        {
            dblclick.Invoke(str);
        }
    }
}
Look at the client Program class code as given below. Read each comment carefully. The dblclick event is subscribed in way similar to click event. We have used separate event handler for dblclick event. In this example, we could have used the same event handler which is used for single click event (because both event handlers have similar method signature). Note that both delegates have same signature. If they had different signatures then event handler for both events must be different. 

using System;
// Main program
class Program
{
    static void Main(string[] args)
    {
        Button button = new Button();
        //subscribe click event with Client_ClickHandler
        button.click += Client_ClickHandler;
        // call a method of Button which will in turn raise event
        button.ClickEventRaiser();

        //subscribe dblclick event with Client_DblClickHandler
        button.dblclick += Client_DblClickHandler;
        // call a method of Button which will in turn raise event
        button.DoubleClickEventRaiser();
    }
    private static void Client_DblClickHandler(string message)
    {
        if (message != null)
        {
            Console.WriteLine("Button is double clicked " + message);
        }
    }

    private static void Client_ClickHandler(string message)
    {
        if (message != null)
        {
            Console.WriteLine("Button is clicked " + message);
        }
    }
}

Using Event Instead of Delegate

In above examples, we have used delegates for event programming. C# provides Event to facilitate the coding. Event provides better syntax compared to delegate and provides encapsulation. We modify the above examples for Event programming.


public delegate void ClickDel(string message);
class Button
{
    public event ClickDel click;
    public void ClickEventRaiser()
    {
        OnClick("Inside ClickEventRaiser");
    }
    private void OnClick(string str)
    {
        if (click != null)
        {
            click.Invoke(str);
        }
    }
}

Note that we have used event keyword to convert click delegate into click event. The client code is as follows. It is not modified.

using System;
// Main program
class Program
{
    static void Main(string[] args)
    {
        Button button = new Button();
        //subscribe click event with Client_ClickHandler
        button.click += Client_ClickHandler;
        // call a method of Button which will in turn raise event
        button.ClickEventRaiser();
    }
    private static void Client_ClickHandler(string message)
    {
        if (message != null)
        {
            Console.WriteLine("Button is clicked " + message);
        }
    }
}

We define an event using delegate. The event keyword is followed by delegate type and then an event variable is used to declare an event. The above code does not seem to be better in any way that when we used only delegate (not event). The real benefit of event is obvious when we use build-in delegate in event programming. In stead of custom delegates ( ClickDel or DblClickDel ), we use built-in delegate in the following modified code.

Button class code is as follows.


class Button
{
    public event EventHandler click; // EventHandler is built-in delegate
    public void ClickEventRaiser()
    {
        OnClick();
    }
    private void OnClick()
    {
        if (click != null)
        {
            click.Invoke(this, EventArgs.Empty);
        }
    }
}

Program class code is modified for event handler method because we have used EventHandler built-in method in Button class. The EventHandler delegate takes two parameters of types object and EventArgs. So, we use Client_ClickHandler with 2 parameters compatible with EventHandler.


using System;
// Main program
class Program
{
    static void Main(string[] args)
    {
        Button button = new Button();
        //subscribe click event with Client_ClickHandler
        button.click += Client_ClickHandler;
        // call a method of Button which will in turn raise event
        button.ClickEventRaiser();
    }
    private static void Client_ClickHandler(object Sender, EventArgs e)
    {
        Console.WriteLine( Sender.ToString()+ " is clicked " +   e.ToString());
    }
}
The following is old post.

Event, Publisher and Subscriber classes

Event is message which is sent from event sender class (i.e. publisher) to the event receiver (i.e. subscriber) class to signal the occurrence of an action. Here, action is the method in the sender class in which event is invoked. Event sender class is the publisher class that contains event definition and its invocation i.e. raising the event in a method.

Delegate Model

The event is defined using event keyword which is followed by a delegate and then the event name. Event is intimately related with delegate. The delegate before the event name implies what kind of method can be invoked when event occurs. In fact, event is a wrapper of delegate. Event is therefore said to be based on delegate model. The visibility of event is public. In the following code, EventHandler is an in-built delegate. EventHandler takes two parameters- sender object and event data. You can define an event using custom delegate as well but is rare in practice. EventHandler and its derived classes are enough to deal with events.

In the following code, an event named as ThresholdReached is defined in event class, Counter.


class Counter {
  public event EventHandler ThresholdReached;
  
  }


Raise Event using protected virtual method

Defining an event is not enough unless we define how the event is raised. Typically, to raise the event, we add a method that is marked as protected and virtual in the publisher class. Conventionally, the name of this method begins with On prefix followed by the event name e.g. OnThresholdReached. Look at the following code; the method OnThresholdReached is run then the ThresholdReached event is raised. Note that when event  ThresholdReached is raised then event sender object and event data (EventArgs) are passed as parameters. The event data object is an object of type EventArgs or its derived type.


class Counter
{
    public event EventHandler ThresholdReached; // define event

    protected virtual void OnThresholdReached(EventArgs e)
    {
        ThresholdReached?.Invoke(this, e); // raise the event
    }

    // provide remaining implementation for the class
}

Subscriber class

Raising event is received by another entity, called subscriber of the event. Events in .NET are based on the delegate model. The delegate model follows the observer design pattern, which enables a subscriber to register with and receive notifications from a provider. An event sender pushes a notification that an event has happened, and the subscriber i.e. the event receiving class receives that notification and defines a response to it. The response is defined using an event handler method.

Recall that an event is a message sent by an object to signal the occurrence of an action. For example, the action can be caused by user interaction, such as a button click, or it can result from some other program logic, such as changing a property's value. The object that raises the event is called the event sender or publisher. 

The event sender i.e. publisher doesn't know which object or method will receive (handle) the events it raises.

Delegates with events

Delegates have many uses in .NET. In the context of events, a delegate is an intermediary (or pointer-like mechanism) between the event source and the code that handles the event. You associate a delegate with an event by including the delegate type in the event declaration. In other words, event is wrapper of delegate. Note that the event is an intermediary between publisher and subscriber objects.

EventHandler

.NET provides the EventHandler and EventHandler delegates to support most event scenarios. Use the EventHandler delegate for all events that don't include event data. Use the EventHandler delegate for events that include data about the event. These delegates have no return type value and take two parameters (an object for the source of the event and an object for event data).

A delegate acts as an event dispatcher for the class that raises the event by maintaining a list of registered event handlers for the event.

What is event handler?

Event handler is a method in the subscriber class which receives the event. To respond to an event, you define an event handler method in the subscriber i.e. event receiving class. This event handler method must match the signature of the delegate for the event for which the handling code is written. In the event handler method, we perform the actions that are required when the event is raised, such as print a message after the user clicks a button. To receive notifications when the event occurs, event handler method must subscribe to the event.

Therefore, we create subscriber class in which we define the event handler method. We create an instance of the publisher class in this subscriber class. We attach the event raising method of the publisher class with this instance and then use += operator to bind it with the event handler method. Look at the following code.


class Subscriber
{
    static void Main()
    {
        var c = new Counter(); // instance of publisher
        c.ThresholdReached += SubscriberThresholdReached; // event handler delegate in right hand

        // provide remaining implementation for the class
    }
    // event handler method
    static void SubscriberThresholdReached(object sender, EventArgs e)
    {
        Console.WriteLine("The threshold was reached.");
    }
}

Static and dynamic event handlers

.NET allows subscribers to register for event notifications either statically or dynamically. Static event handlers are in effect for the entire life of the class whose events they handle. Dynamic event handlers are explicitly activated and deactivated during program execution, usually in response to some conditional program logic. For example, they can be used if event notifications are needed only under certain conditions or if an application provides multiple event handlers and run-time conditions define the appropriate one to use.

.NET provides the EventHandler and EventHandler<TEventArgs> delegates to support most event scenarios. Use the EventHandler delegate for all events that don't include event data. Use the EventHandler<TEventArgs> delegate for events that include data about the event. These delegates have no return type value and take two parameters (an object for the source of the event and an object for event data).

Steps in Event Programming

  1. Define a publisher class.
  2. Define event in the publisher class.
  3. Define an event raising method in publisher class.
  4. Raise the event in the a method in publisher class.
  5. Define a subscriber class.
  6. Create an object of the publisher class in it.
  7. Subscribe the publisher event with event handler method of subscriber using += operator. The += is used to register or subscribe the event with event handler given as RHS operand. The -= is used to unregister or unsubscribe event with event handler.
  8. Define the event handler method in the subscriber class.

Example 1- Event Raised and Subscribed  The following example shows how to raise and consume an event that doesn't have data. It contains a class named BusinessProcess that has an event called ProcessCompleted. This event is raised when OnProcessCompleted action is called in ProcessStart metod. Just after raising the event, the control goes to subscriber class called Program. This class invokes event handler method called Pb_ProcessCompleted. The method signature of this Pb_ProcessCompleted must match that of Notify delegate.


using System;
namespace ConsoleApp1
{
    public delegate void Notify(); // custom delegate defined
    class Program
    {
        static void Main(string[] args)
        {
            //ProcessCompleted event will be raised when ProcessStart runs.
            // as it contains the OnProcessCompleted event raiser method.
            // so the line of code with Pb_ProcessCompleted is executed after pb.ProcessStart
            BusinessProcess pb = new BusinessProcess();
            pb.ProcessCompleted += Pb_ProcessCompleted;
            pb.ProcessStart();

        }

        private static void Pb_ProcessCompleted()
        {
            Console.WriteLine( "Process is complete");
        }
    }
    class BusinessProcess // publisher class
    {
        public event Notify ProcessCompleted; // event defined
        public void ProcessStart()
        {
            Console.WriteLine("Process Started!");
            OnProcessCompleted(); // invoke the event raiser method
        }

        protected virtual void OnProcessCompleted()
        {
            Console.WriteLine( "Invoke the event");
            ProcessCompleted?.Invoke(); // event raised, will be handled by event handler method
        }
    }
}

Example 2- Event without event data The following example shows how to raise and consume an event that doesn't have event data. It contains a class named Counter that has an event called ThresholdReached. This event is raised when a counter value equals or exceeds a threshold value. The EventHandler delegate is associated with the event because no event data is provided. The event raising Add method of event sender class is called again and again. The Add method is defined so that it invokes the event named as ThresholdReached. Note as per convention, it should be named OnAdd but it is not mandatory.


using System;

namespace ConsoleApplication1
{
    class ProgramOne
    {
        static void Main(string[] args)
        {
            Counter c = new Counter(new Random().Next(10));
            c.ThresholdReached += c_ThresholdReached;

            Console.WriteLine("press 'a' key to increase total");
            while (Console.ReadKey(true).KeyChar == 'a')
            {
                Console.WriteLine("adding one");
                c.Add(1);
            }
        }

        static void c_ThresholdReached(object sender, EventArgs e)
        {
            Console.WriteLine("The threshold was reached.");
            Environment.Exit(0);
        }
    }

    class Counter
    {
        private int threshold;
        private int total;

        public Counter(int passedThreshold)
        {
            threshold = passedThreshold;
        }

        public void Add(int x)
        {
            total += x;
            if (total >= threshold)
            {
                ThresholdReached?.Invoke(this, EventArgs.Empty);
            }
        }

        public event EventHandler ThresholdReached;
    }
} 

Examples 3.1,3.2 and 3.3 are based on following facts.

  • User inputs two integer numbers.
  • If the sum of these two numbers is greater than 100 then raise an event.
  • The user will be notified that the sum is greater than 100.
  • In example 3.3, event returns sum data to the event handler method.

Example 3.1- Event without event data using custom delegate


using System;
namespace ConsoleApp1
{
    class Program // subscriber class
    {
        static void Main(string[] args)
        {
            Publisher publisher= new Publisher();
            publisher.notify += Publisher_notify; // delegate match
            publisher.Add(10,99);
        }

        private static void Publisher_notify() // event handler method
        {
            Console.WriteLine("Sum is greater than 100.");
        }
    }
    public delegate void Notify();
    class Publisher
    {
        public event Notify notify;

        public void Add(int num1, int num2) 
        {
            int sum = num1 + num2;
            if (sum > 100)
            {
                notify.Invoke(); // raise the event
            }
        }
    }
}

Example 3.2- Event without event data using in-build delegate EventHandler

The EventHandler takes two parameters, sender object and EventArgs type event data. This is why we use two arguments with Invoke method. In the subscriber, the event handler method must have the same types of parameters.

using System;
namespace ConsoleApp1
{
    class Program// subscriber class
    {
        static void Main(string[] args)
        {
            Publisher publisher = new Publisher();
            publisher.notify += Publisher_notify; // delegate match
            publisher.Add(10, 99);
        }

        private static void Publisher_notify(object sender, EventArgs e)
        {
            Console.WriteLine("Sum is greater than 100.");
        }
    }
    class Publisher
    {
        public event EventHandler notify;

        public void Add(int num1, int num2)
        {
            int sum = num1 + num2;
            if (sum > 100)
            {
                notify.Invoke(this, EventArgs.Empty); // raise the event
            }
        }
    }
}

Example 3.3- Event with event data using in-build delegate EventHandler

In this example, integer type data is sent by the publisher to the subscriber. The event handler consumes that data.

using System;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Publisher publisher = new Publisher();
            publisher.notify += Publisher_notify;
            publisher.Add(10, 99);
        }

        private static void Publisher_notify(object sender, int e)
        {
            Console.WriteLine("Sum "+ e.ToString() +" is greater than 100.");
        }
    }
    class Publisher
    {
        public event EventHandler<int> notify;

        public void Add(int num1, int num2)
        {
            int sum = num1 + num2;
            if (sum > 100)
            {
                notify.Invoke(this, sum);
            }
        }
    }
}

Summary

  • The class who raises events is called Publisher, and the class which receives the notification is called Subscriber
  • Typically, a publisher raises an event when some action occurred.
  • There can be multiple subscribers of a single event.
  • Event is an encapsulated delegate i.e. a wrapper around a delegate. It depends on the delegate.
  • The delegate defines the signature for the event handler method of the subscriber class. The signature of the handler method must match the delegate signature.
  • Events in .NET follow the observer design pattern.
  • Use "event" keyword with delegate type variable to declare an event.
  • Register with an event using the += operator. Unsubscribe it using the -= operator.
  • Pass event data using EventHandler<TEventArgs>.
  • Derive EventArgs base class to create custom event data class.
  • Event is better than delegate in the sense of encapsulation and succinct syntax. 

No comments:

Post a Comment

Hot Topics