User Tools

Site Tools


notes:csharp:events

Events in C#

(MSDN) Events enable an object to notify other objects when something of interest occurs. The class that sends (or raises) the event is called the publisher and the classes that receive (or handle) the event are called subscribers (a.k.a. observers).

Events have the following properties:

  • The publisher determines when an event is raised; the subscribers determine what action is taken in response to the event.
  • An event can have multiple subscribers. A subscriber can handle multiple events from multiple publishers.
  • Events that have no subscribers are never raised.
  • You may not assign an event handler to an event using '=', you have to use '+='.
  • Event handlers should be declared private or protected, not public.

You should raise events when your type must communicate with multiple subscribers to inform them of actions.

Although events can be based on any delegate type it is recommended that you base your events on the .NET pattern by using EventHandler or EventHandler<TEventArgs> delegates.

// sender is the source of the event.
// args is an object that contains the event data.
public delegate void EventHandler(object sender, EventArgs args);
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs args);

Example: Define a RatingChanged event that allows a publisher (VideoClip) to inform subscribers when video rating has changed. Use the built-in delegate EventHandler<TEventArgs> for the event:

using System;
 
namespace ConsoleTest
{
    // [1] Define a class to hold custom event information that 
    // you want the publisher to send to subscribers.
    public class RatingChangedArgs : EventArgs
    {
        // Expose data as properties or as read-only fields.
        public readonly float OldRating;
        public readonly float NewRating;
 
        public RatingChangedArgs(float oldRating, float newRating)
        {
            this.OldRating = oldRating;
            this.NewRating = newRating;
        }
    }
 
    // The VideoClip class is an event publisher.
    class VideoClip
    {
        // [2] Declare the public event using EventHandler<T>.
        public event EventHandler<RatingChangedArgs> RatingChanged;
 
        // [3] Wrap the event invocation code inside a protected virtual 
        // method to allow derived classes to override the invocation behavior.
        protected virtual void OnRatingChanged(RatingChangedArgs args) =>
            // The ?. operator ensures that the event is raised only when 
            // any listeners are attached to the event.
            RatingChanged?.Invoke(this, args);
 
        public string Title { get; set; }
 
        private float rating;
        public float Rating
        {
            get
            {
                return rating;
            }
 
            set
            {
                if (rating == value)
                    return;
 
                // [4] Inform subscribers when video rating has changed.
                OnRatingChanged(new RatingChangedArgs(rating, value));
                rating = value;
            }
        }
    }
 
    // This class subscribes to an event.
    class Subscriber
    {
        private string id;
        public Subscriber(string id, VideoClip clip)
        {
            this.id = id;
 
            // [5] Subscribe to the event.
            clip.RatingChanged += Subscriber_RatingChanged;
        }
 
        // [6] Define what actions to take when the event is raised.
        private void Subscriber_RatingChanged(object sender, RatingChangedArgs args)
        {
            Console.WriteLine("Subscriber: " + id);
            Console.WriteLine("VideoClip: " + ((VideoClip)sender).Title);
            Console.WriteLine("Old rating: {0:F1}", args.OldRating);
            Console.WriteLine("New rating: {0:F1}", args.NewRating);
            Console.WriteLine();
        }
    }
 
    class Program
    {
        static void Main()
        {
            // Instantiate the publisher and a few subscribers.
            VideoClip clip = new VideoClip() { Title = "A Funny Cat", Rating = 4.1f };
 
            Subscriber s1 = new Subscriber("s1", clip);
            Subscriber s2 = new Subscriber("s2", clip);
 
            // [7] Raise the RatingChanged event.
            clip.Rating = 4.9f; // change the rating
        }
    }
}

Output:

Subscriber: s1
VideoClip: A Funny Cat
Old rating: 4.1
New rating: 4.9
 
Subscriber: s2
VideoClip: A Funny Cat
Old rating: 4.1
New rating: 4.9

In the above code, the following two code snippets are equivalent:

// [5] Subscribe to the event.
clip.RatingChanged += Subscriber_RatingChanged; // C# 2.0
// [5] Subscribe to the event.
clip.RatingChanged += new EventHandler<RatingChangedArgs>(Subscriber_RatingChanged); // C# 1.1

Also, the following two code snippets are equivalent:

RatingChanged?.Invoke(this, args); // C# 6 (null conditional operator)
// Make a temporary copy of the event to avoid possibility of a race condition if the last subscriber 
// unsubscribes immediately after the null check and before the event is raised.
EventHandler<RatingChangedArgs> handler = RatingChanged;
 
// Event is null if there are no subscribers.
if (handler != null)
    handler(this, args);

You can also add an event handler using a lambda expression. This is useful if you don't have to unsubscribe from the event later:

