C# Part 6 – Events, Delegations, and Asynchronous Programming  

Moving on to C# Part 6, it delves into the essential topics of events and delegates within the C# programming language. Events and delegates are fundamental concepts in C# that enable a flexible and efficient way to handle communication between objects or components of a program.

Delegates act as function pointers, allowing methods to be passed as parameters, which is particularly useful for event handling. Events, on the other hand, provide a standardized way to declare, subscribe to, and trigger notifications when certain actions or conditions occur in a program.

Asynchronous programming in C# is a programming paradigm that allows tasks or operations to be executed independently without blocking the main program’s execution. It enables developers to initiate time-consuming tasks and continue with other work while waiting for those tasks to complete.

Events, Delegations, and Asynchronous Programming

Events and Delegations in C#

Delegations in C#

Delegates in C# serve as versatile function pointers, enabling the encapsulation and invocation of methods. They are instrumental in achieving flexibility and extensibility in C# code. Essentially, a delegate is a type that defines a method’s signature it can reference.

This allows for the dynamic assignment of methods at runtime, making delegates a powerful tool for implementing callbacks, event handling, and creating more modular and decoupled code.

By using delegates, developers can write cleaner and more maintainable code, as they facilitate the separation of concerns and the implementation of design patterns like the observer pattern.

Creating Delegates in C#

Creating a delegate in C# involves defining a delegate type that specifies the method signature it can reference. Delegates act as function pointers or references to methods, allowing for dynamic method invocation.

To create a delegate, you first declare the delegate type, specifying the return type and parameter types that match the method you want to reference. For instance, if you have a method with the signature int Calculate(int a, int b), you create a delegate like delegate int MyDelegate(int a, int b).

Once the delegate type is defined, you can instantiate it by assigning it to a method with a compatible signature. This enables you to call the method indirectly through the delegate. Delegates are essential for achieving callback mechanisms, event handling, and creating more modular and extensible code in C#.

using System;
namespace SimpleDelegateExample
{
    // Define a delegate type that matches the signature of the operation methods.
    delegate int MathOperation(int a, int b);
    class Calculator
    {
        // Method to add two numbers
        public static int Add(int a, int b)
        {
            return a + b;
        }

        // Method to subtract two numbers
        public static int Subtract(int a, int b)
        {
            return a - b;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Create instances of the MathOperation delegate and associate them with the Add and Subtract methods.
            MathOperation addDelegate = Calculator.Add;
            MathOperation subtractDelegate = Calculator.Subtract;

            // Perform addition operation using the delegate
            int result1 = addDelegate(5, 3);
            Console.WriteLine("5 + 3 = " + result1);

            // Perform subtraction operation using the delegate
            int result2 = subtractDelegate(8, 4);
            Console.WriteLine("8 - 4 = " + result2);

            Console.ReadLine();
        }
    }
}

Delegates as Parameter Types in C#

In C#, delegates can also be used as parameter types, offering a powerful way to pass functions as arguments to other methods. This capability enables the implementation of callback mechanisms and the creation of highly flexible and reusable code.

By accepting delegates as parameters, a method can receive instructions about which specific function to execute when a particular action or event occurs. This is particularly valuable in scenarios such as event handling, where different event handlers can be registered dynamically.

using System;

namespace DelegateAsParameterExample
{
    // Define a delegate type for a binary operation (takes two integers and returns an integer).
    delegate int BinaryOperation(int a, int b);

    class Calculator
    {
        // Method that performs a binary operation using a delegate as a parameter.
        public static int PerformOperation(int a, int b, BinaryOperation operation)
        {
            return operation(a, b);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Define delegate instances for addition and subtraction.
            BinaryOperation addDelegate = (x, y) => x + y;
            BinaryOperation subtractDelegate = (x, y) => x - y;

            // Use the PerformOperation method with delegates as parameters.
            int result1 = Calculator.PerformOperation(5, 3, addDelegate);
            Console.WriteLine("5 + 3 = " + result1);

            int result2 = Calculator.PerformOperation(8, 4, subtractDelegate);
            Console.WriteLine("8 - 4 = " + result2);

            Console.ReadLine();
        }
    }
}

Using Events in C#

