Skip to content

C# - Events

Events are a mechanism in C# that enables a class or object (the publisher) to notify other classes or objects (the subscribers) when something of interest occurs. This is based on the Publisher-Subscriber pattern and is fundamental for building loosely coupled and responsive applications.

Common examples include user actions in a UI (a button click), a task completing in the background, or a system state change (a temperature sensor exceeding a threshold).

The modern event pattern in C# involves three main parts:

  • The Delegate: This is a type that defines the signature of the event handler method. The standard convention is to use System.EventHandler or the generic System.EventHandler<TEventArgs>.
  • The Event Data: A class that inherits from System.EventArgs. It’s used to pass data from the publisher to the subscribers about the event.
  • The Event Itself: A member of the publisher class, declared with the event keyword. This keyword ensures that subscribers can only add (+=) or remove (-=) handlers, not directly invoke the event.

Let’s create a Boiler class that fires an event when its temperature gets dangerously high. We will follow modern C# best practices.

// This class carries the information about the event.
public class BoilerEventArgs : EventArgs
{
public double Temperature { get; }
public string Message { get; }
public BoilerEventArgs(double temperature, string message)
{
Temperature = temperature;
Message = message;
}
}
public class Boiler
{
// 1. Define the event using the generic EventHandler<T> delegate.
// This states that any handler for this event must accept BoilerEventArgs.
public event EventHandler<BoilerEventArgs>? BoilerAlert;
private double _currentTemperature;
public void SetTemperature(double temp)
{
_currentTemperature = temp;
Console.WriteLine($"Boiler temperature set to: {_currentTemperature}°C");
// 2. Check the condition and raise the event if necessary.
if (_currentTemperature > 95.0)
{
// Create the event arguments
var args = new BoilerEventArgs(_currentTemperature, "DANGER: Boiler is overheating!");
// 3. Raise the event
OnBoilerAlert(args);
}
}
// 4. A protected virtual method to raise the event (best practice).
// This allows derived classes to customize the event-raising behavior.
protected virtual void OnBoilerAlert(BoilerEventArgs e)
{
// The null-conditional operator (?.) ensures we only invoke the event
// if there are subscribers, preventing a NullReferenceException.
BoilerAlert?.Invoke(this, e);
}
}

Step 3: Create the Subscriber and Handle the Event

Section titled “Step 3: Create the Subscriber and Handle the Event”
// Subscriber class
public class BoilerLogger
{
// Event handler method. The signature must match the EventHandler<BoilerEventArgs> delegate.
public void LogAlert(object? sender, BoilerEventArgs e)
{
Console.WriteLine($"LOG: Alert received. Temp: {e.Temperature}°C. Message: {e.Message}");
}
}
// Another subscriber
public class SafetySystem
{
public void ShutdownBoiler(object? sender, BoilerEventArgs e)
{
Console.WriteLine($"SAFETY: Shutting down boiler immediately! Temp was {e.Temperature}°C.");
}
}
// --- Putting It All Together ---
// 1. Create instances of the publisher and subscribers
var boiler = new Boiler();
var logger = new BoilerLogger();
var safety = new SafetySystem();
// 2. Subscribe to the event. The methods are now event handlers.
boiler.BoilerAlert += logger.LogAlert;
boiler.BoilerAlert += safety.ShutdownBoiler;
// You can also subscribe using a lambda expression for concise handlers
boiler.BoilerAlert += (sender, e) => {
Console.WriteLine($"EMAIL: Notifying administrator about temperature {e.Temperature}°C.");
};
// 3. Trigger the actions that will cause the event to be raised
Console.WriteLine("--- Normal Operation ---");
boiler.SetTemperature(80);
Console.WriteLine("\n--- DANGER --- ");
boiler.SetTemperature(98);
// To unsubscribe:
// boiler.BoilerAlert -= logger.LogAlert;

When the above code is compiled and executed, it produces the following result:

--- Normal Operation ---
Boiler temperature set to: 80°C
--- DANGER ---
Boiler temperature set to: 98°C
LOG: Alert received. Temp: 98°C. Message: DANGER: Boiler is overheating!
SAFETY: Shutting down boiler immediately! Temp was 98°C.
EMAIL: Notifying administrator about temperature 98°C.
  • GUI Frameworks: In WPF, MAUI, or WinForms, UI elements like Button publish a Click event. Your code subscribes to this event to perform an action.
  • ASP.NET Core: The application lifecycle has events like ApplicationStarted that services can hook into.
  • Asynchronous Programming: A background service can raise an event like WorkCompleted to notify the main application thread that results are ready.
  • Memory Leaks: This is the most common problem. If a subscriber with a long lifetime (e.g., a static class) subscribes to an event from a publisher with a short lifetime, the publisher will not be garbage collected because the subscriber holds a reference to it. Always unsubscribe (-=) from events when the subscriber is no longer needed, especially in classes that implement IDisposable.
  • Thread Safety: Event handlers are executed on the same thread that raises the event. If a background thread raises an event that updates the UI, you must dispatch the UI update back to the UI thread. This is a critical concept in GUI programming.
  • Use EventHandler<T>: Avoid creating custom delegate types. Stick to the standard EventHandler (for events with no data) and EventHandler<TEventArgs> for consistency.
  • Make Event Data Immutable: Properties on your EventArgs class should be readonly ({ get; }) to prevent subscribers from modifying the event data, which could cause unpredictable side effects for other subscribers.