C# Delegates and Events — Delegate Types, Multicast, Event, and EventHandler
In this tutorial, you will learn about C# Delegates and Events. We cover key concepts, practical examples, and best practices to help you master this topic.
C# delegates are type-safe references to methods that enable callback mechanisms, with events building on delegates to provide a standardized publish-subscribe notification pattern.
What You'll Learn
You will master delegates and events in C#: declaring and using delegate types, multicast delegates that invoke multiple methods, the event keyword for controlled subscription, the EventHandler delegate pattern, and how .NET uses events throughout the framework.
Why It Matters
Delegates and events are fundamental to the .NET Framework. Events power UI frameworks (button clicks, key presses), ASP.NET Core middleware pipelines, and system notifications. Delegates enable LINQ (Func/Action delegates), asynchronous programming, and callback patterns. Understanding these patterns is essential for writing decoupled, extensible code.
Real-World Use
UI applications use events for button clicks, text changes, and window events. ASP.NET Core uses delegates in middleware pipelines. Timer callbacks use EventHandler. Custom events are used in domain-driven design for domain events. Delegates are used for plug-in architectures and Strategy patterns.
Learning Path
graph LR
A["22: LINQ Advanced"] --> B["23: Delegates & Events"]
B --> C["24: Lambdas"]
C --> D["25: Extension Methods"]
D --> E["26: Nullable Reference Types"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Delegate Basics
// Declare a delegate type
public delegate void LogHandler(string message);
// Method matching the delegate signature
public static void ConsoleLogger(string message)
{
Console.WriteLine($"[Console] {message}");
}
public static void FileLogger(string message)
{
File.AppendAllText("log.txt", $"[File] {message}\n");
}
// Usage
LogHandler logger = ConsoleLogger;
logger("Application started"); // Invokes ConsoleLogger
logger = FileLogger;
logger("Error occurred"); // Invokes FileLogger
Multicast Delegates
A single delegate can invoke multiple methods:
LogHandler multiLogger = ConsoleLogger;
multiLogger += FileLogger;
multiLogger += (msg) => Console.WriteLine($"[Lambda] {msg}");
multiLogger("Event occurred");
// Invokes all three in order of subscription
// Removing handlers
multiLogger -= FileLogger;
multiLogger("After removal");
// Invokes ConsoleLogger and Lambda only
// Get invocation list
foreach (LogHandler handler in multiLogger.GetInvocationList())
{
Console.WriteLine($" Registered handler: {handler.Method.Name}");
}
Built-in Delegate Types: Func, Action, Predicate
// Action: returns void
Action sayHello = () => Console.WriteLine("Hello!");
Action<string> log = msg => Console.WriteLine(msg);
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);
sayHello(); // Hello!
log("Test"); // Test
printSum(3, 4); // 7
// Func: takes parameters, returns value
Func<int, int, int> add = (a, b) => a + b;
Func<double, double> square = x => x * x;
Func<string, int> parse = int.Parse;
Console.WriteLine(add(5, 3)); // 8
Console.WriteLine(square(4.0)); // 16
Console.WriteLine(parse("42")); // 42
// Predicate: returns bool
Predicate<int> isEven = x => x % 2 == 0;
Predicate<string> isLong = s => s.Length > 10;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isLong("Hi")); // False
The Event Keyword
Events restrict delegate access: only the declaring class can invoke the delegate; subscribers can only add/remove handlers.
public class Button
{
// Event declaration (uses EventHandler<T> pattern)
public event EventHandler? Clicked;
// Custom event with custom delegate
public event EventHandler<KeyEventArgs>? KeyPressed;
// Method that raises the event
public void Click()
{
Console.WriteLine("Button clicked, raising event...");
Clicked?.Invoke(this, EventArgs.Empty);
}
public void PressKey(char key)
{
KeyPressed?.Invoke(this, new KeyEventArgs(key));
}
}
public class KeyEventArgs : EventArgs
{
public char Key { get; }
public KeyEventArgs(char key) => Key = key;
}
// Usage
var button = new Button();
// Subscribe
button.Clicked += (sender, args) => Console.WriteLine(" Handler 1: Button was clicked!");
button.Clicked += (sender, args) => Console.WriteLine(" Handler 2: Logging click event");
button.Click();
// Output:
// Button clicked, raising event...
// Handler 1: Button was clicked!
// Handler 2: Logging click event
EventHandler Pattern
The standard .NET event pattern:
public class OrderService
{
// Standard event pattern
public event EventHandler<OrderProcessedEventArgs>? OrderProcessed;
public void ProcessOrder(int orderId, decimal amount)
{
Console.WriteLine($"Processing order #{orderId}...");
// Business logic
var args = new OrderProcessedEventArgs
{
OrderId = orderId,
Amount = amount,
ProcessedAt = DateTime.UtcNow,
Success = true
};
// Raise event (null check for no subscribers)
OnOrderProcessed(args);
}
// Protected virtual method for raising (inheritance-friendly)
protected virtual void OnOrderProcessed(OrderProcessedEventArgs e)
{
OrderProcessed?.Invoke(this, e);
}
}
public class OrderProcessedEventArgs : EventArgs
{
public int OrderId { get; init; }
public decimal Amount { get; init; }
public DateTime ProcessedAt { get; init; }
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
}
// Subscriber
var orderService = new OrderService();
orderService.OrderProcessed += (sender, e) =>
{
string status = e.Success ? "succeeded" : "failed";
Console.WriteLine($" Order #{e.OrderId} {status}: ${e.Amount}");
Console.WriteLine($" Processed at: {e.ProcessedAt:HH:mm:ss}");
};
orderService.ProcessOrder(1001, 299.99m);
Events with Custom Accessors (add/remove)
public class ButtonManager
{
private EventHandler? _clicked;
public event EventHandler? Clicked
{
add
{
Console.WriteLine($"Subscriber added: {value.Method.Name}");
_clicked += value;
}
remove
{
Console.WriteLine($"Subscriber removed: {value.Method.Name}");
_clicked -= value;
}
}
public void SimulateClick()
{
_clicked?.Invoke(this, EventArgs.Empty);
}
}
Delegates as Parameters
public class DataProcessor
{
public List<TResult> Process<T, TResult>(
IEnumerable<T> data,
Func<T, bool> filter,
Func<T, TResult> transformer)
{
return data
.Where(filter)
.Select(transformer)
.ToList();
}
}
var processor = new DataProcessor();
var numbers = Enumerable.Range(1, 20);
var result = processor.Process(
numbers,
filter: n => n % 2 == 0, // Keep only even
transformer: n => $"Number: {n} * 2 = {n * 2}"
);
foreach (var item in result)
Console.WriteLine(item);
Async Event Handlers
public class AsyncEventExample
{
public event EventHandler<AsyncEventArgs>? DataProcessed;
public async Task ProcessDataAsync()
{
Console.WriteLine("Processing data...");
await Task.Delay(100);
// Fire-and-forget for async event handlers
var handlers = DataProcessed?.GetInvocationList();
if (handlers != null)
{
var args = new AsyncEventArgs();
foreach (EventHandler<AsyncEventArgs> handler in handlers)
{
if (handler.Target is IAsyncEventHandler asyncHandler)
await asyncHandler.HandleAsync(this, args);
else
handler(this, args);
}
}
}
}
Common Mistakes
Mistake 1: Forgetting to Check for Null Before Invoking
event?.Invoke(this, args); is the safe pattern. Without the null check, if there are no subscribers, the event is null and causes a NullReferenceException.
Mistake 2: Using Public Delegate Fields Instead of Events
Public delegate fields allow external code to invoke the delegate directly. Events restrict invocation to the declaring class. Always use events for public notification points.
Mistake 3: Not Unsubscribing Events (Memory Leaks)
Subscribed event handlers prevent the subscriber from being garbage collected. Always unsubscribe when the subscriber is disposed:
button.Clicked -= ButtonHandler;
Mistake 4: Raising Events Outside the Declaring Class
Events can only be raised from within the class that declares them. This is intentional Encapsulation. Use a protected virtual method for derived class access.
Mistake 5: Assuming Event Handler Execution Order
The order in which multicast delegates execute is not guaranteed by the specification. Do not rely on subscription order for correctness.
Mistake 6: Using += Without Checking for Duplicates
+= adds the handler even if it is already subscribed. Each unsubscription removes only one instance. Use the same delegate instance for both add and remove.
Practice Questions
- What is the difference between a delegate and an event?
- How does multicast delegation work?
- Why does the EventHandler pattern use sender and EventArgs parameters?
- When would you use
Func<T, TResult>instead of a custom delegate type? - Write a TemperatureMonitor class that raises an event when the temperature exceeds a threshold.
Challenge
Create a simple publish-subscribe event bus that allows any type of event to be published and any subscriber to listen for specific event types. Use generics and the standard EventHandler pattern.
FAQ
Mini Project
Create a stock price monitoring system:
public class StockPriceChangedEventArgs : EventArgs
{
public string Symbol { get; init; }
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
public decimal ChangePercent => (NewPrice - OldPrice) / OldPrice * 100;
}
public class Stock
{
private decimal _price;
private static readonly Random _random = new();
public string Symbol { get; }
public decimal Price => _price;
public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;
public Stock(string symbol, decimal initialPrice)
{
Symbol = symbol;
_price = initialPrice;
}
public void UpdatePrice(decimal newPrice)
{
var oldPrice = _price;
_price = newPrice;
PriceChanged?.Invoke(this, new StockPriceChangedEventArgs
{
Symbol = Symbol,
OldPrice = oldPrice,
NewPrice = newPrice
});
}
public void SimulateDay()
{
var change = _price * (decimal)(_random.NextDouble() * 0.1 - 0.05);
UpdatePrice(_price + change);
}
}
public class StockTrader
{
public string Name { get; }
public StockTrader(string name) => Name = name;
public void OnPriceChanged(object? sender, StockPriceChangedEventArgs e)
{
if (e.ChangePercent > 3)
Console.WriteLine($" {Name}: ALERT! {e.Symbol} up {e.ChangePercent:F2}%");
else if (e.ChangePercent < -3)
Console.WriteLine($" {Name}: ALERT! {e.Symbol} down {e.ChangePercent:F2}%");
}
}
var msft = new Stock("MSFT", 400.00m);
var trader1 = new StockTrader("Alice");
var trader2 = new StockTrader("Bob");
msft.PriceChanged += trader1.OnPriceChanged;
msft.PriceChanged += trader2.OnPriceChanged;
Console.WriteLine("Stock Price Changes:");
for (int day = 1; day <= 5; day++)
{
Console.WriteLine($"\nDay {day}:");
msft.SimulateDay();
Console.WriteLine($" Current price: ${msft.Price:F2}");
}
Expected output (varies):
Stock Price Changes:
Day 1:
Alice: ALERT! MSFT down -3.50%
Bob: ALERT! MSFT down -3.50%
Current price: $386.00
Day 2:
Current price: $389.86
Day 3:
Alice: ALERT! MSFT up 4.12%
Bob: ALERT! MSFT up 4.12%
Current price: $405.92
Day 4:
Current price: $406.35
Day 5:
Current price: $401.47
What's Next
You have mastered delegates and events in C#. The next lesson covers lambdas: lambda syntax, closures, expression trees, and Func/Action/Predicate usage.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro