TaskCompletionSource Gotcha When Using Lambdas

A short while back, fellow Xamarin MVP Michael Ridland (@rid00z) published a great post here:

http://www.michaelridland.com/xamarin/taskcompletionsource-xamarin-beautiful-async/

on using TaskCompletionSource to 'mimic' async functionality and allow you to await things that you otherwise wouldn't be able to.

If you're using this to wrap calls to web services, as I do, there's something to watch out for. In this case you may want to wire up a completed event just to handle the completion of a specific call to the web service. Consider the following code:

public Task<YourReturnType> CallYourWebServiceAsync(int yourParameter, string yourOtherParameter )
{
    TaskCompletionSource<YourReturnType> tcs = new TaskCompletionSource<YourReturnType>();

    try
    {
        webService.yourWebServiceCallCompleted +=  (sender, e) =>
        {
            if (e.Cancelled)
            {
                tcs.TrySetCanceled();
            }
            else if (e.Error != null)
            {
                errorHandlingService.LogException(e.Error);
            }
            else
            {
                // Do stuff here
                tcs.SetResult(e.Result);
            }
        };

        webService.yourWebServiceCallAsync(yourParameter, yourOtherParameter);
    }
    catch (Exception ex)
    {
        errorHandlingService.LogException(ex);
    }

    return tcs.Task;
}

This assumes a couple of things:

  1. You have an instance of a class called webService (perhaps generated by SlSvcUtil.exe) to handle the actual web service calls.
  2. You have an instance of an errorHandlingService class - or you replace this with whatever you're using for error handling

We're using a lambda to create and wire up a completed event, and then we're calling out to the web service. This achieves what we're after. The web service gets called, the completed event gets fired on completion and allows us to get the return value or interrogate the exception which was returned. All well and good. However - where does that completed event get unwired? The simple answer is that if you don't take control of this and ensure that you're doing it yourself, you'll come unstuck. This can be achieved by instantiating an EventHandler to contain the code, and then wiring up and unwiring it yourself, like this:

 public Task<YourReturnType> CallYourWebServiceAsync(int yourParameter, string yourOtherParameter )
{
    TaskCompletionSource<YourReturnType> tcs = new TaskCompletionSource<YourReturnType>();

    try
    {
        EventHandler<getAuditDetailsCompletedEventArgs> handler = null;

        handler = (sender, e) =>
        {
            if (e.Cancelled)
            {
                tcs.TrySetCanceled();
            }
            else if (e.Error != null)
            {
                errorHandlingService.LogException(e.Error);
            }
            else
            {
                // Do stuff here
                tcs.SetResult(e.Result);
            }

            webService.getAuditDetailsCompleted -= handler;
        };
        webService.getAuditDetailsCompleted += handler;

        webService.yourWebServiceCallAsync(yourParameter, yourOtherParameter);
    }
    catch (Exception ex)
    {
        errorHandlingService.LogException(ex);
    }

    return tcs.Task;
}