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
- More ways to solve this problem: StackOverflow - Is it possible to await an event instead of another async method?
- A more in-depth guide to this solution: MSDN - Tasks and the Event-based Asynchronous Pattern
Comments