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);
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);
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.