Events & Event Handlers
The event keyword builds on delegates to implement the observer (publish/subscribe) pattern safely. A class publishes an event; other classes subscribe by attaching handler methods with +=.
event vs Delegate Field
Declaring a plain public delegate field lets external code invoke or replace the delegate directly — a security hole. The event keyword restricts external access so that outside classes can only += (subscribe) or -= (unsubscribe). Only the declaring class can invoke the event.
public event EventHandler<DataEventArgs> DataReceived;EventHandler and EventHandler<T>
The BCL provides two standard delegate types for events:
EventHandler—void (object sender, EventArgs e)EventHandler<TEventArgs>—void (object sender, TEventArgs e)whereTEventArgs : EventArgs
Following this convention makes your events compatible with the broader .NET ecosystem (UI frameworks, reflection, serialisation).
EventArgs
System.EventArgs is the base class for event data. Derive from it to pass custom data:
public class PriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
}Raising Events Safely
Always use the null-conditional invoke pattern to avoid a race condition where the last subscriber unsubscribes between the null check and the invocation:
DataReceived?.Invoke(this, new DataEventArgs { Value = data });Event Accessors (add / remove)
You can override the default delegate storage by providing explicit add and remove accessors — useful for thread-safe event storage or forwarding subscriptions to an underlying event:
private EventHandler _onClick;
public event EventHandler Click
{
add { _onClick += value; }
remove { _onClick -= value; }
}Unsubscribing and Memory Leaks
Forgetting to unsubscribe (-=) is a common source of memory leaks: the publisher holds a strong reference to the subscriber via the delegate. Always unsubscribe in Dispose() or when the subscriber is no longer needed. The weak event pattern (via WeakEventManager in WPF) solves this at the cost of more complexity.
Code Examples
using System;
public class Counter
{
private int _count;
// Standard EventHandler — no custom data needed
public event EventHandler? ThresholdReached;
public int Threshold { get; }
public Counter(int threshold)
{
Threshold = threshold;
}
public void Increment()
{
_count++;
Console.WriteLine(````````Count = ${_count}````````);
if (_count >= Threshold)
{
// Raise the event using null-conditional invoke (thread-safe)
ThresholdReached?.Invoke(this, EventArgs.Empty);
}
}
}
var counter = new Counter(threshold: 3);
// Subscribe with a lambda handler
counter.ThresholdReached += (sender, e) =>
{
var c = (Counter)sender!;
Console.WriteLine(````````** Threshold of ${c.Threshold} reached! **````````);
};
counter.Increment();
counter.Increment();
counter.Increment(); // triggers event
counter.Increment(); // triggers againThe event keyword restricts external callers to += and -=. The publisher raises the event with ?.Invoke to safely handle zero subscribers.
using System;
// Custom event data
public class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; init; } = "";
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
public decimal Change => NewPrice - OldPrice;
}
public class StockMarket
{
// Typed event using EventHandler<T>
public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;
private decimal _price;
public string Symbol { get; }
public StockMarket(string symbol, decimal initialPrice)
{
Symbol = symbol;
_price = initialPrice;
}
public void UpdatePrice(decimal newPrice)
{
if (newPrice == _price) return;
var args = new StockPriceChangedEventArgs
{
Symbol = Symbol,
OldPrice = _price,
NewPrice = newPrice,
};
_price = newPrice;
PriceChanged?.Invoke(this, args); // raise
}
}
// Subscriber method (could be in a different class)
static void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
string direction = e.Change > 0 ? "UP" : "DOWN";
Console.WriteLine(````````[${e.Symbol}] ${direction} ${Math.Abs(e.Change):C2} (${e.OldPrice:C2} → ${e.NewPrice:C2})````````);
}
var msft = new StockMarket("MSFT", 300m);
msft.PriceChanged += OnPriceChanged;
msft.UpdatePrice(315m);
msft.UpdatePrice(298m);
msft.UpdatePrice(298m); // no change — event not raisedEventHandler<TEventArgs> carries strongly-typed event data. The custom EventArgs class acts as a data transfer object for the event, keeping the event signature clean.
using System;
public class Button
{
public event EventHandler? Clicked;
public void Click() => Clicked?.Invoke(this, EventArgs.Empty);
}
public class ClickLogger : IDisposable
{
private readonly Button _button;
private int _clickCount;
public ClickLogger(Button button)
{
_button = button;
_button.Clicked += HandleClick; // subscribe
Console.WriteLine("ClickLogger subscribed.");
}
private void HandleClick(object? sender, EventArgs e)
{
_clickCount++;
Console.WriteLine(````````Button clicked! Total: ${_clickCount}````````);
}
public void Dispose()
{
_button.Clicked -= HandleClick; // ALWAYS unsubscribe to prevent leaks
Console.WriteLine("ClickLogger unsubscribed and disposed.");
}
}
var btn = new Button();
using (var logger = new ClickLogger(btn))
{
btn.Click();
btn.Click();
} // Dispose() called — handler removed
Console.WriteLine("After dispose:");
btn.Click(); // no output from logger — successfully unsubscribedFailing to unsubscribe causes the publisher to hold a reference to the subscriber, preventing garbage collection. Unsubscribing in Dispose() ensures clean lifecycle management.
Quick Quiz
1. Why does the 'event' keyword restrict access compared to a public delegate field?
2. What is the safest way to raise an event in C# to avoid a null-reference exception when no subscribers exist?
3. What base class should custom EventArgs classes derive from?
4. Why is forgetting to unsubscribe from an event a potential memory leak?
Was this lesson helpful?