Creating events in C# involves defining an event within a class using the event keyword, typically associated with a delegate type that specifies the event’s signature. This delegate type represents the format of methods that event handlers will use to respond to the event. The event acts as a notification mechanism that signals when a particular action or condition occurs within the class.

Subscribing to events is the process of registering event handlers, which are external methods or functions, to be executed when the event is triggered. Event handlers subscribe to an event using the += operator, establishing a link between the event and the method that will respond to it. Multiple event handlers can be registered for a single event, enabling multiple actions to occur when the event is invoked.

Invoking events is the act of triggering the event. This is typically done within the class that defines the event, using the EventName?.Invoke(this, EventArgs.Empty) syntax. The ?. operator ensures that the event is only invoked if there are registered event handlers. When the event is invoked, all registered event handlers associated with that event are called, and they execute their respective logic in response to the event.

Unsubscribing from events is the process of removing event handlers that were previously registered. Event handlers can be unsubscribed using the -= operator. This is useful when an object or component no longer needs to respond to a particular event, helping to prevent memory leaks and unnecessary event handling as the program continues to run.

using System;

namespace EventExample
{
    class Button
    {
        // Declare a Click event using the EventHandler delegate.
        public event EventHandler Click;

        // Method to simulate a button click event.
        public void SimulateClick()
        {
            Console.WriteLine("Button clicked!");
            OnClick();
        }

        // Method to raise the Click event.
        protected virtual void OnClick()
        {
            Click?.Invoke(this, EventArgs.Empty);
        }
    }

    class Program
    {
        // Event handler method for the Button's Click event.
        static void Button_Click(object sender, EventArgs e)
        {
            Console.WriteLine("Button click event handled.");
        }

        static void Main(string[] args)
        {
            Button button = new Button();

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

            // Simulate a button click, which will invoke the Click event.
            button.SimulateClick();

            // Unsubscribe from the Click event.
            button.Click -= Button_Click;

            // Simulate another button click, but this time, no event handlers will be called.
            button.SimulateClick();

            Console.ReadLine();
        }
    }
}

Asynchronous programming in C#

Asynchronous programming in C# is a programming paradigm that offers a more efficient and responsive way to handle tasks that may take time to complete, such as I/O operations, network communication, or long-running computations. It enables developers to write code that doesn’t block the main execution thread, allowing other tasks to proceed concurrently.

One of the key features of asynchronous programming in C# is the async and await keywords. By marking a method with the async keyword, you signal that it contains asynchronous code.

The await keyword is used within an async method to indicate points where the program can pause and allow the main thread to continue executing other tasks. When the awaited task completes, the program returns to the async method to continue its work.

Asynchronous programming is particularly valuable in scenarios where responsiveness is crucial. For instance, in a graphical user interface (GUI) application, blocking the main thread with time-consuming operations can lead to a frozen or unresponsive UI.

By employing asynchronous techniques, such as asynchronous event handling or parallel execution, developers can ensure that the UI remains responsive to user interactions while background tasks proceed independently.

Moreover, asynchronous programming also contributes to efficient resource utilization. In situations like server applications, where numerous clients connect and request data simultaneously, asynchronous methods allow the server to handle multiple requests concurrently without the need for excessive threading, improving scalability and performance.

In conclusion, asynchronous programming in C# is a vital technique that promotes responsiveness and scalability in applications. By using the async and await keywords and other asynchronous constructs, developers can write code that efficiently manages time-consuming tasks, leading to more robust and user-friendly software.

Asynchronous methods in C#

Asynchronous methods allow developers to execute tasks concurrently and improve the responsiveness of applications. These methods are identified by the async modifier, which signifies that they contain asynchronous code.

Within an asynchronous method, developers can use the await keyword to pause the execution of that method until a specific asynchronous operation completes, such as I/O operations, network requests, or time-consuming computations.

By doing so, asynchronous methods enable the main program thread to continue executing other tasks, preventing it from blocking and ensuring that the application remains responsive to user interactions.

In C#, the Task and Task<T> types are integral components of the Task Parallel Library (TPL), designed to simplify and manage asynchronous and parallel programming. The Task type represents an asynchronous operation that may or may not return a result. It’s used for operations where the outcome or return value is not immediately needed.

Task<T>, on the other hand, is a generic type and is employed when you expect a result of type T from the asynchronous operation. Both types allow you to schedule, manage, and await asynchronous operations efficiently.

