Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# Lectures - Lecture 5:Events, Delegates, Delegates Chain by C# example

0.00/5 (No votes)
2 Aug 2016 3  
This is 5th lecture of the set I'm giving. It is related to delegates and events

Full Lectures Set


Introduction

In this article I'm going to review quite sensitive topic that is difficult to understand and explain. We're going to talk about events, delegates and delegates chains. I'm not sure if my explanation of this topic will be very clear  for everyone. When I gave this lecture to my students we discussed a lot and clarified lots of things while these discussions. Here we don't have ability to discuss face to face and I have only text to share my thoughts. Hope this article will help you to get better understanding of its topic. If you find my article not clear or full, try other sources and examples.

Events

Events is the type of members that you can define in class. More about classes and their members in C# you can read in my article here.  Type that defines an event can notify other objects about some specific situation that happened with it or its objects. For example class KeyboardKey may define an event PressedDown. Now every object  that wants to know when the key was pressed down, should subscribe for the PressedDown and it will receive notification each time event happens. This functionality is realized by events type members. Events functionality is widely used in UI implementations when some graphical controls send notifications due to user actions or data changes. Modern UI libraries are built at events concept, but UI is not only the single place where events are commonly used. Events is the simplest and most convenient way for class to notify others about its status change and other object\s need to know about this change. Events model is built basing on delegates. We will review delegates in details later in this article and for events explanation we use delegates without diving into details.

You can see below the sequence diagram that explains how C# deals with events. In our example we have 3 classes: CellPhone, CellPhoneCallsLog and CallHandler. CellPhone class has event NewCallEvent that he raise when new call arrives from network. When we create objects of CellPhoneCallsLog and CallHandler they subscribe for NewCallEvent, this is action #1. When CellPhone receives NewCallEvent, this is action #2 it notifies all subscribes about it, this is action #3:

Let's go further and develop the example below as real classes and that are written in C# language:

Step 1 Declaring event member: To declare event member you need to use event keyword. Mostly events are open and have public visibility. The format of event declaration is following:

visibility modificator  + event keyword  +  event type  + event name

In our case declaration is:

public event EventHandler<CallEventArgs> NewCallEvent;

Let's review each part of event declaration:

  • Visibility modificator - as for any type member for event you need to define visibility by using one of visibility modificators. As I mentioned earlier for events public visibility is usually used.
  • Event keyword - here everything is clear just type event on your keyboard
  • Event type - this is very important type of event declaration. Event type is a delegate type of the event. Subscribes for this event should provide a method which prototype corresponds to EventType delegate. Event type delegate has specific format as well:

visibiliry modificator + delegate keyword + void keyword + type name + two parameters: first parameter of type object and second of type EventArgs (see next step about EventArgs). You may define your own delegate type that fits the format above but there is standard common EvenHandler delegate that is defined as:

public delegate void EventHandler<TEventArgs> (Object sender, TEventArgs e) where TEventArgs: EventArgs;

In most cases it is enough to use it.

  • Event name - is the name that you want to give to your event

Step 2 EventArgs: when event is raised, object that contains the event should pass additional information to objects that receive notification about the event. No matter if you want to pass some additional information or not, you must put object of class derived from EventArgs or of class EvenArgs to EventHandler delegate. If you don't pass any additional information with event you may not create new object of class EventArgs and use EventArgs.Empty  property instead. If you decided to  pass some additional information for event subscribers you need to implement you own class. There are few rules for this class:

  1. It must derive EventArgs class
  2. Its name must end with EventArgs. As in our example this is CallEventArgs
  3. All data that you want to share should be accessed by properties
  4. I will recommend to use read-only fields for EventArgs classes. This is how it is implemented in example that I provide below

Step 3 Defining method that responsible for notify subscribers about event: you need to define a specific method that is responsible for notification about event.  This should be protected virtual method that takes one parameter, object of EventArgs (or class that you defined) that contains additional information about event. Default implementation of this method checks if there re subscribers for event and invokes  it. According to Jeff Richter and other sources it is better to copy reference to event to temp variable and only then invoke it. This will prevent NullReferenceException when you try to invoke from one thread and second thread set reference to null before invoking. In our case implementations looks like:

