The content below is retrieved from Jupyter notebooks that you can find in this repository.
Source: Microsoft documentation.
Events enable a class or object (the publisher) to notify other classes or objects (the subscribers) when something occurs.
- The publisher determines when to raise an event. It can have multiple subscribers.
 - The subscriber determines how to handle an event when it’s raised. It can be subscribed to multiple events.
 
System.EventHandler is a delegate to handle an event that has no event data. Its signature is:
public delegate void EventHandler(object? sender, EventArgs e);
It’s equivalent to:
public Action<object?, EventArgs> ActionEventHandler;
System.EventArgs provides a value to use for events with no data, but it also represents the base class for events with data, e.g.
public class MyEventArgs : EventArgs {
    public string Message { get; }
    public DateTime DateTime { get; }
    public MyEventArgs(string message) {
        Message = message;
        DateTime = DateTime.Now;
    }
}
It’s possible to define a custom event handler if we have custom event data, like the class defined above:
public delegate void DefinedEventHandler(object sender, MyEventArgs args);
which can also be defined using generics with System.Action class.
Events are declared with the event keyword in the publisher class:
public event DefinedEventHandler DefinedEvent;
There’s an option to use generic types for custom event data so it’s not necessary to define a new delegate, as shown below:
public class Publisher {
    public event EventHandler<MyEventArgs> MyEvent;
    public void SendMessage(string message) {
        var eventArgs = new MyEventArgs(message);
        OnRaiseMyEvent(eventArgs);
    }
    public void OnRaiseMyEvent(MyEventArgs args) {
        // use a copy of the event to avoid a race condition
        var raiseEvent = MyEvent;
        if (raiseEvent is null) {
            // event is null if it doesn't have subscribers
            return;
        }
        // raise the event by invoking it as calling a method
        raiseEvent(this, args);
    }
}
public class Subscriber {
    public int Id { get; }
    public Subscriber(int id, Publisher publisher) {
        Id = id;
        // the event gets a new subscriber, which is a method to handle it
        publisher.MyEvent += HandleMyEvent;
    }
    /// The parameters of this method match the signature of Publisher.MyEvent
    public void HandleMyEvent(object sender, MyEventArgs args) {
        Console.WriteLine($"Subscriber {Id} received the following message:\n{args.Message}\nAt {args.DateTime}");
    }
}
var publisher = new Publisher();
var subscribers = Enumerable.Range(1, 3)
    .Select(i => new Subscriber(i, publisher))
    .ToList();
publisher.SendMessage("Good afternoon to all!");
Subscriber 1 received the following message:
Good afternoon to all!
At 1/21/2022 6:47:40 PM
Subscriber 2 received the following message:
Good afternoon to all!
At 1/21/2022 6:47:40 PM
Subscriber 3 received the following message:
Good afternoon to all!
At 1/21/2022 6:47:40 PM
Events are a special type of multicast delegates that can only be invoked from within the class that declares them. In other words, only the publishers can raise their own events.