Awaiting events

Introduction

Right now .NET has support for multiple asynchronous programming models. Two very popular models are the ‘events’ model and the ‘async/await’ model. When working with different libraries you will see both models being used side by side. But what if you want to convert the event model to the async/await model?

The problem

Events are great when things are simple, like reacting to a button click or a key stroke. But things will get messy really fast when more complicated things must be done based on multiple events, like reacting to a button click while a key is being pressed. When you try this you will most likely end up in the infamous ‘Callback Hell’.

The new async/await model solves a lot of the issues that the event model has. For example, doing something once two tasks are completed is made super easy with the async/await model.

public async Task DoSomethingOnceTwoTasksComplete()
{
    Task one = Task.Run(...);
    Task two = Task.Run(...);

    await Task.WhenAll(one, two);

    // Do something once both tasks are completed
}

So I was wondering if it is possible to convert a event to a task. I found out it is!

Case

To demonstrate the problem and the solution we will work on the following case:

An UWP application has multiple pages that all live inside the same Frame instance. This frame contains a Navigated event. This event is fired when the navigation to a given Page type is completed (documentation). In stead of listening to this event we will try to ‘await’ this event. So we can do the following:

// Our goal
public async Task<Page> Navigated()
{
    return await frame.Navigated;
} 

The solution

To solve this ‘problem’ we will be using the TaskCompletionSource<T> class (documentation). Using this TaskCompletionSource<T> a method can return an instance to a task, which it can complete at any moment by calling the SetResult(T) method.

To convert a event to a task we will hook up an event handler which will complete the TaskCompletionSource<T>. A simple implementation is given in the following code snippet:

public async Task<Page> Navigated()
{
    var result = new TaskCompletionSource<Page>();
    frame.Navigated += (sender, args) =>
    {
        // Complete the task once the event is fired
        result.SetResult(args.Content as Page);
    };

    // Return a refernce to the task that will be completed 
    // once the `SetResult(T)` method is called
    return result.Task;
}

Pitfall

The given solution will get the job done, but it still has two problems. First, an event can be fired multiple times. But a Task can only be completed once. If you called the SetResult(T) method more then once, it will throw an exception. Secondly, the event handler will never be unsubscribed. This can result in memory leaks in your application.

Final solution

To solve both problems we need to unregister the event handler after it is called. By doing this it won’t be possible to complete the task multiple times. This will also solve our potential memory leak.

The code snippet below demonstrates how this can be achieved.

// Bad example
public async Task<Page> Navigated()
{
    var result = new TaskCompletionSource<Page>();

    // Create a event handler
    NavigatedEventHandler eventHandler = null;
    eventHandler = (sender, args) =>
    {
        result.SetResult(args.Content as Page);

        // Unregister the event handler once the event is fired 
        frame.Navigated -= eventHandler;
    };

    // Register the event handler
    frame.Navigated += eventHandler;

    return result.Task;
}
Further reading

Comments