In my last post, I explained the concept of events, delegates, and event handlers at a high level. In this post, let’s take a look at delegates. Why delegates and not events, because among them, they are the tricky ones and can be understood in relative isolation.
Delegates form the glue connecting the events and event handlers and are instrumental in conveying to all the subscribed event handlers about the firing of an event. What I will try to show in this post are the following:
1. High-level conceptual coverage of delegates,
2. Technical overview of delegates,
3. How a delegate can execute a connected method, aka an event handler
High-Level Overview:
Consider the following scenario where you have a pipeline going out from a pumping station and connected to that pipeline are houses and industries. Houses will use the supplied water for cleaning, cooking, etc. while the industries will use the same water for chemical and other manufacturing purposes.
The pumping station here is synonymous with an event firing when an action of interest takes place. The water is the information conveyed by the event, and the houses and industries are the event handlers which process the supplied water, aka the information in the desired form. Delegates form the plumbing connecting the events and event handlers.
Before we move any further, we must understand what is a callback mechanism.
Callback Mechanism
Essentially, a callback mechanism that lets us chain pieces of code together in such a way that the first piece calls the second. The call can be made in two ways:
1) Synchronous callbacks, in which the first code (calling code) waits for the second code (called code) to finish the work and deliver the result, and, only after that resumes execution.
E.g. You can have a windows form application which has a button which upon clicking starts execution of a long running process. While the process is running the windows form will remain unresponsive.
2) Asynchronous callbacks, where the first code calls the second code but does not wait for it to finish the work. The work by the second piece of code is carried out on a different thread.
Callback mechanism is known by different names in different programming languages. In C, it is known as function pointers, and in C# it is called “delegates”.
In C#, delegates form the plumbing that allows one code to call another code synchronously or asynchronously. Adding to that is the fact that they are type-safe, i.e. you can only call methods that comply with the delegate’s signature and return type. In this post we will focus only on the synchronous calling via delegates
Use cases for delegates
Delegates can be used in two scenarios:
1. To broadcast changes in an object’s state
2. To broadcast fulfillment of a condition
Delegates behind the scenes are a special type of class, which means that we need to create an object of delegate type to use it for callback purposes.
Let’s dive into practical examples that show the use cases in action.
Broadcast changes in an object’s state
Consider human beings and their sensitiveness to ambient temperature. We can create a class “Human” and broadcast the response of its object to different temperatures.
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; }
//Delegate declaration
public delegate void TemperatureSensationHandler(string message);
// Member variable of the declared delegate
public TemperatureSensationHandler temperatureSensationMembers;
public Human()
{
}
// User defined constructor
public Human(int id, string name, float minTemp, float maxTemp)
{
Id = id;
Name = name;
minTemp = MinTemp;
maxTemp = MaxTemp;
}
public void TemperatureSensation(float temperature)
{
if (temperatureSensationMembers!= null)
{
if (temperature > MaxTemp)
{
temperatureSensationMembers("I am feeling hot");
}
else if (temperature < MinTemp)
{
temperatureSensationMembers("I am feeling cold");
}
else
{
temperatureSensationMembers("I am feeling normal");
}
}
}
}
In the code above, we have a class "Human" with four properties, of which two are of importance - MinTemp and MaxTemp. The method TemperatureSensation accepts a float type parameter and depending upon where the value stands in the spectrum between MinTemp and MaxTemp, broadcasts an appropriate message. In the following code, we create an object of the Human class and test the functionality of delegate declared in the Human class.
class Program
{
static void Main(string[] args)
{
Human human = new Human()
{ Id = 1, Name = "Parakh", MinTemp = 25.5f, MaxTemp = 45.0f };
human.temperatureSensationMembers += OnTemperatureSensationEvent;
human.TemperatureSensation(100);
human.TemperatureSensation(30);
human.TemperatureSensation(10);
Console.ReadLine();
}
public static void OnTemperatureSensationEvent(string message)
{
Console.WriteLine(message);
}
}
In the code above we created an object of the Human class and instantiated it with some data. Then, we enrolled in the delegate object a method that matched the delegate’s signature and return type. Then we invoked the TemperatureSensation method on the human object which then broadcasted the messages to the delegate’s subscribed members.
The above code when executed yields the following output
Now there’s one flaw in the code above. Delegate class at the .Net framework level overrides the “+” and “-“ operators, allowing the subscription and unsubscription of complying methods. It means that having direct access to the delegate object is sensitive and a developer should not be able to empty a delegate object’s invocation list by using the “-“ operator repeatedly.
We thus encapsulate the subscription and unsubscription of the methods to the delegate object via methods.
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; }
//Delegate declaration
public delegate void TemperatureSensationHandler(string message);
// Member variable of the declared delegate
private TemperatureSensationHandler temperatureSensationMembers;
//Registration and un-registration methods
public void RegisterTempSensationHandlerMethods(TemperatureSensationHandler methodToRegister)
{
temperatureSensationMembers += methodToRegister;
}
public void UnRegisterTempSensationMethods(TemperatureSensationHandler methodToUnregister)
{
if (temperatureSensationMembers != null)
{
temperatureSensationMembers -= methodToUnregister;
}
}
public Human()
{
}
// User defined constructor
public Human(int id, string name, float minTemp, float maxTemp)
{
Id = id;
Name = name;
minTemp = MinTemp;
maxTemp = MaxTemp;
}
public void TemperatureSensation(float temperature)
{
if (temperatureSensationMembers!= null)
{
if (temperature > MaxTemp)
{
temperatureSensationMembers("I am feeling hot");
}
else if (temperature < MinTemp)
{
temperatureSensationMembers("I am feeling cold");
}
else
{
temperatureSensationMembers("I am feeling normal");
}
}
}
}
With the modified code, we now can register and un-register the methods without exposing the delegate object to a developer.
The code above yields the same output.
Broadcast of fulfilment of a condition
Some conditions warrant broadcast of information when they materialize. Examples would include intimation upon sending or receiving a message, dispatch, and delivery of a physical item in case of e-commerce applications, etc.
Here we will take an example of checking out a book from a library.
We have four classes - Book, Member, Author, and Library. The idea in this example is to broadcast a message upon execution of certain activities like searching for a book, searching for member records, and successful or unsuccessful checking out of a book.
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 class Library
{
public int LibraryId { get; set; }
public List<Book> Books { get; set; }
public List<Member> Members { get; set; }
public Library()
{
LibraryId = 1;
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
}
};
}
//Delegate which can accept methods having a string parameter
public delegate void CheckoutHandler(string message);
private CheckoutHandler checkOutHandlerMembers;
public void RegisterCheckOutHandlerMembers(CheckoutHandler methodToRegister)
{
checkOutHandlerMembers += methodToRegister;
}
public void UnRegisterCheckOutHandlerMembers(CheckoutHandler methodToUnregister)
{
if (checkOutHandlerMembers != null)
{
checkOutHandlerMembers -= methodToUnregister;
}
}
public void CheckOutBook(int MemberId, int bookId)
{
//The operation of search will be printed out by the program only
//if there are members to the delegate type
if (checkOutHandlerMembers != null)
{
checkOutHandlerMembers("Searching for book and Member records");
}
Book bookToBeCheckedOut = Books.Find(book => book.BookId == bookId);
Member MemberCheckingOut = Members.Find(Member => Member.MemberId == MemberId);
//The operation of success in finding the book and Member will
//only be printed if there are members to the delegate type
if (bookToBeCheckedOut != null &&
MemberCheckingOut != null &&
checkOutHandlerMembers != null)
{
checkOutHandlerMembers("Book and Member records found.");
}
if (bookToBeCheckedOut.IsCheckedOut == false)
{
//The final result, whether the book can be checked
//out needs to be printed whether
//there are any members to the delegate type.
//That's why if-else statement with duplicate
//console writes
if (checkOutHandlerMembers != null)
{
checkOutHandlerMembers("Congratulations, the book is available");
}
else
{
Console.WriteLine("Congratulations, the book is available");
}
}
else
{
if (checkOutHandlerMembers != null)
{
checkOutHandlerMembers("Apologies, but the book is already
checked out to an existing Member.");
}
else
{
Console.WriteLine("Apologies, but the book is
already checked out to an existing Member.");
}
}
}
}
}
The model created above is consumed in the console application.
class Program
{
static void Main(string[] args)
{
Library library = new Library();
library.RegisterCheckOutHandlerMembers(OnCheckingOutEvent);
library.CheckOutBook(1, 1);
Console.ReadLine();
}
public static void OnCheckingOutEvent(string message)
{
Console.WriteLine(message);
}
}
When executed the following output appears:
In the output shown above, the program emitted certain messages related to the searching of the member and book details and successful checkout of the book. Now, remember, the domain is responsible for emitting the information, but what we do with the information is entirely in the hands of the developer. We can compose an email or an SMS and embed the emitted message or have it logged in a log file or a database.
Summary
Delegates help in separating the act of broadcasting information and how that information is consumed.
I hope this post helped in laying a strong foundation by presenting the concept of delegates in an easy to understand manner.
References
- Pro C# 8 with .NET Core 3 by Andrew Troelsen and Phil Japikse
- CLR via C# by Jeffrey Richter