Using these types, developers can work with asynchronous code more effectively by avoiding the complexity of manual thread management. Tasks provide mechanisms for waiting on, cancelling, composing, and aggregating asynchronous operations. This abstraction simplifies concurrent programming, making it easier to create responsive and scalable applications while abstracting the underlying details of threading and synchronization. Whether working with I/O-bound or CPU-bound tasks, the Task and Task<T> types are powerful tools that help streamline asynchronous programming in C#.

using System;
using System.Threading.Tasks;

namespace AsyncMethodExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Fetching data asynchronously...");

            // Call the asynchronous method to fetch data.
            string result = await FetchDataAsync();

            // Display the fetched data.
            Console.WriteLine("Fetched data: " + result);

            Console.ReadLine();
        }

        static async Task<string> FetchDataAsync()
        {
            // Simulate an asynchronous operation, such as making an HTTP request.
            await Task.Delay(2000); // Simulate a delay of 2 seconds.

            return "This is the fetched data.";
        }
    }
}

Task.Run and TaskFactory in C#

Task.Run and TaskFactory are essential components of the Task Parallel Library (TPL) that provide convenient ways to create and manage tasks for parallel and asynchronous programming.

Task.Run is a static method that simplifies the creation of tasks by allowing developers to specify a delegate (typically a lambda expression) representing the work to be performed concurrently. It encapsulates the task creation process and executes it on the ThreadPool, making it easy to parallelize work.

using System;
using System.Threading.Tasks;

namespace TaskRunExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main thread started.");

            // Start a task using Task.Run to perform work concurrently.
            Task task = Task.Run(() => PerformParallelWork());

            Console.WriteLine("Main thread continues executing other tasks.");

            // Wait for the parallel task to complete.
            task.Wait();

            Console.WriteLine("Main thread finished.");

            Console.ReadLine();
        }

        static void PerformParallelWork()
        {
            Console.WriteLine("Parallel work started.");
            // Simulate some time-consuming operation.
            Task.Delay(2000).Wait();
            Console.WriteLine("Parallel work completed.");
        }
    }
}

On the other hand, TaskFactory is a class that offers more control and customization over task creation. Developers can use TaskFactory to define custom options, such as task cancellation tokens, scheduling options, and child tasks, making it a versatile tool for complex parallel scenarios.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskFactoryExample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main thread started.");

            // Create a TaskFactory instance.
            TaskFactory factory = new TaskFactory();

            // Use TaskFactory to create and start a new task.
            Task task1 = factory.StartNew(() => PerformTask(1));
            Task task2 = factory.StartNew(() => PerformTask(2));

            // Wait for both tasks to complete.
            Task.WaitAll(task1, task2);

            Console.WriteLine("Both tasks have completed.");

            Console.ReadLine();
        }

        static void PerformTask(int taskId)
        {
            Console.WriteLine($"Task {taskId} started.");
            // Simulate some work.
            Thread.Sleep(2000);
            Console.WriteLine($"Task {taskId} completed.");
        }
    }
}

The async and await pattern in C#

The async and await pattern is a powerful and user-friendly approach to asynchronous programming that simplifies the development of responsive and non-blocking applications.

This pattern leverages the async and await keywords to create asynchronous methods that can execute time-consuming or I/O-bound operations without blocking the main program’s execution.

By marking a method as async, you signal that it contains asynchronous code. When the await keyword is used within an async method, it allows the program to pause the method’s execution without blocking the main thread, enabling other tasks to proceed concurrently.

Once the awaited operation completes, the program returns to the async method to continue its work. This pattern greatly enhances the maintainability of asynchronous code by making it look more like synchronous code, making it easier to reason about and debug.

using System;
using System.IO;
using System.Threading.Tasks;

namespace AsyncAwaitExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Reading a file asynchronously...");

            // Call an asynchronous method using the async and await keywords.
            string fileContents = await ReadFileAsync("sample.txt");

            Console.WriteLine("File contents:");
            Console.WriteLine(fileContents);

            Console.ReadLine();
        }

        static async Task<string> ReadFileAsync(string fileName)
        {
            try
            {
                // Asynchronously read the file contents.
                using (StreamReader reader = new StreamReader(fileName))
                {
                    return await reader.ReadToEndAsync();
                }
            }
            catch (Exception ex)
            {
                return $"Error: {ex.Message}";
            }
        }
    }
}

