.NET Academy

Events

Let objects notify any number of listeners when something happens, without knowing who they are.

11 min read

You will learn

  • Declare an event and raise it safely
  • Subscribe and unsubscribe handlers with += and -=
  • Use EventHandler<T> and custom event arguments
  • Recognize the publisher/subscriber pattern

"Let me know when something happens"

A download finishes. A temperature crosses a limit. A user clicks a button. Often several parts of a program care about such moments — update the UI, write a log, send an alert — and the object where it happens shouldn't need to know about all of them.

Events solve this with the publisher/subscriber pattern:

  • The publisher declares an event and raises it when something happens.
  • Any number of subscribers attach handlers, and get called each time.

Declaring and raising an event

class Downloader
{
    public event EventHandler<string>? Completed;     // the event
 
    public void Download(string url)
    {
        // ... download the file ...
        Completed?.Invoke(this, url);                 // raise it
    }
}
  • event + a delegate type declares the event. EventHandler<T> is the standard shape: (object? sender, T args).
  • ?.Invoke(...) raises it — the ?. matters, because an event with no subscribers is null.
  • By convention, sender is the object raising the event: this.

Subscribing

var downloader = new Downloader();
 
downloader.Completed += (sender, url) => Console.WriteLine($"Done: {url}");
downloader.Completed += LogCompletion;     // a method works too
 
downloader.Download("https://example.com/file.zip");
// Done: https://example.com/file.zip
// [log] https://example.com/file.zip
 
void LogCompletion(object? sender, string url) => Console.WriteLine($"[log] {url}");

Handlers run in the order they were added, synchronously, when the event is raised.

Unsubscribing

downloader.Completed -= LogCompletion;

To unsubscribe a lambda, you must keep a reference to the same delegate:

EventHandler<string> handler = (s, url) => Console.WriteLine(url);
downloader.Completed += handler;
downloader.Completed -= handler;      // works

Event leaks

The publisher holds references to every handler — and through them, to the subscribers. If a short-lived object subscribes to a long-lived one and never unsubscribes, it can never be garbage collected. Unsubscribe when the subscriber is done.

Why the event keyword?

You could expose a public delegate field instead, but then anyone could raise it or replace all its handlers with =. The event keyword restricts outside code to += and -=; only the declaring class can raise it.

Custom event arguments

When you need to pass several values, define an arguments type — a record works nicely:

record OrderPlacedEventArgs(int OrderId, decimal Total);
 
class Shop
{
    public event EventHandler<OrderPlacedEventArgs>? OrderPlaced;
 
    public void PlaceOrder(int id, decimal total)
    {
        // ... save the order ...
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs(id, total));
    }
}
 
shop.OrderPlaced += (_, e) => Console.WriteLine($"Order {e.OrderId}: {e.Total:F2}");

Where you'll meet events

  • UI frameworks (Blazor, WPF, MAUI): Click, TextChanged…
  • INotifyPropertyChanged: the PropertyChanged event powers data binding.
  • .NET APIs: FileSystemWatcher.Changed, Process.Exited, AppDomain.UnhandledException.

In backend systems, the same idea scales up into domain events and message queues — publishers and subscribers in different services. You'll see those in Backend Fundamentals and Architecture.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • Events implement publisher/subscriber: publishers raise, subscribers handle.
  • Declare with public event EventHandler<T>? Name; and raise with Name?.Invoke(this, args).
  • Subscribe with +=, unsubscribe with -= — keep a reference to lambdas you'll remove.
  • Unsubscribe to avoid keeping subscribers alive longer than intended.

Exercise

Thermostat raises TooHot with the temperature whenever SetTemperature is called with a value above Limit. Stock raises PriceChanged (symbol, new price) when Price is set to a DIFFERENT value — setting the same price again raises nothing. Both must work with no subscribers.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

Raise an event safely with the null-conditional operator: TooHot?.Invoke(this, temperature);

Hint 2

Stock.Price needs a backing field: in the setter, return early if value == price; otherwise store it and raise PriceChanged?.Invoke(Symbol, value).

Show solution
var thermostat = new Thermostat(30);
thermostat.TooHot += (sender, temp) => Console.WriteLine($"Too hot: {temp}°C");
thermostat.SetTemperature(35);

class Thermostat
{
    public double Limit { get; }
    public event EventHandler<double>? TooHot;

    public Thermostat(double limit) => Limit = limit;

    public void SetTemperature(double temperature)
    {
        if (temperature > Limit)
        {
            TooHot?.Invoke(this, temperature);
        }
    }
}

class Stock
{
    private decimal price;

    public string Symbol { get; }
    public event Action<string, decimal>? PriceChanged;

    public decimal Price
    {
        get => price;
        set
        {
            if (value == price) return;
            price = value;
            PriceChanged?.Invoke(Symbol, value);
        }
    }

    public Stock(string symbol, decimal price)
    {
        Symbol = symbol;
        this.price = price;
    }
}

Quiz

4 questions · 3 correct to pass
  1. 1.What does `button.Click += OnClick;` do?

  2. 2.Why write `Changed?.Invoke(this, args)` instead of `Changed(this, args)`?

  3. 3.What does the event keyword add compared to a public delegate field?

  4. 4.What's a common cause of memory leaks with events?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)