Skip to main content

C# Event Handlers

Introduction

Event handlers are a crucial part of C# programming, especially when building interactive applications. They form the foundation of event-driven programming, allowing your code to respond to actions like button clicks, data changes, or system notifications.

In this tutorial, we'll explore event handlers in C#, which build upon the delegate concept we've covered previously. You'll learn how to create, subscribe to, and trigger events with practical examples that will help you understand how event handlers work in real-world applications.

What is an Event Handler?

An event handler in C# is a method that gets called when a specific event occurs. It's essentially a way for objects to notify other objects when something of interest happens.

Let's break down the key components:

  1. Event Publisher - The class that contains and raises the event
  2. Event Subscriber - The class that registers for and responds to the event
  3. Event Handler - The method that executes when the event is raised
  4. Event Arguments - Data associated with the event

Basic Syntax for Event Handlers

The standard pattern for an event handler method in C# follows this signature:

csharp
void EventHandlerMethod(object sender, EventArgs e)
  • sender: The object that raised the event
  • EventArgs: Contains any additional data related to the event (or EventArgs.Empty if no data)

Creating a Simple Event

Let's start with a simple example of creating and handling an event:

csharp
using System;

// Event publisher class
public class Button
{
// Step 1: Define an event using a delegate
public event EventHandler Click;

// Method to simulate clicking the button
public void SimulateClick()
{
// Step 2: Raise the event
OnClick(EventArgs.Empty);
}

// Protected virtual method to raise the Click event
protected virtual void OnClick(EventArgs e)
{
// Step 3: Check if there are any subscribers
Click?.Invoke(this, e);
}
}

// Program to demonstrate event handling
class Program
{
static void Main()
{
// Step 4: Create an instance of the publisher
Button button = new Button();

// Step 5: Subscribe to the event
button.Click += Button_Click;

// Step 6: Trigger the event
Console.WriteLine("Simulating button click...");
button.SimulateClick();

// Output:
// Simulating button click...
// Button was clicked!
}

// Step 7: Event handler method
private static void Button_Click(object sender, EventArgs e)
{
Console.WriteLine("Button was clicked!");
}
}

In this example, we've created a Button class with a Click event. When SimulateClick() is called, it raises the event, which then calls any subscribed event handlers.

Custom Event Arguments

Often, you'll want to pass additional data with your events. For this, you can create a custom event args class:

csharp
using System;

// Step 1: Create custom event args
public class MessageEventArgs : EventArgs
{
public string Message { get; }
public DateTime Timestamp { get; }

public MessageEventArgs(string message)
{
Message = message;
Timestamp = DateTime.Now;
}
}

// Event publisher
public class MessagePublisher
{
// Step 2: Define the event with custom event args
public event EventHandler<MessageEventArgs> MessageReceived;

// Method to publish a message
public void PublishMessage(string message)
{
// Step 3: Create event args and raise event
MessageEventArgs args = new MessageEventArgs(message);
OnMessageReceived(args);
}

// Protected method to raise the event
protected virtual void OnMessageReceived(MessageEventArgs e)
{
MessageReceived?.Invoke(this, e);
}
}

class Program
{
static void Main()
{
// Create publisher
MessagePublisher publisher = new MessagePublisher();

// Subscribe to the event
publisher.MessageReceived += HandleMessage;

// Publish a message
publisher.PublishMessage("Hello, Events!");

// Output:
// Message received at 2023-09-26 14:30:45: Hello, Events!
}

// Event handler with custom event args
static void HandleMessage(object sender, MessageEventArgs e)
{
Console.WriteLine($"Message received at {e.Timestamp}: {e.Message}");
}
}

This example shows how to create custom event arguments to pass additional data (a message and timestamp) when the event is raised.

Multiple Event Handlers

One of the powerful features of events is that multiple handlers can subscribe to the same event. Let's see how this works:

csharp
using System;

public class NotificationService
{
public event EventHandler<string> Notification;

public void SendNotification(string message)
{
OnNotification(message);
}

protected virtual void OnNotification(string message)
{
Notification?.Invoke(this, message);
}
}

class Program
{
static void Main()
{
var service = new NotificationService();

// Subscribe multiple handlers
service.Notification += LogNotification;
service.Notification += DisplayNotification;
service.Notification += SendEmailNotification;

// Send a notification
service.SendNotification("System update required");

// Unsubscribe one handler
Console.WriteLine("\nUnsubscribing email notifications...\n");
service.Notification -= SendEmailNotification;

// Send another notification
service.SendNotification("Process completed");

// Output:
// LOG: System update required
// DISPLAY: System update required
// EMAIL: System update required
//
// Unsubscribing email notifications...
//
// LOG: Process completed
// DISPLAY: Process completed
}

static void LogNotification(object sender, string message)
{
Console.WriteLine($"LOG: {message}");
}

static void DisplayNotification(object sender, string message)
{
Console.WriteLine($"DISPLAY: {message}");
}

static void SendEmailNotification(object sender, string message)
{
Console.WriteLine($"EMAIL: {message}");
}
}

In this example, we subscribe three different handlers to the same event. When the event is raised, all three handlers execute in the order they were subscribed. We then demonstrate how to unsubscribe a handler.