protected virtual void OnNewCallEvent(CallEventArgs e)
{
         EventHandler<CallEventArgs> temp = System.Threading.Volatile.Read(ref NewCallEvent);
         if (temp != null)
          temp(this, e);
}

Step 4 Defining method that will raise the event: Once we completed with event definition we need to create method that will convert some input data to event that we want to raise. No specific rules here, you can raise event from any method of your class by calling virtual function described in step 3.

Step 5 Create class\es that will receive notifications about event: Now when we have class that creates and initialize event, let's create class that receives this event. This class should have methods which signature corresponds to delegate of event handled. This function actually will handle something when event happen. In our case this is:

public void AddNewCallToLog(object sender, CallEventArgs e)
{
                Console.WriteLine("Adding to log call with following data:");
                Console.WriteLine("Name:" + e.CallerName);
                Console.WriteLine("Number:" + e.CallerNumber);
                Console.WriteLine("Time:" + e.CallStartTime.ToString());
}

This class should be able to take object of class that has event defined and connect or disconnect  delegate method to event. I make them simple and below is example:

public void AttachListener(CellPhone phone)
{
               phone.NewCallEvent += AddNewCallToLog;
}
public void DetachListener(CellPhone phone)
{
               phone.NewCallEvent -= AddNewCallToLog;
}

Once we completed all 5 steps we can try to test what we created. Code below demonstrates things that we described in steps above:

        ///////////////////////////////////////////////EVENTS////////////////////////////////////////////
        internal sealed class CallEventArgs: EventArgs
        {
            private readonly string   m_CallerName;
            private readonly DateTime m_CallStartTime;
            private readonly string m_CallerNumber;
            public CallEventArgs(string callername, string callernumber, DateTime starttime)
            {
                m_CallerName = callername;
                m_CallerNumber = callernumber;
                m_CallStartTime = starttime;
            }
            public string CallerName
            {
                get { return m_CallerName; }
            }
            public string CallerNumber
            {
                get { return m_CallerNumber; }
            }
            public DateTime CallStartTime
            {
                get { return m_CallStartTime; }
            }
        }
        internal class CellPhone
        {
            public event EventHandler<CallEventArgs> NewCallEvent;
            protected virtual void OnNewCallEvent(CallEventArgs e)
            {
                //here we make a copy of event to make sure we don't
                //call on null
                EventHandler<CallEventArgs> temp = System.Threading.Volatile.Read(ref NewCallEvent);
                if (temp != null)
                    temp(this, e);
            }
            public void NewCallHappened(string username, string usernumber, DateTime time)
            {
                //create event data
                CallEventArgs eventData = new CallEventArgs(username, usernumber, time);
                //raise the event
                OnNewCallEvent(eventData);
            }
        }
        internal sealed class CellPhoneCallsLog
        {
            public void AttachListener(CellPhone phone)
            {
                phone.NewCallEvent += AddNewCallToLog;
            }
            public void DetachListener(CellPhone phone)
            {
                phone.NewCallEvent -= AddNewCallToLog;
            }
            private void AddNewCallToLog(object sender, CallEventArgs e)
            {
                Console.WriteLine("I'm CallsLog and handling this call by adding to log following data:");
                Console.WriteLine("Name:" + e.CallerName);
                Console.WriteLine("Number:" + e.CallerNumber); 
                Console.WriteLine("Time:" + e.CallStartTime.ToString());
            }
        }
        internal sealed class CallHandler 
        {
            public CallHandler(CellPhone phone)
            {
                phone.NewCallEvent += HandleTheCall;
            }
            private void HandleTheCall(object sender, CallEventArgs e)
            {
                Console.WriteLine("I'm CallHandler and handling this call by:");
                Console.WriteLine("-ringing music");
                Console.WriteLine("-vibrate");
                Console.WriteLine("-show caller information at screen");
            }
            public void DetachFromNewCallEvent(CellPhone phone)
            {
                phone.NewCallEvent -= HandleTheCall;
            }
        }