I/O-bound operations in C#

I/O-bound operations refer to tasks or operations that are primarily limited by input and output operations, such as reading from or writing to files, accessing databases, making network requests, or interacting with external resources.

These operations tend to be time-consuming because they involve waiting for data to be read from or written to external sources, during which the CPU remains relatively idle. To handle I/O-bound operations efficiently, asynchronous programming techniques like the async and await pattern are commonly used in C#.

By performing I/O-bound operations asynchronously, the program can continue executing other tasks while waiting for the I/O operation to complete, thus improving the overall responsiveness and efficiency of the application.

using System;
using System.IO;
using System.Threading.Tasks;

namespace AsyncIOExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Reading a file and performing another task concurrently...");

            // Start reading the file asynchronously.
            Task<string> readFileTask = ReadFileAsync("sample.txt");

            // Perform another task concurrently.
            DoAnotherTask();

            // Wait for the file reading task to complete.
            string fileContents = await readFileTask;

            Console.WriteLine("File contents:");
            Console.WriteLine(fileContents);

            Console.ReadLine();
        }

        static async Task<string> ReadFileAsync(string fileName)
        {
            try
            {
                // Asynchronously read the file contents.
                using (StreamReader reader = new StreamReader(fileName))
                {
                    return await reader.ReadToEndAsync();
                }
            }
            catch (Exception ex)
            {
                return $"Error: {ex.Message}";
            }
        }

        static void DoAnotherTask()
        {
            Console.WriteLine("Concurrent task started.");
            // Simulate some work.
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Concurrent task: " + i.ToString());
                Task.Delay(5).Wait();
            }
            Console.WriteLine("Concurrent task completed.");
        }
    }
}

ConfigureAwait in C#

ConfigureAwait is a method that can be used in combination with the await keyword when working with asynchronous code. It allows developers to control the context in which the asynchronous operation continues after it completes.

By default, when an asynchronous operation completes, it continues executing on the same context (e.g., the UI thread or the current synchronization context) from which it was called. However, in some scenarios, it may be desirable to specify a different context for the continuation, especially when avoiding deadlocks or improving performance in certain applications.

The ConfigureAwait method accepts a boolean parameter (continueOnCapturedContext) that determines whether the continuation should use the captured context (true) or not (false).

Setting it to false can be particularly useful in scenarios where you want to avoid context capture, such as in ASP.NET Core or other server-based applications, to prevent potential deadlocks and improve scalability. In contrast, in UI applications, you may often want to continue on the captured context to ensure that the continuation can safely interact with the UI elements.

In this example, we use ConfigureAwait(false) when awaiting the PerformAsyncOperation method. This means that the continuation of the asynchronous operation (await Task.Delay) will not use the captured synchronization context. As a result, it might execute on a different thread, potentially improving performance in certain scenarios, such as server applications.

using System;
using System.Threading.Tasks;

namespace ConfigureAwaitExample
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Main thread: " + Environment.CurrentManagedThreadId);

            await PerformAsyncOperation().ConfigureAwait(false);

            Console.WriteLine("Back to the main thread: " + Environment.CurrentManagedThreadId);

            Console.ReadLine();
        }

        static async Task PerformAsyncOperation()
        {
            Console.WriteLine("Inside async method: " + Environment.CurrentManagedThreadId);

            await Task.Delay(1000); // Simulate an asynchronous delay.

            Console.WriteLine("Continuation inside async method: " + Environment.CurrentManagedThreadId);
        }
    }
}

The Observer Pattern in C#

The Observer Pattern is a behavioral design pattern that defines a one-to-many dependency between objects, allowing one object (the subject) to notify multiple other objects (the observers) about changes in its state.

This pattern is widely used in C# for implementing event handling, such as in GUI frameworks, where various UI elements need to respond to changes in a data model.

To implement the Observer Pattern in C#, you typically follow these steps.

Define the Subject

Create a class that represents the subject or the object being observed. This class contains the state that observers are interested in tracking.

Define the Observer

