TaskCompletionSource - Creating Async APIs for Events & Callbacks

Making events great again!

Sometimes when writing async code we find ourselves in situations where we want to use libraries that are not async by design. For example you might want to await on the result from an event or callback. This is where TaskCompletionSource comes in handy, let us begin our journey with the definition from Microsoft Docs,

TaskCompletionSource<TResult> Class

Represents the producer side of a Task<TResult> unbound to a delegate, providing access to the consumer side through the Task property.

To understand the producer & consumer relationship of a Task<TResult>, think of the interaction between a waiter and diner in a restaurant. The waiter takes the diner’s order to the kitchen where all the work is done. While the meal is being prepared the diner is free to socialise or scroll through their Twitter feed. Once ready the waiter brings the meal to the diner for their enjoyment. In this analogy we have,

  • The producer as the waiter
    • the interface between the kitchen and the diner
  • The delegate as the kitchen
    • this is where all the hard work is done
  • The consumer as the diner
    • only concerned with the culinary result

With a TaskCompletionSource there is no delegate, it exposes a Task property to the consumer. It is up to you, dear developer, to set the result of that task.

Let’s say you have two buttons in your UI that expose a click event handler. You want a Task that returns true for one button and false for the other.

private Task<bool> MyAwesomeButtonTask()
{
    var tcs = new TaskCompletionSource<bool>();

    TrueBtn.Click += (sender, args) =>
    {
        tcs.TrySetResult(true);
    };
    FalseBtn.Click += (sender, args) =>
    {
        tcs.TrySetResult(false);
    };

    return tcs.Task;
}

The above will return a Task that produces the result true when TrueBtn is clicked, and false for FalseBtn. The best part is that the consumer can utilise the async/await pattern.

You can only set the result of a TaskCompletionSource once, so I prefer using TrySetResult. If you’re doing something that can cause an exceptional circumstance then use TrySetException (you do not need to set the result if an exception has been set).

I’ve found this technique really useful in Xamarin when writing async interfaces for platform specific APIs, which are generally event and callback based (e.g. Bluetooth & GPS). Take a look at the Xamarin Essentials GitHub repo, which provides excellent real world examples.

Next time you come across an event driven API and yearn for an async implementation, just remember that TaskCompletionSource has got you covered!

comments powered by Disqus