Anonymous Event Handlers

You can also use anonymous methods or lambda expressions as event handlers:

csharp
using System;

public class Counter
{
public event EventHandler<int> ThresholdReached;

private int threshold;
private int total;

public Counter(int threshold)
{
this.threshold = threshold;
}

public void Add(int value)
{
total += value;
if (total >= threshold)
{
OnThresholdReached(total);
}
}

protected virtual void OnThresholdReached(int total)
{
ThresholdReached?.Invoke(this, total);
}
}

class Program
{
static void Main()
{
Counter counter = new Counter(10);

// Subscribe using a lambda expression
counter.ThresholdReached += (sender, total) =>
Console.WriteLine($"Threshold reached with value: {total}");

// Add values
counter.Add(5);
counter.Add(3);
counter.Add(4); // This will trigger the event

// Output:
// Threshold reached with value: 12
}
}

Anonymous handlers like this are convenient for simple event handling without creating separate methods.

Real-World Example: Progress Reporting

Let's look at a practical example where events are commonly used - reporting progress during a long-running operation:

csharp
using System;
using System.Threading;

public class ProgressEventArgs : EventArgs
{
public int PercentComplete { get; }
public string StatusMessage { get; }

public ProgressEventArgs(int percentComplete, string statusMessage)
{
PercentComplete = percentComplete;
StatusMessage = statusMessage;
}
}

public class FileProcessor
{
public event EventHandler<ProgressEventArgs> ProgressChanged;
public event EventHandler ProcessingComplete;

public void ProcessFiles(int fileCount)
{
for (int i = 0; i < fileCount; i++)
{
// Simulate processing a file
Thread.Sleep(500);

// Calculate progress percentage
int percentComplete = (i + 1) * 100 / fileCount;

// Report progress
OnProgressChanged(new ProgressEventArgs(
percentComplete,
$"Processed file {i + 1} of {fileCount}"));
}

// Processing complete
OnProcessingComplete();
}

protected virtual void OnProgressChanged(ProgressEventArgs e)
{
ProgressChanged?.Invoke(this, e);
}

protected virtual void OnProcessingComplete()
{
ProcessingComplete?.Invoke(this, EventArgs.Empty);
}
}

class Program
{
static void Main()
{
var processor = new FileProcessor();

// Subscribe to events
processor.ProgressChanged += (sender, e) =>
{
Console.WriteLine($"Progress: {e.PercentComplete}% - {e.StatusMessage}");
DrawProgressBar(e.PercentComplete);
};

processor.ProcessingComplete += (sender, e) =>
{
Console.WriteLine("\nAll files have been processed!");
};

// Start processing
Console.WriteLine("Starting file processing...\n");
processor.ProcessFiles(5);

// Output:
// Starting file processing...
//
// Progress: 20% - Processed file 1 of 5
// [== ]
// Progress: 40% - Processed file 2 of 5
// [==== ]
// Progress: 60% - Processed file 3 of 5
// [====== ]
// Progress: 80% - Processed file 4 of 5
// [======== ]
// Progress: 100% - Processed file 5 of 5
// [==========]
//
// All files have been processed!
}

static void DrawProgressBar(int percentComplete)
{
int width = 10;
int filledWidth = width * percentComplete / 100;

Console.Write("[");
for (int i = 0; i < width; i++)
{
if (i < filledWidth)
Console.Write("=");
else
Console.Write(" ");
}
Console.WriteLine("]");
}
}

This example demonstrates how events can be used to report progress during a long-running operation, which is a common pattern in many applications.

Best Practices for Event Handlers

When working with events in C#, follow these best practices:

  1. Always check for null before raising an event:

    csharp
    MyEvent?.Invoke(this, eventArgs);
  2. Use the standard naming conventions:

    • Event names should be verbs or verb phrases (like "Click", "ValueChanged")
    • Event handler methods should be named Object_Event
    • Event argument classes should end with "EventArgs"
  3. Make event-raising methods protected and virtual to allow proper inheritance.

  4. Unsubscribe from events when you no longer need them to prevent memory leaks.

  5. Keep event handlers small and focused on a single responsibility.

  6. Consider thread safety when events might be raised from different threads.

Summary

Event handlers are a powerful feature in C# that enable event-driven programming. They allow objects to communicate with each other without creating tight coupling. In this tutorial, we've covered:

  • The basics of event handlers and their syntax
  • Creating and subscribing to events
  • Custom event arguments
  • Working with multiple event handlers
  • Using anonymous methods and lambda expressions
  • A real-world example with progress reporting

Understanding event handlers is essential for building responsive and maintainable C# applications, especially those with user interfaces or that need to respond to external actions or state changes.

Additional Resources

Exercises

  1. Create a simple Timer class that raises an event called Tick every second.

  2. Build a StockMonitor class that raises events when stock prices change by more than a specified threshold.

  3. Implement a Logger class that publishes events for different log levels (Info, Warning, Error), and create multiple subscribers that handle each level differently.

  4. Extend the FileProcessor example to include pause, resume, and cancel functionality using events.

  5. Create a custom control that raises events when a user interacts with it in different ways.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)