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.

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.

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 event using EventHandler<T>.
        public event EventHandler<RatingChangedArgs> RatingChanged;
 
        // [4] Wrap event invocations inside a protected virtual method
        // to allow derived classes to override the event invocation behavior.
        protected virtual void OnRatingChanged(RatingChangedArgs args)
        {
            RatingChanged?.Invoke(this, args);
        }
 
        public string Title { get; set; }
 
        private float rating;
        public float Rating
        {
            get
            {
                return rating;
            }
 
            set
            {
                if (rating == value)
                    return;
 
                // [5] 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;
 
            // [6] Subscribe to the event.
            clip.RatingChanged += Subscriber_RatingChanged;
        }
 
        // [7] 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);
 
            // [8] 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 lines are equivalent:

// [6] Subscribe to the event.
clip.RatingChanged += Subscriber_RatingChanged; // C# 2.0
// [6] 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);
}

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: 2017/03/29 by leszek