ThinkingCog

Articles written by Parakh Singhal

Events and Delegates – Part 3 of 3

In part 2 of this series, I introduced delegates as the implementation of the callback mechanism in .net. We saw how a delegate act as a pipeline connecting code that generates information to the code that uses that information.

In this post, I will introduce events and how they fit neatly into the entire picture and complement the abstraction that we started with delegates.

Non-Technical Overview of Events

Events in real life are important occurrences which, when happen, result in something done. Similarly, events in object-oriented programming languages represent important occurrences which when exercised need something done.

Technical Overview

In the previous post we saw how we can use a delegate as a pipeline to broadcast the information from an object to the subscribers.

Events take this concept to the next level and defer the work of creating the underlying delegate to the compiler. All a developer has to worry about is creating the right kind of event that fits in the domain model.

Events can be created with the help of the “event” keyword. We have to use a user defined delegate or a .net framework provided delegate to let the compiler know what type of underlying delegate needs to be formed behind the scenes. To keep the article short and focus on understanding the concept, I will only cover the EventHandler delegate provided by the .net framework in this post.

The EventHandler delegate is provided as a ready to use delegate. Its signature comprises passing an object type and an object of the EventArgs class. The first parameter is an object of the class that exposes the event itself, and the EventArgs object represents additional information related to the emitted event that might not be contained in the object of the class itself. The delegate returns a void.

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

To pass any event information that might not be contained in the object, we can create a user-defined class derived from EventArgs class, and provide the peripheral information and functionality related to the emitted event via properties and methods.

The concepts covered in the technical overview will become a bit clearer in the example code covered in use cases.

Use Cases of Events

Events are calls that are raised when programmed conditions are met. Some examples are:

1. To broadcast changes in an object’s state
2. To broadcast fulfilment of a condition

We saw in the previous post how we have to create additional code to register and un-register methods that want to use the delegates. With the event keyword we can bypass all that boilerplate code and directly work with events and event handlers.

Broadcast changes in an object’s state

I am going to continue an example discussed in my previous post. Human beings are sensitive to their ambient temperature. In the code below, we will create a class “Human” and broadcast the response of its object to different temperatures. In my previous post, this was implemented with the help of delegates, but in this post, I will use events to implement the same.

public class Human
    {
        // Basic properties
        public int Id { get; set; }
        public string Name { get; set; }
        public float MinTemp { get; set; }
        public float MaxTemp { get; set; }
        private string feeling;
        public string Feeling
        {
            get
            {
                return feeling;
            }
            set
            {
                feeling = value;
 
                // If there are subscribers to the OnTemperatureChanged event
                // change in Feeling property will trigger off the event
                if (OnTemperatureChanged != null)
                {
                    OnTemperatureChanged(this, new EventArgs());
                }
            }
        }
 
        // Events
        public event EventHandler OnTemperatureChanged;
 
        public Human()
        {
 
        }
 
        // User defined constructor
        public Human(int id, string name, float minTemp, float maxTemp, string feeling)
        {
            Id = id;
            Name = name;
            MinTemp = minTemp;
            MaxTemp = maxTemp;
            Feeling = feeling;
        }
 
        public void TemperatureSensation(float temperature)
        {
 
            if (temperature > MaxTemp)
            {
                // If the OnTemperatureChanged event's invocation list is not empty
                // change in Feeling property's value will trigger the event to fire off
                Feeling = "I am feeling hot";
            }
            else if (temperature < MinTemp)
            {
                Feeling = "I am feeling cold";
            }
            else
            {
                Feeling = "I am feeling normal";
            }
        }
    }

In the code above, we have a class "Human" with five properties, of which three are of importance - MinTemp, MaxTemp and Feeling. The method TemperatureSensation accepts a float type parameter and depending on where the value stands in the spectrum between MinTemp and MaxTemp, sets the value of the Feeling property. We have an explicit implementation of the Feeling property and if there are subscribers to the OnTemperatureChanged event, it fires off when the value of the Feeling property’s underlying variable is set to a new value. In the following code, we create an object of the Human class and test the functionality of event declared in the Human class.

class Program
    {
        static void Main(string[] args)
        {
            Human human = new Human() 
            {
                Id = 1,
                Name = "Parakh Singhal",
                MaxTemp = 45f,
                MinTemp = 10f, 
                Feeling = null
            };
 
            human.OnTemperatureChanged += OnTemperatureChangeHandler;
 
            human.TemperatureSensation(8f);
            human.TemperatureSensation(25f);
            human.TemperatureSensation(50f);
 
            Console.WriteLine("Press any key to terminate the program...");
            Console.ReadKey();
        }
 
        public static void OnTemperatureChangeHandler(object sender, EventArgs e)
        {
            if (sender is Human)
            {
                Human human = sender as Human;
                Console.WriteLine(human.Feeling);
            }
        }
    }

 

In the code above we created an object of the Human class and instantiated it with some data. Then, we enrolled in a method “OnTemperatureChangeHandler” that matched the “OnTemperatureChanged“ event’s underlying delegate’s signature and return type. Then we invoked the TemperatureSensation method on the human object which then broadcasted the messages to the event’s subscribed members.

The code when executed provides the following output:

3 of n 01 Object State

Contrast this code with the one created in the previous post, where not only we operated on the back of a delegate object, but also created the registration and un-registration methods for the methods to get enrolled in the delegate’s invocation list, to prevent direct access to delegate object’s invocation list. We also had more lines of programming as we had to trigger off the delegate wherever the state of the object was deemed to change. The new code is not only concise but also more maintainable.

