C# Observer Pattern
Introduction
The Observer Pattern is one of the most commonly used design patterns in software development. If you've ever used event handlers in a user interface or subscribed to notifications in an app, you've already seen the Observer Pattern in action! This pattern establishes a one-to-many relationship between objects, where multiple "observers" can monitor and respond to changes in a single "subject" object.
In C#, the Observer Pattern is elegantly implemented using delegates and events, which provide a built-in mechanism for this exact purpose. This makes C# particularly well-suited for event-driven programming.
What is the Observer Pattern?
The Observer Pattern defines a dependency between objects so that when one object (the subject) changes state, all its dependents (observers) are automatically notified and updated.
Key components of this pattern include:
- Subject - The object that maintains a list of observers and notifies them of state changes
- Observer - The objects that are interested in changes to the subject
- Notification mechanism - How observers are informed about changes
Why Use the Observer Pattern?
- Loose coupling - Subjects and observers are loosely coupled, meaning they can interact without detailed knowledge of each other
- Broadcast communication - One subject can notify many observers at once
- Dynamic relationships - Observers can be added or removed at runtime
Implementing the Observer Pattern in C#
Let's explore how to implement the Observer Pattern in C# using delegates and events.
Basic Implementation
Here's a simple example of the Observer Pattern:
using System;
using System.Collections.Generic;
// The Subject class
public class WeatherStation
{
// The delegate that defines the event signature
public delegate void WeatherChangedEventHandler(float temperature, float humidity, float pressure);
// The event that observers will subscribe to
public event WeatherChangedEventHandler WeatherChanged;
private float temperature;
private float humidity;
private float pressure;
// Method to update measurements and notify observers
public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
// Notify all observers
NotifyObservers();
}
private void NotifyObservers()
{
// Check if there are any subscribers before invoking
if (WeatherChanged != null)
{
WeatherChanged(temperature, humidity, pressure);
}
}
}
// An Observer class
public class WeatherDisplay
{
private string displayName;
public WeatherDisplay(string name)
{
displayName = name;
}
// This method will be called when the weather changes
public void OnWeatherChanged(float temperature, float humidity, float pressure)
{
Console.WriteLine($"{displayName} Display: Temperature = {temperature}°C, Humidity = {humidity}%, Pressure = {pressure} hPa");
}
}
// Usage example
class Program
{
static void Main(string[] args)
{
// Create the subject
WeatherStation weatherStation = new WeatherStation();
// Create observers
WeatherDisplay currentDisplay = new WeatherDisplay("Current Conditions");
WeatherDisplay statisticsDisplay = new WeatherDisplay("Statistics");
// Register observers with the subject
weatherStation.WeatherChanged += currentDisplay.OnWeatherChanged;
weatherStation.WeatherChanged += statisticsDisplay.OnWeatherChanged;
// Change the weather measurements
Console.WriteLine("Weather station updated with new data:");
weatherStation.SetMeasurements(27.5f, 65.0f, 1013.1f);
// Unregister one observer
Console.WriteLine("\nUnregistering Statistics Display...\n");
weatherStation.WeatherChanged -= statisticsDisplay.OnWeatherChanged;
// Change the measurements again
Console.WriteLine("Weather station updated with new data:");
weatherStation.SetMeasurements(28.2f, 70.0f, 1010.6f);
}
}
Output:
Weather station updated with new data:
Current Conditions Display: Temperature = 27.5°C, Humidity = 65%, Pressure = 1013.1 hPa
Statistics Display: Temperature = 27.5°C, Humidity = 65%, Pressure = 1013.1 hPa
Unregistering Statistics Display...
Weather station updated with new data:
Current Conditions Display: Temperature = 28.2°C, Humidity = 70%, Pressure = 1010.6 hPa
Step-by-Step Explanation
-
We define the
WeatherStation
class (the subject) which contains:- A delegate
WeatherChangedEventHandler
defining the signature for the event - An event
WeatherChanged
based on the delegate - Properties for temperature, humidity, and pressure
- A method
SetMeasurements
to update these values and notify observers - A method
NotifyObservers
that triggers the event
- A delegate
-
We create the
WeatherDisplay
class (the observer) which:- Has a method
OnWeatherChanged
that matches the signature of the event - Displays the weather information when notified
- Has a method
-
In the
Main
method:- We create one subject (weatherStation) and two observers (displays)
- We subscribe the observers to the weather station's event using
+=
- We call
SetMeasurements
to update the weather data, which automatically notifies all observers - We demonstrate unsubscribing from an event using
-=
Modern Implementation with EventHandler<T>
The above example works well, but modern C# practice favors using EventHandler<T>
with custom event args. Here's an updated implementation:
using System;
// Custom event arguments class
public class WeatherEventArgs : EventArgs
{
public float Temperature { get; }
public float Humidity { get; }
public float Pressure { get; }
public WeatherEventArgs(float temperature, float humidity, float pressure)
{
Temperature = temperature;
Humidity = humidity;
Pressure = pressure;
}
}
// The Subject class using EventHandler<T>
public class ModernWeatherStation
{
// Event using the generic EventHandler<T>
public event EventHandler<WeatherEventArgs> WeatherChanged;
private float temperature;
private float humidity;
private float pressure;
// Method to update measurements and notify observers
public void SetMeasurements(float temperature, float humidity, float pressure)
{
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
// Create event args
WeatherEventArgs args = new WeatherEventArgs(temperature, humidity, pressure);
// Notify all observers using the null-conditional operator
WeatherChanged?.Invoke(this, args);
}
}
// An Observer class
public class ModernDisplay
{
private string displayName;
public ModernDisplay(string name)
{
displayName = name;
}
// Event handler method
public void OnWeatherChanged(object sender, WeatherEventArgs e)
{
Console.WriteLine($"{displayName}: Temperature = {e.Temperature}°C, " +
$"Humidity = {e.Humidity}%, Pressure = {e.Pressure} hPa " +
$"(Source: {sender.GetType().Name})");
}
}
// Usage example
class ModernExample
{
static void RunExample()
{
// Create the subject
ModernWeatherStation weatherStation = new ModernWeatherStation();
// Create observers
ModernDisplay phoneApp = new ModernDisplay("Phone App");
ModernDisplay webDashboard = new ModernDisplay("Web Dashboard");
// Register observers
weatherStation.WeatherChanged += phoneApp.OnWeatherChanged;
weatherStation.WeatherChanged += webDashboard.OnWeatherChanged;
// Change the weather data
weatherStation.SetMeasurements(25.0f, 60.0f, 1012.0f);
}
}
Output:
Phone App: Temperature = 25°C, Humidity = 60%, Pressure = 1012 hPa (Source: ModernWeatherStation)
Web Dashboard: Temperature = 25°C, Humidity = 60%, Pressure = 1012 hPa (Source: ModernWeatherStation)
Key Improvements:
- Standard Event Pattern: Using
EventHandler<T>
follows C# conventions - Type Safety: Custom event args provide better type safety
- Null-conditional Operator: Using
?.Invoke()
safely invokes the event without explicit null checks - Reference to Sender: The event handler receives a reference to the sender object
Real-World Example: Stock Market Tracker
Let's look at a more practical example where the Observer Pattern would be useful - a stock market tracking application:
using System;
using System.Collections.Generic;
// Event arguments
public class StockChangedEventArgs : EventArgs
{
public string Symbol { get; }
public decimal OldPrice { get; }
public decimal NewPrice { get; }
public StockChangedEventArgs(string symbol, decimal oldPrice, decimal newPrice)
{
Symbol = symbol;
OldPrice = oldPrice;
NewPrice = newPrice;
}
public decimal PriceChange => NewPrice - OldPrice;
public decimal PercentageChange => (NewPrice - OldPrice) / OldPrice * 100;
}
// Stock - the subject
public class Stock
{
public string Symbol { get; }
private decimal price;
public decimal Price
{
get => price;
set
{
if (price != value)
{
decimal oldPrice = price;
price = value;
OnPriceChanged(new StockChangedEventArgs(Symbol, oldPrice, price));
}
}
}
// Define the event
public event EventHandler<StockChangedEventArgs> PriceChanged;
public Stock(string symbol, decimal initialPrice)
{
Symbol = symbol;
price = initialPrice;
}
// Method to trigger the event
protected virtual void OnPriceChanged(StockChangedEventArgs e)
{
PriceChanged?.Invoke(this, e);
}
}
// StockMarket - manages a collection of stocks
public class StockMarket
{
private Dictionary<string, Stock> stocks = new Dictionary<string, Stock>();
// Add a stock to the market
public void AddStock(string symbol, decimal initialPrice)
{
if (!stocks.ContainsKey(symbol))
{
Stock newStock = new Stock(symbol, initialPrice);
stocks.Add(symbol, newStock);
}
}
// Get a stock by symbol
public Stock GetStock(string symbol)
{
if (stocks.TryGetValue(symbol, out Stock stock))
{
return stock;
}
return null;
}
// Update a stock price
public void UpdatePrice(string symbol, decimal newPrice)
{
if (stocks.TryGetValue(symbol, out Stock stock))
{
stock.Price = newPrice;
}
}
}
// Observer - Stock Tracker
public class StockTracker
{
private string name;
public StockTracker(string name)
{
this.name = name;
}
// Track a stock by subscribing to its price changes
public void Track(Stock stock)
{
stock.PriceChanged += OnStockPriceChanged;
Console.WriteLine($"{name} has started tracking {stock.Symbol} at ${stock.Price}");
}
// Stop tracking a stock
public void Untrack(Stock stock)
{
stock.PriceChanged -= OnStockPriceChanged;
Console.WriteLine($"{name} has stopped tracking {stock.Symbol}");
}
// Event handler
private void OnStockPriceChanged(object sender, StockChangedEventArgs e)
{
string direction = e.PriceChange >= 0 ? "▲" : "▼";
Console.WriteLine($"{name} Alert: {e.Symbol} {direction} ${e.NewPrice:F2} " +
$"({e.PercentageChange:F2}%)");
}
}
// Usage example
class StockExample
{
public static void RunExample()
{
// Create a stock market
StockMarket market = new StockMarket();
// Add some stocks
market.AddStock("AAPL", 150.00m);
market.AddStock("MSFT", 250.00m);
market.AddStock("GOOGL", 2800.00m);
// Create trackers (observers)
StockTracker mobileApp = new StockTracker("Mobile App");
StockTracker desktopApp = new StockTracker("Desktop App");
// Start tracking stocks
mobileApp.Track(market.GetStock("AAPL"));
mobileApp.Track(market.GetStock("MSFT"));
desktopApp.Track(market.GetStock("GOOGL"));
desktopApp.Track(market.GetStock("AAPL"));
Console.WriteLine("\n--- Market Updates ---\n");
// Update prices and observe notifications
market.UpdatePrice("AAPL", 152.35m);
market.UpdatePrice("MSFT", 249.20m);
market.UpdatePrice("GOOGL", 2850.50m);
Console.WriteLine("\n--- Untracking AAPL from Mobile App ---\n");
// Unsubscribe one observer from a stock
mobileApp.Untrack(market.GetStock("AAPL"));
// Update again
market.UpdatePrice("AAPL", 153.75m);
}
}
Output:
Mobile App has started tracking AAPL at $150.00
Mobile App has started tracking MSFT at $250.00
Desktop App has started tracking GOOGL at $2800.00
Desktop App has started tracking AAPL at $150.00
--- Market Updates ---
Mobile App Alert: AAPL ▲ $152.35 (1.57%)
Desktop App Alert: AAPL ▲ $152.35 (1.57%)
Mobile App Alert: MSFT ▼ $249.20 (-0.32%)
Desktop App Alert: GOOGL ▲ $2850.50 (1.80%)
--- Untracking AAPL from Mobile App ---
Mobile App has stopped tracking AAPL
Desktop App Alert: AAPL ▲ $153.75 (0.92%)
This example demonstrates a realistic scenario where:
- Stocks act as subjects, emitting price change events
- Applications (like mobile and desktop apps) act as observers, subscribing to these changes
- The stock market manages the creation and updating of stocks
- Observers can dynamically subscribe and unsubscribe from updates
Best Practices for the Observer Pattern in C#
- Always check for null before invoking events (or use the
?.
operator) - Consider thread safety when implementing in multi-threaded applications
- Use weak references for long-lived subjects to avoid memory leaks
- Follow the standard event pattern with
EventHandler<T>
and custom event args - Keep observers lightweight to prevent performance issues
- Consider unsubscribing when observers are no longer needed
When to Use the Observer Pattern
The Observer Pattern is particularly useful when:
- You need to maintain consistency across related objects
- You want to decouple objects that interact with each other
- A change to one object requires changing others, but you don't know how many objects need to change
- An object needs to notify other objects without making assumptions about them
Summary
The Observer Pattern provides an elegant way to establish communication between objects without creating tight dependencies between them. In C#, this pattern is naturally supported through delegates and events, making it easy to implement.
We've explored:
- The basic structure of the Observer Pattern
- How to implement it using custom delegates and events
- A more modern approach using
EventHandler<T>
- A real-world application with a stock market tracker
- Best practices for implementing the pattern
By mastering the Observer Pattern, you'll be able to create more maintainable, flexible, and loosely coupled systems that can easily adapt to changing requirements.
Exercises
- Modify the weather station example to include a forecast observer that predicts weather based on changes in temperature, humidity, and pressure.
- Create a news publisher system where a news agency publishes articles and different subscribers (email, SMS, web) receive notifications.
- Implement a progress tracker where multiple UI elements (progress bar, status text, log) observe and update based on task completion.
- Add thread safety to the stock market example to ensure it works correctly with multiple threads updating stocks simultaneously.
- Create an observable collection class that notifies observers when items are added, removed, or modified.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)