C# Events
Implementing the observer pattern for responsive applications
📢 What are Events?
Events enable classes to notify other classes when something interesting happens. Built on delegates, events provide a safe way to implement the observer pattern for loosely coupled, responsive applications.
// Define an event
public event EventHandler ButtonClicked;
// Raise the event
ButtonClicked?.Invoke(this, EventArgs.Empty);
// Subscribe to the event
button.ButtonClicked += OnButtonClicked;
void OnButtonClicked(object sender, EventArgs e)
{
Console.WriteLine("Button was clicked!");
}
Output:
Button was clicked!
Event Concepts
Publisher
Class that raises the event
public event EventHandler
MyEvent;
Subscriber
Class that handles the event
obj.MyEvent +=
HandleEvent;
EventHandler
Standard event delegate type
EventHandler<MyArgs>
myEvent;
EventArgs
Data passed with events
class MyEventArgs :
EventArgs { }
🔹 Basic Event Implementation
Events in C# follow the publisher-subscriber pattern to enable loose coupling between components. A publisher class declares an event using the `event` keyword, while subscribers attach handler methods with `+=`. When the publisher raises the event, all subscribed handlers execute. For example, a `Button` class might have a `Clicked` event that multiple listeners respond to, allowing separate modules to react to user interactions without direct dependencies, promoting modular and testable code.
using System;
// Publisher class
class Button
{
// Define the event
public event EventHandler Clicked;
// Method to raise the event
public void Click()
{
Console.WriteLine("Button clicked!");
// Raise the event (notify subscribers)
Clicked?.Invoke(this, EventArgs.Empty);
}
}
// Subscriber class
class Program
{
static void Main()
{
Button button = new Button();
// Subscribe to the event
button.Clicked += OnButtonClicked;
button.Clicked += OnButtonClickedAgain;
// Trigger the event
button.Click();
}
// Event handler method
static void OnButtonClicked(object sender, EventArgs e)
{
Console.WriteLine("Handler 1: Button click detected!");
}
static void OnButtonClickedAgain(object sender, EventArgs e)
{
Console.WriteLine("Handler 2: I also detected the click!");
}
}
Output:
Button clicked!
Handler 1: Button click detected!
Handler 2: I also detected the click!
🔹 Custom EventArgs
Custom `EventArgs` classes allow events to carry detailed, type-safe data to subscribers. By inheriting from `EventArgs`, you can define properties like `OldTemperature` and `NewTemperature` for a `TemperatureChanged` event. Subscribers then receive meaningful context, enabling precise reactions—such as logging changes or triggering alerts when thresholds are crossed. This practice enriches event-driven communication, making systems more informative and adaptable to complex business logic without compromising encapsulation.
using System;
// Custom EventArgs class
class TemperatureChangedEventArgs : EventArgs
{
public double OldTemperature { get; set; }
public double NewTemperature { get; set; }
}
// Publisher class
class Thermostat
{
private double temperature;
// Event with custom EventArgs
public event EventHandler<TemperatureChangedEventArgs> TemperatureChanged;
public double Temperature
{
get { return temperature; }
set
{
if (temperature != value)
{
double oldTemp = temperature;
temperature = value;
// Raise event with data
OnTemperatureChanged(oldTemp, temperature);
}
}
}
protected virtual void OnTemperatureChanged(double oldTemp, double newTemp)
{
TemperatureChanged?.Invoke(this, new TemperatureChangedEventArgs
{
OldTemperature = oldTemp,
NewTemperature = newTemp
});
}
}
class Program
{
static void Main()
{
Thermostat thermostat = new Thermostat();
// Subscribe to event
thermostat.TemperatureChanged += OnTemperatureChanged;
// Change temperature (triggers event)
thermostat.Temperature = 20;
thermostat.Temperature = 25;
thermostat.Temperature = 18;
}
static void OnTemperatureChanged(object sender, TemperatureChangedEventArgs e)
{
Console.WriteLine($"Temperature changed from {e.OldTemperature}°C to {e.NewTemperature}°C");
if (e.NewTemperature > 23)
Console.WriteLine(" → It's getting warm!");
else if (e.NewTemperature < 20)
Console.WriteLine(" → It's getting cold!");
}
}
Output:
Temperature changed from 0°C to 20°C
Temperature changed from 20°C to 25°C
→ It's getting warm!
Temperature changed from 25°C to 18°C
→ It's getting cold!
🔹 Subscribing and Unsubscribing
Dynamic event handler management is achieved using `+=` to subscribe and `-=` to unsubscribe. This allows runtime control over which methods respond to events, crucial for preventing memory leaks by removing references when handlers are no longer needed. For instance, an alarm system may attach multiple responders initially, then detach one while keeping others active. Proper unsubscription ensures efficient resource use and prevents unintended invocations, maintaining clean event flow throughout the application lifecycle.
using System;
class Alarm
{
public event EventHandler AlarmRaised;
public void Trigger()
{
Console.WriteLine("ALARM TRIGGERED!");
AlarmRaised?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Main()
{
Alarm alarm = new Alarm();
// Subscribe handlers
alarm.AlarmRaised += Handler1;
alarm.AlarmRaised += Handler2;
Console.WriteLine("First trigger (both handlers):");
alarm.Trigger();
// Unsubscribe one handler
alarm.AlarmRaised -= Handler1;
Console.WriteLine("\nSecond trigger (only Handler2):");
alarm.Trigger();
// Unsubscribe remaining handler
alarm.AlarmRaised -= Handler2;
Console.WriteLine("\nThird trigger (no handlers):");
alarm.Trigger();
}
static void Handler1(object sender, EventArgs e)
{
Console.WriteLine(" Handler1: Calling security!");
}
static void Handler2(object sender, EventArgs e)
{
Console.WriteLine(" Handler2: Sending notification!");
}
}
Output:
First trigger (both handlers):
ALARM TRIGGERED!
Handler1: Calling security!
Handler2: Sending notification!
Second trigger (only Handler2):
ALARM TRIGGERED!
Handler2: Sending notification!
Third trigger (no handlers):
ALARM TRIGGERED!
🔹 Real-World Example: Stock Price Monitor
A stock price monitor using events provides a clean, decoupled way to notify multiple components when a stock value changes. For instance, when AAPL's price updates, an event can automatically alert a portfolio tracker, a charting widget, and a notifications panel—all without tightly coupling these modules. This pattern improves scalability and maintainability, allowing new listeners to be added without modifying the core stock-fetching logic, making it ideal for real-time financial applications.
using System;
class PriceChangedEventArgs : EventArgs
{
public string StockSymbol { get; set; }
public decimal OldPrice { get; set; }
public decimal NewPrice { get; set; }
public decimal ChangePercent { get; set; }
}
class Stock
{
private decimal price;
public string Symbol { get; set; }
public event EventHandler<PriceChangedEventArgs> PriceChanged;
public decimal Price
{
get { return price; }
set
{
if (price != value)
{
decimal oldPrice = price;
price = value;
decimal change = ((value - oldPrice) / oldPrice) * 100;
OnPriceChanged(new PriceChangedEventArgs
{
StockSymbol = Symbol,
OldPrice = oldPrice,
NewPrice = value,
ChangePercent = change
});
}
}
}
protected virtual void OnPriceChanged(PriceChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
class Program
{
static void Main()
{
Stock apple = new Stock { Symbol = "AAPL", Price = 150 };
// Multiple subscribers
apple.PriceChanged += LogPriceChange;
apple.PriceChanged += CheckAlert;
apple.PriceChanged += UpdatePortfolio;
// Simulate price changes
apple.Price = 155;
apple.Price = 148;
}
static void LogPriceChange(object sender, PriceChangedEventArgs e)
{
Console.WriteLine($"[LOG] {e.StockSymbol}: ${e.OldPrice} → ${e.NewPrice} ({e.ChangePercent:F2}%)");
}
static void CheckAlert(object sender, PriceChangedEventArgs e)
{
if (Math.Abs(e.ChangePercent) > 2)
Console.WriteLine($"[ALERT] Significant change in {e.StockSymbol}!");
}
static void UpdatePortfolio(object sender, PriceChangedEventArgs e)
{
Console.WriteLine($"[PORTFOLIO] Updated {e.StockSymbol} value\n");
}
}
Output:
[LOG] AAPL: $150 → $155 (3.33%)
[ALERT] Significant change in AAPL!
[PORTFOLIO] Updated AAPL value
[LOG] AAPL: $155 → $148 (-4.52%)
[ALERT] Significant change in AAPL!
[PORTFOLIO] Updated AAPL value
🔹 Event Best Practices
Follow these essential guidelines to implement robust, maintainable events in your applications. Always check for null before raising an event to avoid NullReferenceException. Use the built-in EventHandler delegate for standard scenarios, and consider custom EventArgs for passing additional data. Keep event handlers short and non-blocking to prevent UI freezes, and remember to unsubscribe when objects are no longer needed to prevent memory leaks in long-running applications.
Event Guidelines:
- Naming: Use past tense for event names (Clicked, Changed, Loaded)
- Null Check: Always use ?. when raising events to avoid null reference errors
- EventArgs: Inherit from EventArgs for custom event data
- Sender Parameter: First parameter should be object sender
- Protected Method: Use protected virtual OnEventName method to raise events
- Unsubscribe: Always unsubscribe from events to prevent memory leaks
// Good event pattern
public class MyClass
{
// Event declaration
public event EventHandler<MyEventArgs> DataChanged;
// Protected method to raise event
protected virtual void OnDataChanged(MyEventArgs e)
{
// Null-conditional operator
DataChanged?.Invoke(this, e);
}
}
🔹 Events vs Delegates
While events are built on delegates, they provide crucial encapsulation and safety mechanisms that plain delegates lack. Events restrict outside code from invoking the delegate list directly or overriding other subscribers—actions that are possible with public delegates. This encapsulation enforces the publisher-subscriber pattern, making your code more robust and preventing unintended side effects. Essentially, events are delegates with built-in access control, designed specifically for safe, multi-consumer notification scenarios.
Key Differences:
- Events: Can only be invoked from within the declaring class
- Delegates: Can be invoked from anywhere
- Events: Use += and -= for subscription
- Delegates: Can be directly assigned with =
- Events: Better for publisher-subscriber pattern
- Delegates: Better for callbacks and method passing