Here we leverage the code that gets created by C# compiler for us. The event keyword, behind the scenes gets expanded into a delegate accepting the standard input parameters of an object type and an EventArgs class object and returning a void. We can then enrol methods with the same signature and return type, using the overridden “+” operator. Methods can be un-registered using the “-“ operator.

Broadcast of fulfilment of a certain condition

Some conditions when they get fulfilled, warrant an intimation. The information about the fulfilment of condition is broadcasted via events and the subscribers then process the information as they deem fit.

Let’s take an example of a library where the information about successful checkout of a book is processed.

 

public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
public class Book
    {
        public int BookId { get; set; }
        public string ISBN { get; set; }
        public string Title { get; set; }
        public Author Author { get; set; }
        public int PageCount { get; set; }
        public bool IsCheckedOut { get; set; }
    }
public class Member
    {
        public int MemberId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int NumberOfBooksCheckedOut { get; set; }
        public List<Book> BooksCheckedOut { get; set; }
    }
public Library()
        {
            Books = new List<Book>()
                        {
                            new Book()
                            {
                                BookId = 1,
                                Title = "Alice in Wonderland",
                                IsCheckedOut = false,
                                PageCount = 200,
                                Author = new Author()
                                {
                                    AuthorId = 1,
                                    FirstName = "Lewis",
                                    LastName = "Carroll"
                                }
                            },
                            new Book()
                            {
                                BookId = 2,
                                Title = "Bad Blood",
                                IsCheckedOut = false,
                                PageCount = 350,
                                Author = new Author()
                                {
                                    AuthorId = 2,
                                    FirstName = "John",
                                    LastName = "Carreyrou"
                                }
                            },
                            new Book()
                            {
                                BookId =  3,
                                Title = "The Dream Machine",
                                IsCheckedOut = false,
                                PageCount = 250,
                                Author = new Author()
                                {
                                    AuthorId = 3,
                                    FirstName="Mitchell",
                                    LastName = "Waldrop"
                                }
                            },
                            new Book()
                            {
                                BookId = 4,
                                Title = "The Structure of Scientific Revolution",
                                IsCheckedOut = false,
                                PageCount = 500,
                                Author = new Author()
                                {
                                    AuthorId = 4,
                                    FirstName = "Thomas",
                                    LastName=  "Kuhn"
                                }
                            },
                            new Book()
                            {
                                BookId =5,
                                Title = "Sapiens: A Brief History of Humankind",
                                IsCheckedOut = false,
                                PageCount = 450,
                                Author =new Author()
                                {
                                    AuthorId = 5,
                                    FirstName = "Yuval",
                                    LastName = "Hariri"
                                }
                            }
                        };
            Members = new List<Member>()
                        {
                            new Member()
                            {
                                MemberId = 1,
                                FirstName = "Parakh",
                                LastName = "Singhal",
                                NumberOfBooksCheckedOut = 0
                            },
                            new Member()
                            {
                                MemberId = 2,
                                FirstName = "Prateek",
                                LastName = "Mathur",
                                NumberOfBooksCheckedOut = 0
                            },
                            new Member()
                            {
                                MemberId =3,
                                FirstName = "Sumant",
                                LastName = "Sharma",
                                NumberOfBooksCheckedOut = 0
                            }
                        };
        }
 
        public void CheckOutBook(int memberId, int bookId)
        {
            Book book = Books.Find(book => book.BookId == bookId);
            Member member = Members.Find(member => member.MemberId == memberId);
 
            if (book.IsCheckedOut)
            {

Console.WriteLine("Apologies, but book is already checked out.

Please select another book.");

            }
            else if (member.NumberOfBooksCheckedOut >= 2)
            {

Console.WriteLine("Apologies, but there are already 2 books

checked out to you.");

            }
            else
            {
                book.IsCheckedOut = true;
                member.NumberOfBooksCheckedOut += 1;

OnSuccessfulCheckOut?.Invoke(this,

new LibraryEventArgs() { Message = "Checkout of book is successful" });

 
            }
        }
    }
 
public sealed class LibraryEventArgs : EventArgs
    {
        public string Message { get; set; }
    }

 

The model created above is consumed in the console application.

class Program
    {
        static void Main(string[] args)
        {
            Library library = new Library();
 
            library.OnSuccessfulCheckOut += CheckoutSuccessfulEventHandler;
 
 
            library.CheckOutBook(1, 1);
 
            Console.WriteLine("Press any key to exit the program...");
            Console.ReadKey();
        }
 
        private static void CheckoutSuccessfulEventHandler(object sender, EventArgs e)
        {
            if (sender is Library && e is LibraryEventArgs)
            {
                LibraryEventArgs libraryEventArgs = e as LibraryEventArgs;
                Console.WriteLine(libraryEventArgs.Message);
            }
        }    
}

 

When executed the following output appears:

3 of n 02 Object State

The example shows usage of event for emitting information for an important activity with the help of event. In the example when the book is successfully checked out, the event OnSuccesfulCheckOut is fired, provided that the underlying delegate’s invocation list is not empty, and the information regarding the successful checkout of the book is emitted to the subscribed event handlers.

Summary

Delegates help separation with the process of emitting the information and processing that information, events take things to the next level by raising the level of abstraction and using a more natural vocabulary.

I hope that this post helped you in rounding out the concept of events and delegates and how events are built on the foundation of delegates, which is the reason, delegates are discussed first in academic texts.

References

  1. Pro C# 8 with .NET Core 3 by Andrew Troelsen and Phil Japikse
  2. CLR via C# by Jeffrey Richter
blog comments powered by Disqus