Using TaskCompletionSource to Turn an Ugly Duckling into a Swan

TL;DR

If you're not interested in the background story, you can skip to this part

Background Story

The example here is in Xamarin. If you are not familiar with Xamarin, do not get discouraged, since the techniques used here are universal.

A while ago I wrote a Xamarin.Android application that had some ugly reflection in it. It looked something like this:

Original C# code

var bluetoothPanClass = Class.ForName("android.bluetooth.BluetoothPan");
var btPanCtor = bluetoothPanClass.GetDeclaredConstructor(Class.FromType(typeof(Context)), 
                  Class.FromType(typeof(IBluetoothProfileServiceListener)));
btPanCtor.Accessible = true;

var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
  .Single(m => m.Name == "setBluetoothTethering");
setBluetoothTetheringMethod.Accessible = true;

var btServiceListener = new BTPanServiceListener();
var btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);

btServiceListener.ServiceConnected += (s, e) =>
  setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);

Here I make a new instance of the BluetoothPan class and call the setBluetoothTethering method.

Now, the reflection is unavoidable. But the code above is not very reusable. What if I want to do other things with that BluetoothPan class? Surely, I don't want to think about reflection every time I need to deal with this class. Let's create a wrapper that hides all the difficulties. Once done, usage should look something like this:

using the wrapper

var wrapper = new BTPanWrapper(context);
wrapper.SetBluetoothTethering(isEnabled);

The Wrapper

Most of the Wrapper code is quite simple, just a one-on-one mapping between the methods of the Java class and our C# class. For example:

mapping setBluetoothTethering

public void SetBluetoothTethering(bool isEnabled)
{
  var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
    .Single(m => m.Name == "setBluetoothTethering");
  setBluetoothTetheringMethod.Accessible = true;

  setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
}

However, there is a larger problem here. The BluetoothPan instance needs to be initialized asynchronously before we call any method. If we stick to the original code, we would end up with something like this:

var wrapper = new BTPanWrapper(context);
wrapper.InstanceReady += (s, e) =>
    wrapper.SetBluetoothTethering(isEnabled);
drake no like

Of course, that's not how we normally do async stuff in C#. Much more appreciated would be:

var wrapper = new BTPanWrapper(context);
await wrapper.InitBTPanServiceAsync();
wrapper.SetBluetoothTethering(isEnabled);
drake like

Android uses a lot of events for asynchronous operations, because Java doesn't use async and await. This leads to messy code quickly. Also when dealing with the older parts of .NET, you'll find a lot of events for async operations or even those pesky BeginXXX-EndXXX combos. Fortunately, there is this thing called TaskCompletionSource that can wrap any async operation into an awaitable Task.

TaskCompletionSource

The way it works is quite simple. Consider the following code:

public Task<string> GetDataWrapper(int value)
{
  var tcs = new TaskCompletionSource<string>();

  var proxy = new Service1Client();
  //start async operation
  proxy.GetDataAsync(value);
  //will be raised when work is done
  proxy.GetDataCompleted += (s, e) => tcs.SetResult(e.Result);

  return tcs.Task;
}

You create a new method that returns a Task with the type of the result, in this case string. You create a TaskCompletionSource with the same return type. The TaskCompletionSource contains a Task. This Task should be immediately returned. In the meantime you can start the asynchronous operation you want to wrap. When that one completes, you inform the TaskCompletionSource to set the result (or error) on the returned task. How the wrapped operation indicates readiness doesn't matter.

Back to the Wrapper

This is the complete code of the BluetoothPan wrapper. Let's go over the interesting details.

Complete Wrapper Code

class BTPanWrapper
{
  private static readonly Class bluetoothPanClass;
  private static readonly Constructor btPanCtor;

  private readonly Context context;

  private BTPanServiceListener btServiceListener;
  private Java.Lang.Object btSrvInstance;
  private TaskCompletionSource<Java.Lang.Object> getServiceTcs;

  static BTPanWrapper()
  {
    bluetoothPanClass = Class.ForName("android.bluetooth.BluetoothPan");
    btPanCtor = bluetoothPanClass.GetDeclaredConstructor(Class.FromType(typeof(Context)), 
                  Class.FromType(typeof(IBluetoothProfileServiceListener)));
    btPanCtor.Accessible = true;
  }