Usage:
Console.WriteLine("---------------------------------EVENTS------------------------:");
CellPhone phone = new CellPhone();
CellPhoneCallsLog log = new CellPhoneCallsLog();
CallHandler handler = new CallHandler(phone);
log.AttachListener(phone);
Console.WriteLine("----------------Calling first time with two listeners:");
phone.NewCallHappened("sergey", "1234567", DateTime.Now);
handler.DetachFromNewCallEvent(phone);
Console.WriteLine("----------------Calling again without CallHandler:");
phone.NewCallHappened("sergey", "1234567", DateTime.Now);

Once event is defined and you build your project .NET compiler generates code to register and unregister from events. I will not dive into details of this here, but if you want understand how it works I recommend you to read some sources that explain this. You can start from Jeff Richter book "CLR via C#". Briefly declaration of delegates generates two methods add_<Event Name> and remove_<Event Name>. These methods implement realization of subscription for event. Instead of writing these functions all the time all you need is to define delegate in your class, all the rest .NET will do for you in standard way.

Delegates by example

Delegates is a C# name for such called callback functions. Callbacks are successfully used many years in different programming languages, C# is not exception. If using callbacks in some programming languages is difficult and it is very sensitive area, in C# .NET developers make your life easy. In comparison to other languages C# has great support for delegates. They have more wide functionality and are more powerful and safe that in other programming languages as non managed C++, for example. Callbacks are used in .NET very often: windows states, menu items change, file system changes, asynchronous operations, etc. these are  places where you can register your function to receive notifications. Below I'm going show what delegate is and how to use it in your program.

Let's start from example and create delegate in C#:

  1. First of all to use any delegate you need to declare new delegate type and specify callback function signature. For this purpose you need to use delegate keyword:
    internal delegate string DelegateForOutput(string s);

    Above I declared delegate type that creates signature for function that returns type string and receive one string parameter as input.

  2. Once we have delegate type we should implement function that invokes it.
  3. Next step is to pass delegate to function that executes it. Here we can proceed with static methods and object methods. If you want to pass static method as delegate all you need is to define static function that fits delegate signature and pass it to function that invokes delegate. If you want to  pass delegate of an instance you can do it similar to static way but instead of type name you use object name.

Code below demonstrates things that you read in this part of the article:

internal delegate string DelegateForNotification(string s);

 internal class Notificator
 {
     public void DemoNotification(string s, DelegateForNotification outputFunction)
     {
         Console.WriteLine("we're going to process string: " + s);
         if (outputFunction != null) //if we have callbacks let's call them
         {
             Console.WriteLine("Function that is used as delegate is: " + outputFunction.Method);
             Console.WriteLine("Delegate output is: " + outputFunction(s));
         }
         else
         {
             Console.WriteLine("Sorry, but no processing methods were registered!!!");
         }
     }
 }
 internal class NotificationHandler
 {
     public string SendNotificationByMail(string s)
     {
         Console.WriteLine("We are sending notification:\"" + s + "\" by MAIL");
         return "MAIL NOTIFICATION IS SENT";
     }
     public string SendNotificationByFax(string s)
     {
         Console.WriteLine("We are sending notification: \"" + s + "\" by FAX");
         return "FAX NOTIFICATION IS SENT";
     }
     public static string SendNotificationByInstantMessenger(string s)
     {
         Console.WriteLine("We are sending notification: \"" + s + "\" by IM");
         return "IM NOTIFICATION IS SENT";
     }
 }

Usage:

