You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
csharp-basics/11-delegates-and-events.md

10 KiB

marp paginate math theme title
true true mathjax buutti N. Delegates and events

Delegates and events

Overview

  • Delegates
    • Multicast Delegates
    • Anonymous Methods
  • Events

Delegates

  • Delegates are reference type variables that hold a reference to a method or multiple methods
    • Class objects hold a reference to a class instance, delegate objects hold a reference to a method / methods
  • Similar to function pointers in C and C++, or how any function in JavaScript works
  • Allows for methods to be passed as variables, useful for creating, for example, events

Creating a delegate

  • Declare a delegate using the following syntax:

    delegate returnType DelegateName(parameters);
    
  • Example:

    delegate void PrintDelegate(string output);
    
  • This creates a new delegate of type void, named PrintDelegate and one parameter of type string

  • The referenced method return and parameter types have to match the delegate!

Referencing a delegate

  • After creating the delegate, it can be instantiated and the method assigned to it with the method name:
    delegate void PrintDelegate(string output);
    
    static void Main(string[] args)
    {
      void PrintInLower(string text)
      {
        Console.WriteLine(text.ToLower());
      }
    
      PrintDelegate print = PrintInLower;
      print("AaaBbbCcc");     // Outputs "aaabbbccc"
    }
    

Using multicast delegates

  • Delegates can be composed of multiple methods using the + operator
  • Using the same PrintDelegate delegate as before, we could do this:
delegate void PrintDelegate(string output);
static void Main(string[] args)
{
  void PrintInLower(string text)
  {
    Console.WriteLine(text.ToLower());
  }
  void PrintInUpper(string text)
  {
    Console.WriteLine(text.ToUpper());
  }
  PrintDelegate print = PrintInLower;
  print += PrintInUpper;
  print("AaaBbbCcc");
}

Removing methods

  • Methods can be removed from the delegate with the - operator:
    delegate void PrintDelegate(string output);
    static void Main(string[] args)
    {
      ...
      print -= PrintInLower;
      print("AaaBbbCcc");     // Outputs "AAABBBCCC"
      ...
    }
    

Delegates: An example

  • Let's extend our previous example (without the -= part) by creating a new class called DelegateTest, and giving it a constructor that takes a PrintDelegate object as a parameter:
    public class DelegateTest
    {
      public DelegateTest(PrintDelegate printDelegate)
      {
        printDelegate("This Method Was Called From Another Class!");
      }
    }
    

void PrintInLower(string text)
{
  Console.WriteLine(text.ToLower());
}
void PrintInUpper(string text)
{
  Console.WriteLine(text.ToUpper());
}
// Initialize new delegate which is composed of PrintInLower method
PrintDelegate print = PrintInLower;

// Add PrintInUpper method to the delegate
print += PrintInUpper;

// Send the delegate to the class constructor
DelegateTest delegateTest = new DelegateTest(print);

Now we can create a new DelegateTest object and pass the delegate to the object constructor:

Anonymous methods

  • Delegates can be initialized anonymously (without a specified name)
  • Anonymous method in variable declaration:
    delegate void PrintDelegate(string output);
    bool printUpper = true;
    PrintDelegate printCheckUpper =
      delegate (string text)
      {
        if (printUpper)
          Console.WriteLine(text.ToUpper());
        else
          Console.WriteLine(text);
      };
    
    printCheckUpper("I'm not angry!");        // Outputs I'M NOT ANGRY!
    
  • Notice that the actual method that prints the text is not declared anywhere!

  • You can use an empty anonymous method to initialize a delegate that does nothing:
    delegate void SomeDelegate();
    
    class Program
    {
      static void Main(string[] args)
      {
        // Initialize an empty delegate, add method later...
        SomeDelegate myDelegate = new SomeDelegate(delegate { });
      }
    }
    

Events

The problem

class Game
{
  Sound gameOverSound;
  Window gameOverScreen;

  void OnGameOver()
  {
    gameOverSound.Play();   // plays some sound
    gameOverScreen.Show();  // shows a screen
  }
}
...
class Program
{
  static void Main()
  {
    var game = new Game();

    // somewhere in the game logic...
    // game.OnGameOver();
  }
}
  • Consider a game engine with three classes, Sound, Window and Game.
  • If implemented like this, the Game class has to know about the Sound and Window classes
  • \Rightarrow Game is tightly coupled, and thus dependent on the Sound and Window classes
  • Changes in either of the classes could break the code!

