• Labs icon Lab
  • Core Tech
Labs

Guided: Delegates and Events in C#

This Code Lab is centered around understanding the concepts of delegates and events in programming with C#. Delegates serve as function pointers, enabling you to create dynamic code structures to facilitate communication between components. Events, a crucial aspect of C# programming, will also be a focal point. You'll understand how to implement and subscribe to events, allowing your code to respond to specific occurrences in a decoupled manner. In this lab, you will apply events to incorporate a validation mechanic to enhance an existing application.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 20m
Published
Clock icon Dec 13, 2023

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: Delegates and Events in C# lab.

    In C# programming, the usage of delegates and events represents a pivotal paradigm shift. Delegates act as a type-safe, object-oriented way to represent references to methods. Events are a language feature built on top of delegates, providing a mechanism for components to communicate when an action or state change occurs. These features allow developers to create agile and responsive code structures.


    The ticket tracker application you will be working with has most of its behind-the-scenes and main application logic already implemented for you. However, you will be responsible for adding some user validation to allow an administrator to login in to the application. You will be coupling this with usage of events to notify whether admin login was successful.

    You can also check the solution directory if you are stuck or want to check your implementations at any time.

    Note: The files in the solution directory puts the solution code in the solution namespace for compilation purposes. Do not copy this namespace solution line or it will cause errors when running the application. Only take into account the code within the class block.


    You're invited to experiment and learn interactively. Make use of the Terminal to the right to execute the ticket tracker application by entering the command dotnet run --no-restore when you have completed the lab.

  2. Challenge

    Delegates

    Overview

    Delegates are custom defined types that can hold references to method(s). If you are familiar with function pointers from C and C++, then you will be quite familiar with delegates. However, delegates are also considered type-safe as the compiler performs type checking during compilation to ensure that any methods referred to by the delegate contain the correct return type and parameters as defined by the delegate.


    Declaration and Instantiation

    A delegate is typically declared using the delegate syntax:

    public delegate void ExampleDelegate();
    public delegate int Calculate(int a, int b);
    

    As seen in the example, they are declared just as you would with normal methods but with the delegate keyword preceding the return type. Once a delegate has been declared, any method that has the same return and parameters can be used to instantiate a delegate. When a delegate is instantiated with a method that it references, it is known as a delegate object.

    // Delegate declarations
    public delegate void ExampleDelegate();
    public delegate int Calculate(int a, int b);
    
    static void ExampleMethod() { // Delegates can be both static or instance methods
      // Do some stuff
    }
    
    int Add(int x, int y) {
     // add x and y
    }
    
    int WrongArgNum(int g, int z, int t) {
     // Do something
    }
    
    int WrongArgType(int k, string m) {
     // Do Something
    }
    
    // Instantiate a delegate object like you would with any variable
    ExampleDelegate example1 = ExampleMethod;
    Calculate example2 = Add;
    
    Calculate fail1 = ExampleMethod; // Won't work, Calculate delegate requires int return type but ExampleMethod is void return type.
    
    Calculate fail2 = WrongArgNum; // Won't work, Calculate specifies 2 parameters of type int but WrongArgNum has 3
    
    Calculate fail3 = WrongArgType; // Won't work, Calculate requires that both its parameters be of type int
    
    example1(); // Performs ExampleMethod
    example2(2, 2); // Performs Add(2, 2)
    
    // Can be instantiated using anonymous method
    ExampleDelegate anonFunc = delegate() { 
      Console.WriteLine("anonymous delegate!"); 
    };
    
    // Can be instantiated using lambda function
    ExampleDelegate lambdaFunc = () => { 
      Console.WriteLine("lambda delegate!"); 
    };
    

    Multicast Delegates and Usage

    Delegate objects can be multicast when assigned multiple methods that match their signature. Delegate objects can also be combined with other delegate objects of the same type. When executed, they perform their methods in the order in which they were added.

    // Declare delegate and method definitions
    public delegate void ExampleDelegate();
    
    public static void p1() {
     Console.Write("a");
    }
    
    public void p2() {
     Console.Write("b");
    }
    
    public void p3() {
     Console.Write("c");
    }
    
    // Instantiate delegate objects
    ExampleDelegate ex1 = p1 + p2; // ab
    ExampleDelegate ex2 = p2 + p3; // bc
    
    ex1 -= p2; // a
    ex1 += p3; // ac
    ex1 += ex2; // abc
    ex1 += p2 // abcb
    ex1 -= p1; // bcb
    

    Most importantly, delegate objects can be used as parameters to be passed into other methods. This essentially lets you pass in delegates as callback functions for other methods.

    public delegate void SortDelegate(List<Book> books);
    
    void SortByName(List<Book> books) {
      // Sort books by name
    }
    
    void SortByAuthor(List<Book> books) {
      // Sort books by author
    }
    
    void SortByGenre(List<Book> books) {
      // Sort books by genre
    }
    
    void SortBooks(SortDelegate sortingMethod) { // SortDelegate type for parameter
     // Sort books using specified method
    }
    
    // Instantiate delegate objects
    SortDelegate s1 = SortByName;
    SortDelegate s2 = SortByAuthor;
    SortDelegate s3 = SortByGenre;
    
    SortBooks(s1); 
    SortBooks(s2);
    SortBooks(s3);
    
    // SortBooks can sort by any of the specified sorting methods as they are all attached to SortDelegate objects.
    
  3. Challenge

    Events

    Overview

    Events in C# are essentially messages sent by a class that informs other classes that something has occurred. The class that sends this event, or raises it, is known as the publisher. The class(es) that receive this event, or handles it, are known as the subscribers.

    Events are related to delegates because they are built on top of delegates. For instance, an event handler declaration would typically go as follows:

    public delegate void MyEventHandler(object? sender, CustomEventArgs e)
    

    There are a few things to note here. First is that event handlers must always be of void return type. Secondly, the parameters here specify object? sender and CustomEventArgs e. The sender ostensibly refers to whatever object raised the event (with the ? being necessary since C# 8 telling the compiler that there is a possibility that the event is null). The CustomEventArgs represents the arguments provided by the event.


    EventHandler

    While the example above specifies a custom event handler, it is considered best practice in C# to utilize the predefined EventHandler and EventHandler<TEventArgs> patterns where possible. You should only really use a custom event handler pattern if neither EventHandler and EventHandler<TEventArgs> is able to meet your needs. In fact, the documentation shows the definition for EventHandler is simply

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

    Now what is the difference between EventHandler and EventHandler<TEventArgs>? Simply put, EventHandler is for when you are not sending custom or important data in the event to the handler. So if you're handler does not need any information besides the fact that the event has occurred, then EventHandler is the way to go. However, if you're handler needs any kind of information included with the event, then you want to use the EventHandler<TEventArgs> generic.

    Remember the EventArgs from the EventHandler definition? That's the base class for event arguments. If you remember the CustomEventArgs parameter from the overview, that is a derived class that you would need to manually implement from EventArgs if you want to send event data from the publisher to the subscriber. You also need to do this if you want to define your own custom event handler instead of using either of the predefined patterns.

    Usage

    Now let's take a look at how you would see event usage:

    // Custom event class for usage in the generic EventHandler<TEventArgs>
    public class ExampleEventArgs : EventArgs {
      public string data { get; set; }  
    	
      public ExampleEventArgs(string msg) {
    		data = msg;
      }		
    }
    
    class EventSender { // Publisher class
      // Declare example events
      public event EventHandler simpleEvent;
      public event EventHandler<ExampleEventArgs> genericEvent;
    	
      // This method is ONLY for genericEvent. It is used to raise the event when sending data to subscribers
      // Best practice to be set as protected virtual to allow derived classes the ability to override or extend the behavior of raising the event
      // Another best practice is the name the event raising method OnEventName
      protected virtual void OnExampleEvent(ExampleEventArgs e) {
        // Do any necessary things in here
        genericEvent?.Invoke(this, e);
      }
    	
      // Place your event raisers in here. When called, the subscribers will be notified
      public void ExampleMethod() {
        // Code that does stuff
        // Used when using the non-generic. Always use EventArgs.Empty since you are not providing extra information
        simpleEvent?.Invoke(this, EventArgs.Empty);
    		
        OnExampleEvent(new ExampleEventArgs("generic event occurred!"));
        }
    }
    	
    class EventReceiver { // Handles raised event
      public void HandleSimpleEvent(object? sender, EventArgs e) { 
    	
        Console.WriteLine("Simple event handled!");
      }
    
      public void HandleGenericEvent(object? sender, ExampleEventArgs e) {
    	  
        Console.WriteLine($"{e.data}");
      }
    }
    	
    // Main
    EventSender sender = new EventSender();
    
    EventReceiver handler = new EventReceiver();
    
    sender.simpleEvent += handler.HandleSimpleEvent; //Attach handler to event, aka subscribe to publisher
    
    sender.genericEvent += handler.HandleGenericEvent; //Attach generic handler to event
    
    sender.ExampleMethod(); //Will trigger the event in the publisher
    
    //Since event is triggered, subscriber will perform HandleSimpleEvent and HandleGenericEvent
    	
    }
    

    This may seem like a lot to digest, but take a look at it a couple times and it should start to make sense. To put it as simply as possible, this is what is happening:

    • Define an event in the publisher class.
    • Invoke the event in one of the publisher classes methods.
    • Define a handler method in the subscriber class.
    • Instantiate both classes, and attach the subscribers handler method to the publishers event using something like +=.
    • Whenever the publisher calls the method that invokes the event, the subscribers handler method will execute.
  4. Challenge

    The Application

    Overview

    Now let's take a look at the application you will be working on. Currently the ticket tracker application only allows users to add a bus or train ticket to the system. There is a login option, but it doesn't really do anything.

    You will be responsible for adding some simple validation that will allow users to login as an admin. Once logged in, they will be given the ability to view all tickets in the system and to remove tickets from the system. You will be utilizing events (and by extension, delegates) to notify users whether login was a success. For this scenario, you will be utilizing the EventHandler<TEventArgs> generic along with implementing a simple subclass for EventArgs similar to what was demonstrated in the previous step.

    As mentioned previously, feel free to checkout the solution directory for help or to compare your implementations. Remember that the solution file is not the only solution, so it's alright to have some divergence so long as your application is functioning as expected. Again, be sure to actively run the application to see what the compiler is saying. It's something that you should be doing by second nature moving forward!


    TicketManager

    First, you will need to complete TicketManager.cs, which will also be the publisher class.

    At the top, add a new class that will represent your custom event argument.

    Instructions 1. Create a `public` class with a name of or similar to `LoginEventArgs`. 2. Make sure this class implements `EventArgs` using `:`. 3. This class should have a single `bool` property named `IsSuccessful` or something similar.

    Within the TicketManager class, define an event using the EventHandler<TEventArgs> generic

    Instructions 1. Create a `public event` field of type `EventHandler?`. Replace `TEventArgs` with the custom argument class you defined above. 2. Name this field `LoginEvent` or something similar.

    With the event defined, you will now need to define a method to raise it.

    Instructions 1. Create a method called called `OnLogin`. It should take your custom event argument type as a parameter. 2. Mark this method as `protected virtual` with a `void` return type. 3. Invoke the event using `LoginEvent?.Invoke(this, e)` or similar.

    Lastly, you will need to implement the Login function. The signature has been defined but it has no method body.

    Instructions 1. Create a `adminUsername` and `adminPassword` string. It can be whatever you wish. 2. Create a `bool` such as `loginSuccessful`. It should only be true when the `username` and `password` parameters both match your specified username and password for admin. 3. If `loginSuccessful` is true, set the `IsLoggedIn` property to true. 4. Raise an event using `OnLogin`. Pass in a new instance of your custom event argument, such as `new LoginEventArgs { IsSuccessful = loginSuccessful }`.

    With that, TicketManager.cs should be complete.


    Program

    Finally, you will need to create your event handler within Program.cs which subscribes to the LoginEvent.

    Instructions 1. Create a method called `HandleLoginEvent()`. It should be `private static void`. 2. It should have the `object? sender` and your custom even argument as parameters. 3. Check if `IsSuccessful` from your custom event argument is true. If it is, print out a success message using `Console.WriteLine()`. Otherwise, print out a failure message. 4. Look for the line `TicketManager ticketManager = new TicketManager()` near the top of the `Main()` method. 5. Immediately after, set your `LoginEvent` in `ticketManager` to your newly defined event handler, for example `ticketManager.LoginEvent += HandleLoginEvent`.
    --- Just like that, you're done! When you run the application you will be a normal user by default. Choosing the login option will now allow you to input a username and password to login as an admin. If successful, you will now be able to remove or view all tickets in the system in addition to adding them like a normal user could.

George is a Pluralsight Author working on content for Hands-On Experiences. He is experienced in the Python, JavaScript, Java, and most recently Rust domains.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.