// Subscribe to the event.
clip.RatingChanged += (sender, args) =>
{
    Console.WriteLine("Subscriber: " + id);
    Console.WriteLine("VideoClip: " + ((VideoClip)sender).Title);
    Console.WriteLine("Old rating: {0:F1}", args.OldRating);
    Console.WriteLine("New rating: {0:F1}", args.NewRating);
};

You cannot easily unsubscribe from an event if you used an anonymous function to subscribe to it. Do not use anonymous functions to subscribe to events if you have to unsubscribe from the event.

Example: Retrieve event handlers using the System.Delegate.GetInvocationList method and invoke them.

Note: Event handlers in the invocation list are executed in the sequential order. It means that if any of the handlers in the invocation list throws an exception, the subsequent actions won't be executed. A remedy for that is to manually raise events with exception handling.

using System;
using System.Linq;
using System.Collections.Generic;
...
public class Publisher
{
    public event EventHandler OnChange = delegate { };
 
    public void Raise()
    {
        var exceptions = new List<Exception>();
 
        // Enumerate the invocation list.
        foreach (Delegate handler in OnChange.GetInvocationList())
        {
            try
            {
                // Manually raise an event handler.
                handler.DynamicInvoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }
 
        if (exceptions.Any())
        {
            throw new AggregateException(exceptions);
        }
    }
}
 
...
 
Publisher p = new Publisher();
 
// Subscriber #1
p.OnChange += (sender, e)
    => Console.WriteLine("Subscriber 1 called");
 
// Subscriber #2 throws an exception.
p.OnChange += (sender, e)
    => { throw new Exception(); };
 
// Subscriber #3
p.OnChange += (sender, e)
    => Console.WriteLine("Subscriber 3 called");
 
try
{
    // Raise all event handlers.
    // Output:
    // Subscriber 1 called
    // Subscriber 3 called
    // Number of exceptions: 1
    p.Raise();
}
catch (AggregateException exc)
{
    Console.WriteLine("Number of exceptions: {0}", exc.InnerExceptions.Count);
}

EventHandlerList

Some classes (such as Windows controls) have large numbers of events. If only a small number of the events is actually used in an application, you can create the event objects only when they are needed at runtime.

Use an object of type System.ComponentModel.EventHandlerList as a container for event objects. The EventHandlerList has the following methods:

  • AddHandler(Object, Delegate) - adds a delegate to the list.
  • RemoveHandler(Object, Delegate) - removes a delegate from the list.

Example: A Logger class collecting events from given subsystems [Source: “Effective C#” by Bill Wagner]:

using System.ComponentModel.EventHandlerList;
...
public sealed class Logger
{
    private static EventHandlerList _handlers = new EventHandlerList();
 
    // The system parameter represents a subsystem that generated a message.
    public static void AddLogger(string system, EventHandler<LoggerEventArgs> ev) =>
        _handlers.AddHandler(sender, ev);
 
    public static void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) =>
        _handlers.RemoveHandler(sender, ev);
 
    public static void AddMessage(string system, int priority, string message)
    {
        if (!string.IsNullOrEmpty(system))
        {
            var handler = _handlers[system] as EventHandler<LoggerEventArgs>;
            var args = new LoggerEventArgs(priority, message);
 
            // Raise an event if the given subsystem has any listeners.
            handler?.Invoke(null, args);
        }
    } 
}

A Logger class that uses a generic Dictionary instead of the EventHandlerList class [Source: “Effective C#” by Bill Wagner]:

public sealed class Logger
{
    private static Dictionary<string, EventHandler<LoggerEventArgs>> _handlers = 
        new Dictionary<string, EventHandler<LoggerEventArgs>>();
 
    public static void AddLogger(string system, EventHandler<LoggerEventArgs> ev)
    {
        if (_handlers.ContainsKey(system))
            _handlers[system] += ev;
        else
            _handlers.Add(system, ev);
    }
 
    // RemoveLogger throws an exception if the collection does not contain a handler for the given system.
    public static void RemoveLogger(string system, EventHandler<LoggerEventArgs> ev) =>
        _handlers[system] -= ev;
 
    public static void AddMessage(string system, int priority, string message)
    {
        if (!string.IsNullOrEmpty(system))
        {
            _handlers.TryGetValue(system, out var handler);
            var args = new LoggerEventArgs(priority, message);
 
            // Raise an event if the given subsystem has any listeners.
            handler?.Invoke(null, args);
        }
    } 
}

Memory Leaks

Keep in mind that strong references may stop the garbage collector from disposing of objects.

Jon Skeet post at StackOveflow:

While an event handler is subscribed, the publisher of the event holds a reference to the subscriber via the event handler delegate (assuming the delegate is an instance method). If the publisher lives longer than the subscriber, then it will keep the subscriber alive even when there are no other references to the subscriber. If you unsubscribe from the event with an equal handler that will remove the handler and the possible leak.

notes/csharp/events.txt · Last modified: 2020/10/08 by leszek