  public BTPanWrapper(Context context)
  {
    this.context = context;
  }

  public Task<Java.Lang.Object> InitBTPanServiceAsync()
  {
    getServiceTcs = new TaskCompletionSource<Java.Lang.Object>();

    btServiceListener = new BTPanServiceListener();
    btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);

    btServiceListener.ServiceConnected += OnServiceConnected;

    return getServiceTcs.Task;
  }

  private void OnServiceConnected(object sender, EventArgs e)
  {
    getServiceTcs.SetResult(btSrvInstance);
    btServiceListener.ServiceConnected -= OnServiceConnected;
  }

  public void SetBluetoothTethering(bool isEnabled)
  {
    var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
      .Single(m => m.Name == "setBluetoothTethering");
    setBluetoothTetheringMethod.Accessible = true;

    setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
  }

  // Other Methods of BTPan...

  private class BTPanServiceListener : Java.Lang.Object, IBluetoothProfileServiceListener
  {
    public event EventHandler ServiceConnected;

    public void OnServiceConnected([GeneratedEnum] ProfileType profile, IBluetoothProfile proxy)
      => ServiceConnected?.Invoke(this, EventArgs.Empty);

    public void OnServiceDisconnected([GeneratedEnum] ProfileType profile) { }
  }

}

First, notice the static constructor:

static BTPanWrapper()
{
  bluetoothPanClass = Class.ForName("android.bluetooth.BluetoothPan");
  btPanCtor = bluetoothPanClass.GetDeclaredConstructor(Class.FromType(typeof(Context)), 
                Class.FromType(typeof(IBluetoothProfileServiceListener)));
  btPanCtor.Accessible = true;
}

To avoid double work, I've set up some reflection that needs to happen exactly once.

The instance constructor doesn't really do a lot

public BTPanWrapper(Context context)
{
  this.context = context;
}

That's because all the interesting stuff happend in InitBTPanServiceAsync:

public Task<Java.Lang.Object> InitBTPanServiceAsync()
{
  getServiceTcs = new TaskCompletionSource<Java.Lang.Object>();

  btServiceListener = new BTPanServiceListener();
  btSrvInstance = btPanCtor.NewInstance(context, btServiceListener);

  btServiceListener.ServiceConnected += OnServiceConnected;

  return getServiceTcs.Task;
}

Here we create the instance of the Java BlueToothPan class and wait until it has raised the ServiceConnected event. When that happens, we set the TaskCompletionSource's task to completed:

private void OnServiceConnected(object sender, EventArgs e)
{
  getServiceTcs.SetResult(btSrvInstance);
  btServiceListener.ServiceConnected -= OnServiceConnected;
}

I decided not to use a lambda expression for OnServiceConnected. Because I want to unsubscribe from that event whenever I want. Now, I'm unsubscribing immediately to avoid keeping the btServiceListener instance alive. This won't have a great impact. But the fact the you cannot unsubscribe with a lambda has always bugged me.

Then we see a bunch of wrapped methods:

public void SetBluetoothTethering(bool isEnabled)
{
  var setBluetoothTetheringMethod = bluetoothPanClass.GetDeclaredMethods()
    .Single(m => m.Name == "setBluetoothTethering");
  setBluetoothTetheringMethod.Accessible = true;

  setBluetoothTetheringMethod.Invoke(btSrvInstance, isEnabled);
}

And finally, I moved this BTPanServiceListener class as an inner class. This way the outside world doesn't have to know about this dirty little secret. (see this blog for more detail)

private class BTPanServiceListener : Java.Lang.Object, IBluetoothProfileServiceListener
{
  public event EventHandler ServiceConnected;

  public void OnServiceConnected([GeneratedEnum] ProfileType profile, IBluetoothProfile proxy)
    => ServiceConnected?.Invoke(this, EventArgs.Empty);

  public void OnServiceDisconnected([GeneratedEnum] ProfileType profile) { }
}

Conclusion

TaskCompletionSource is an excellent tool to have in your belt. It turns any poor asynchronous code into nicely awaitable tasks. This is especially useful when building code that is meant to be reused a lot like libraries.