The solution: Events

  • A solution to this problem is the Publisher-subscriber pattern
  • Events are signals that are raised by a Publisher and received by a Subscriber
    • The publisher does not know or care who, if anyone, receives the signal
    • Changes in the subscriber classes do not affect the publisher
  • In C#, events are multicast delegates
  • When an object triggers an event, the event invokes event handlers
    • Event handlers are delegate instances added to the event

Raising events

  • Events consist of two elements:
    • A delegate that identifies the method that provides the response to the event
      • In the simplest case, we can use the built-in EventHandler delegate
    • An optional class to hold event data if the event provides data.
    public class Publisher
    {
        public event EventHandler SampleEvent;
        // Wrap the event in a protected virtual method
        // to enable derived classes to raise the event.
        protected virtual void OnSampleEvent()
        {
            // Raise the event in a thread-safe manner using the ?. operator.
            SampleEvent?.Invoke(this);
        }
    }
    

Raising the event

Events can only be invoked (i.e., raised) from within the class (or derived classes) or struct where they're declared (the publisher class)

public class Publisher
{
    public event EventHandler SampleEvent;
    protected virtual void OnSampleEvent()
    {
        SampleEvent?.Invoke(this);
    }
}
class Program
{
  static void Main()
  {
    var pub = new Publisher();

    pub.OnSampleEvent();  // ✅ Works fine
    // pub.SampleEvent();    // ❌ Not allowed!
  }
}

Adding subscribers to the event

  • Now that we know how to raise the event, we can add subscribers to it
  • i.e., functions that get called when the event is raised
public class Publisher
{
    public event EventHandler SampleEvent;
    protected virtual void OnSampleEvent()
    {
        SampleEvent?.Invoke(this);
    }
}
class Program
{
  static void Main()
  {
    var pub = new Publisher();

    pub.SampleEvent +=
      () => Console.WriteLine("Sample event!");
    
    pub.OnSampleEvent();
  }
}

Custom event handler

  • We can define the delegate ourselves, for example if we want to send some data to the event.
    public class Publisher
    {
        public delegate void SampleEventHandler(object sender);
    
        // Declare the event.
        public event SampleEventHandler SampleEvent;
    
        protected virtual void OnSampleEvent()
        {
            SampleEvent?.Invoke(this);
        }
    }
    

Event arguments

  • Finally, we can send arguments to the event like this:
    public class SampleEventArgs
    {
        public SampleEventArgs(string text) { Text = text; }
        public string Text { get; } // readonly
    }
    
    public class Publisher
    {
        public delegate void SampleEventHandler(object sender, SampleEventArgs e);
    
        public event SampleEventHandler SampleEvent;
    
        protected virtual void RaiseSampleEvent()
        {
            SampleEvent?.Invoke(this, new SampleEventArgs("Hello"));
        }
    }
    

"Fixing" the Game Over example

public delegate void GameOverHandler();
class Game
{
  public event GameOverHandler GameOver;

  protected virtual void OnGameOver()
  {
      GameOver?.Invoke(this);
  }
}
...
class Program
{
  static void Main()
  {
    var game = new Game();
    Sound gameOverSound;
    Window gameOverScreen;

    game.GameOver += gameOverSound.Play;
    game.GameOver += gameOverScreen.Show;
  }
}
  1. Declare an event handler delegate (GameOverHandler)
  2. Declare an instance of the handler with the event keyword (GameOVer)
  3. Declare a virtual method that invokes the event
  4. Add methods that subscribe to the event

Exercise 1: A rudimentary event system

Create a console application for controlling a plant treatment system with three methods that print the following outputs:

Method Output
void ReleaseWater() Releasing water...
void ReleaseFertilizer() Releasing fertilizer...
void IncreaseTemperature() Increasing temperature...
  • All methods are off by default. Create a main loop where the user can...
    • ...type the name of the method to switch each method on (add it to the delegate)
    • ...type run to execute all the methods that are on

  • Hint: you can just use switch-case for defining which method should be added to the delegate

  • Here's an example console input & output: