Rico Suter's blog.
 


In my projects I have a lot of asynchronous methods with a callback as parameter which is called when the operation has completed. I asked myself how to port these methods to support the new async / await keywords and if possible allow me to use the class with older frameworks which do not support this new functionality. The samples in this article use the Http classes from my library project MyToolkit.

We start with a simple method called LoadHtml which will call the callback action completed after completion:

public void LoadHtml(String url, Action<HttpResponse> completed)
{
    Http.Get(url, completed);
}

To take advantage of the new async / await paradigm introduced in C# 5, we need to implement a new method, called LoadHtmlAsync (the naming convention for Task-based methods requires to add the postfix Async to the method name) which returns a Task object. To remove the callback method, you can use the TaskCompletionSource class which has the method SetResult to set the result after completion. The SetResult method will be called in the callback and will complete the current task:

#if WINRT
public Task<HttpResponse> LoadHtmlAsync(string url)
{
    var task = new TaskCompletionSource<HttpResponse>();
    LoadHtml(url, result => { task.SetResult(result); });
    return task.Task;
}
#endif

Now, the new method can easily be used with the await keyword:

var response = await LoadHtmlAsync("http://www.domain.com");

To minimize redundant code, I’ve developed a helper class (download) which encapsulates the previous shown mechanism:

public static class TaskHelper
{
    public static Task<TResult> RunCallbackMethod<TResult>(Action<Action<TResult>> func)
    {
        var task = new TaskCompletionSource<TResult>();
        func(result => { task.SetResult(result); });
        return task.Task;
    }

    public static Task<TResult> RunCallbackMethod<TResult, T1>(Action<T1, Action<TResult>> func, T1 a)
    {
        var task = new TaskCompletionSource<TResult>();
        func(a, result => { task.SetResult(result); });
        return task.Task;
    }

    public static Task<TResult> RunCallbackMethod<TResult, T1, T2>(Action<T1, T2, Action<TResult>> func, T1 a, T2 b)
    {
        var task = new TaskCompletionSource<TResult>();
        func(a, b, result => { task.SetResult(result); });
        return task.Task;
    }
}

This way the original class with the method LoadHtml can be extended with a WinRT version. Now it’s possible to create two library projects: One for example for Silverlight and one for WinRT with the same files (linked or included from the same directory). You simply have to add the “WINRT” conditional compilation symbol in the WinRT library.

public class MyClass
{
    public void LoadHtml(String url, Action<HttpResponse> completed)
    {
        Http.Get(url, completed);
    }

#if WINRT
    public Task<HttpResponse> LoadHtmlAsync(string url)
    {
        return TaskHelper.RunCallbackMethod<HttpResponse, string>(LoadHtml, url);
    }
#endif
}

Exceptions and cancellation

The Task object also supports exceptions and cancellation which should be used instead of an exception property or canceled flag in the result object. The final LoadHtmlAsync method looks as follows:

public static async Task<HttpResponse> LoadHtmlAsync(string url)
{
    var task = new TaskCompletionSource<HttpResponse>();
    LoadHtml(url, result =>
    {
        if (result.Successful)
            task.SetResult(result);
        else if (result.Canceled)
            task.SetCanceled();
        else
            task.SetException(result.Exception);
    });
    return task.Task;
}

The method can be used like this:

try
{
    var response = await LoadHtmlAsync("http://www.domain.com");
}
catch (OperationCanceledException e)
{
    // TODO add your cancellation logic
}
catch (Exception e)
{
    // TODO add your exception handling logic
}

And here is the code to use the “legacy” version of the method with callbacks:

var response = LoadHtml(url, response => 
{
    if (response.Successful)
    {
        // TODO process html response
    }
    else if (response.Canceled)
    {
        // TODO add your cancellation logic
    }
    else
    {
        var exception = response.Exception;
        // TODO add your exception handling logic
    }
});


Discussion