TaskCompletionSource + Xamarin = Beautiful Async

If you’ve been using Xamarin for a while you probably already know of this functionality.

Just in case you haven’t I just want to give shout out to the TaskCompletionSource class. In short the completion source allows you to create a Task and return it to the caller without having to set the result or use an lambda.

It’s great for working with event based API’s like Native Bluetooth, NSSession and Native mapping engines. All of which provide results via events. The Native Bluetooth API in iOS is a great example of this, so let’s take a look.

TaskCompletionSource and Native API Sample

The function we want to build is below, it returns an awaitable task of float which is the Bluetooth Signal strength.

/// <summary>
/// Gets the Signal strength of Bluetooth Device
/// </summary>
/// <returns>The signal strength.</returns>
public Task<float> GetSignalStrength ()

 

The problem is that the Bluetooth API is based on events, so there’s no real easy way to support this Async. That’s where TaskCompletionSource comes in.
Let’s start by creating our TCS and returning the Task.
public Task<float> GetSignalStrength ()
{
    var bluetoothCompletion = new TaskCompletionSource<float> ();

    //do bluetooth work

    return bluetoothCompletion.Task; 
}

 

Now that we have the TaskCompletionSource there’s a few things we can set:

bluetoothCompletion.SetCanceled (); //Set the task as Canceled 
bluetoothCompletion.SetResult (888); //Sets the result as successful
bluetoothCompletion.SetException (new Exception ()); //Sets an exception

Great the caller has a Task that they can await. Now let’s do the Bluetooth work.

var centralManager = new CBCentralManager(DispatchQueue.CurrentQueue);

centralManager.DiscoveredPeripheral += (object sender, CBDiscoveredPeripheralEventArgs e) => {
    bluetoothCompletion.SetResult(e.RSSI.FloatValue);
  };

centralManager.FailedToConnectPeripheral += (object sender, CBPeripheralErrorEventArgs e) => {
    bluetoothCompletion.TrySetException(new Exception("Failed to connect to device"));
  };

Now let’s put them all together.

/// <summary>
/// Gets the Signal strength of Bluetooth Device
/// </summary>
/// <returns>The signal strength.</returns>
public Task<float> GetSignalStrength ()
{
    var bluetoothCompletion = new TaskCompletionSource<float> ();

    var centralManager = new CBCentralManager(DispatchQueue.CurrentQueue);

    centralManager.DiscoveredPeripheral += (object sender, CBDiscoveredPeripheralEventArgs e) => {
        bluetoothCompletion.SetResult(e.RSSI.FloatValue);
    };

    centralManager.FailedToConnectPeripheral += (object sender, CBPeripheralErrorEventArgs e) => {
        bluetoothCompletion.TrySetException(new Exception("Failed to connect to device"));
    };

    return bluetoothCompletion.Task;
}

 

Great, so now the caller can easily consumer this API in a async way.

var bluetoothStrength = await bluetoothService.GetSignalStrength();

This was a very simple example but the TaskCompletionSource can be used in many scenarios, it can be passed into functions and kept as member variables until you need it. So if it’s not already in your Toolkit, add it and use it, your code will be cleaner.

If you want to know more feel free to drop me a email.

Thanks

Michael

1 Response

  1. All though i am developing Xamarin from past 1.5years, i always thought TaskCompletionSource is used to make Synchronous calls. I never knew i can used await from the “caller” 🙂

Leave a Reply