Create an observer interface or an abstract class that declares methods (typically named Update or similar) that observers must implement to respond to changes in the subject’s state.

Implement Concrete Observers

Create one or more concrete observer classes that implement the observer interface/abstract class. These classes define how they should react when notified of changes in the subject.

Maintain a List of Observers

Inside the subject class, maintain a collection (e.g., a list or dictionary) to keep track of registered observers.

Notify Observers

When the subject’s state changes, it iterates through its list of observers and calls their update methods, passing relevant data. This notifies all registered observers of the change.

Register and Unregister Observers

Provide methods in the subject class to allow observers to register (subscribe) and unregister (unsubscribe) themselves. This allows for dynamic observer management.

using System;
using System.Collections.Generic;

// Step 2: Define the Observer interface.
interface IObserver
{
    void Update(string message);
}

// Step 1: Define the Subject.
class Subject
{
    private List<IObserver> observers = new List<IObserver>();
    private string state;

    public void RegisterObserver(IObserver observer)
    {
        observers.Add(observer);
    }

    public void UnregisterObserver(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void SetState(string newState)
    {
        state = newState;
        NotifyObservers();
    }

    private void NotifyObservers()
    {
        foreach (var observer in observers)
        {
            observer.Update(state);
        }
    }
}

// Step 3: Implement Concrete Observers.
class ConcreteObserver : IObserver
{
    private string name;

    public ConcreteObserver(string name)
    {
        this.name = name;
    }

    public void Update(string message)
    {
        Console.WriteLine($"{name} received update: {message}");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Create the subject and observers.
        Subject subject = new Subject();
        var observer1 = new ConcreteObserver("Observer 1");
        var observer2 = new ConcreteObserver("Observer 2");

        // Register observers with the subject.
        subject.RegisterObserver(observer1);
        subject.RegisterObserver(observer2);

        // Set the subject's state, which notifies the observers.
        subject.SetState("New state!");

        // Unregister an observer.
        subject.UnregisterObserver(observer1);

        // Set the state again to notify remaining observer.
        subject.SetState("Another state!");

        Console.ReadLine();
    }
}

Using Events versus the Observer Pattern

Using events and the Observer Pattern are both techniques for implementing the publish-subscribe pattern, where an object (the subject or publisher) notifies multiple other objects (observers or subscribers) about changes in its state. However, they differ in terms of their implementation and the scenarios where they are most suitable.

Using Events

Events are a built-in language feature in C# and many other programming languages, making them relatively easy to use and understand. Events are typically implemented as delegate types and are commonly used in event-driven programming, such as graphical user interfaces (GUIs) and handling user interactions.

Simplicity

Events provide a straightforward way to handle notifications and decouple the subject from its observers. Event handlers can be subscribed to and unsubscribed from events using the += and -= operators.

Language Support

Events are a language-level construct in C# and are directly integrated with Visual Studio’s designer tools, making them suitable for GUI applications.

Built-in Delegates

Events often use built-in delegate types like EventHandler and Action for event signatures, simplifying the implementation.

Limited Customization

While events are easy to use, they offer limited customization options, which may be a limitation in complex scenarios.

Using the Observer Pattern

The Observer Pattern, on the other hand, is a design pattern that is not tied to any specific language feature, and it provides a more flexible and customizable way to implement the publish-subscribe pattern.

Flexibility

The Observer Pattern allows for greater flexibility and customization. It defines explicit observer interfaces and can support different notification mechanisms beyond events, such as custom update methods.

Decoupling

The Observer Pattern encourages loose coupling between subjects and observers, as it doesn’t rely on language-specific event mechanisms.

Complex Scenarios

The Observer Pattern is well-suited for complex scenarios where you need more control over how subjects notify observers or where you want to implement custom observer interfaces.

Additional Abstractions

Implementing the Observer Pattern may require more code and additional abstractions compared to using events directly.

Choosing between events and the Observer Pattern depends on the specific requirements of your application. If you need a simple and standardized way to handle events, events are a good choice, especially in GUI applications. However, if you require greater flexibility, customization, or support for more complex scenarios, the Observer Pattern provides a more versatile approach to implement the publish-subscribe pattern in C# and other object-oriented languages.

Read more about events in Microsoft Learn.

Read more about Asynchronous Programming Patterns in Microsoft Learn.