//------------------DELEGATES-----------
Console.WriteLine("---------------------------------DELEGATES------------------------:");
Notificator notificator = new Notificator();
NotificationHandler notification_handler = new NotificationHandler();
//calling static delegate
notificator.DemoNotification("static example", null);//nothing happen here we don't pass any handler
notificator.DemoNotification("static example", new DelegateForNotification(NotificationHandler.SendNotificationByInstantMessenger));//nothing happen here
//calling instance delegate
notificator.DemoNotification("instance example", null);//nothing happen here we don't pass any handler
notificator.DemoNotification("instance example", new DelegateForNotification(notification_handler.SendNotificationByMail));
notificator.DemoNotification("instance example", new DelegateForNotification(notification_handler.SendNotificationByFax));
//passing null instead of correct value, it builds well
NotificationHandler nullhandler = null;
try 
{
notificator.DemoNotification("instance example", new DelegateForNotification(nullhandler.SendNotificationByFax));
}
catch (System.ArgumentException e)
{
Console.WriteLine("We've tried to pass null object for delegate and catched: " + e.Message);
}

Delegates behind the scene

Everything looks simple when we declared and used delegates in previous section. This happens due to huge background work that .NET does work us to make usage of delegates simple. Once you declare delegate in your code, CLR while compilation generates separate class for this delegate. This class is not visible in your code, but if you disassemble your binary file, you'll see it. All delegates classes are derived from System.MulticastDelegate. From this class every delegate receives 4 methods:

  • Constructor - that receives an object and pointer to function
  • Invoke -in example above when I call outputFunction(s) there is no such function and behind the scene compiler replaces it to outputFunction.Invoke(s). Signature of invoke is the same as signature of desired method inside Invoke compiler calls _methodPtr on _target (see below for their description)
  • BeginInvoke - not so relevant to review as this way of asynchronous work is obsolete
  • EndInvoke - not so relevant to review as this way of asynchronous work is obsolete

From MulticastDelegate every delegate derives not only methods but also fields. Most important from fields are:

  • _target - this field is null if delegate is for static method, but if it is for instance method, this field contains reference to object that has a delegate method
  • _methodPtr - holds identificator for callback method
  • _invocationList - usually this value is null, it is used for building chain from delegates

As delegate is class you may declare delegate in any place where class may be declared. Class that is generated for delegate has the same visibility modificator as delegate itself.

Delegates chain

Delegates are cool by themselves, but you can do the more cool and useful thing with them and build a chain of delegates. Chain is a set of delegates that gives ability call all methods that are presented by these delegates. Delegates chain is very convenient way when you need to call several delegates one by one.  It helps you to make more short and efficient code for this. To use the chain you need to declare variable of delegate  type and don't assign any method for it. We use null for initial initialization of this variable.  Once variable for chain is defined you can use two options to add more delegates to chain: using static method Combine of class Delegate or use += operator that simplifies combining to chain. Opposite methods to remove delegate from chain is to use Remove method of Delegate class or -= operator. Each time we add new delegate to chain _invocationList (see previous section)  that is array of delegate pointers grows with one more member. Once we pass delegate  chain to function that wait for delegate it analyzes that _invocationList isn't equal to null and works with delegates chain not with single delegate. It calls all delegates in chain in order how they were added to chain. Code below demonstrates the usage of delegates chain basing on classes that we created in code for section #3:

//------------------DELEGATES CHAIN-----------
Console.WriteLine("-----------------------------DELEGATES CHAIN--------------------:");
DelegateForNotification dlg1 = new DelegateForNotification(NotificationHandler.SendNotificationByInstantMessenger);
DelegateForNotification dlg2 = new DelegateForNotification(notification_handler.SendNotificationByMail);
DelegateForNotification dlg3 = new DelegateForNotification(notification_handler.SendNotificationByFax);

DelegateForNotification dlg_chain = null;
dlg_chain += dlg1;
dlg_chain += dlg2;
dlg_chain = (DelegateForNotification)Delegate.Combine(dlg_chain,dlg3);//more complex way to combine delegate to chain

//you can view invocation list
Delegate[] list = dlg_chain.GetInvocationList();
foreach (Delegate del in list)
{
    Console.WriteLine("Method: " + del.Method.ToString());
}
notificator.DemoNotification("chain example", dlg_chain);//calling notification to chain from 3 functions

Sources

  1. Jeffrey Richter - CLR via C#
  2. Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
  3. https://msdn.microsoft.com

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here