Using async/await with WebClient in Windows Phone 8 (or: TaskCompletionSource saves the day)

.NET, Phone, Technical stuff, Windows 8, Windows Phone
1 Comment

This post was imported from my old blog and had 4 comments which are included as a screenshot at the end of this post.

When you share code between Windows Phone 8 and Windows 8, you can use async/await to code against asynchronous APIs, which is a nice step in the right direction. However there are Windows Phone APIs that do not support async/await yet. Probably the most annoying to date is the WebClient (which in Windows 8 has been replaced by a better API called HttpClient).

Now, HttpClient’s methods support async/await. But WebClient’s methods do not support it, and so we have to resort to the old Completed event. This is annoying because even if we pack the HttpClient or the WebClient into a wrapper, we cannot make the wrapper’s methods compatible between Windows Phone 8 and Windows 8.

For instance, we can create a simple dataservice fetching an RSS feed. The service’s interface is:

public interface IRssService
{
    Task<IList<RssArticle>> GetArticles();
}

The DataService implementation uses a Downloader class to do the actual act of downloading. This abstracts the differences between Windows RT and Windows Phone and this object can be shared between the two environments.

public class RssService : IRssService
{
    private const string NewsPageUrl = "http://rss.cnn.com/rss/edition.rss";
 
    public async Task<IList<RssArticle>> GetArticles()
    {
        var downloader = new Downloader();
 
        var feed = await downloader.GetRssFeed(NewsPageUrl);
 
        var reader = new StringReader(feed);
        var document = XDocument.Load(reader);
 
        // Assume that feed is always well formed
 
        var list = document
            .Element("rss")
            .Element("channel")
            .Descendants("item")
            .Select(
                itemXml => new RssArticle
                {
                    Link = new Uri(itemXml.Element("link").Value),
                    Title = itemXml.Element("title").Value,
                    Summary = itemXml.Element("description").Value
                })
            .ToList();
 
        return list;
    }
}

In Windows 8, we use the HttpClient in the Downloader, and async/await to make the asynchronous execution flow more comfortable for the user:

// Windows RT downloader
public class Downloader
{
    public async Task<string> GetRssFeed(string feedUrl)
    {
        var client = new HttpClient();
        var result = await client.GetStringAsync(new Uri(feedUrl));
        return result;
    }
}

In Windows Phone 8, however, because of the usage of the DownloadStringCompleted event, you have to break the flow of execution and use a callback. As a consequence, the Windows Phone 8’s RssService cannot implement the IRssService interface, and there is a larger layer of code that needs to be rewritten instead of being shared.

// Downloader for WP8 using the Completed event
public void GetRssFeed(string feedUrl, Action<string, Exception> callback)
{
    var client = new WebClient();
    client.DownloadStringCompleted += (s, e) =>
    {
        if (e.Error == null)
        {
            callback(null, e.Error);
        }
        else
        {
            callback(e.Result, null);
        }
    };
 
    client.DownloadStringAsync(new Uri(feedUrl));
}

But thankfully there is a way. The way I am using is relying on the TaskCompletionSource’s class, which can be used to let the asynchronous method wait until a certain condition is completed. This can be useful, for instance, if you are sending multiple requests in parallel and waiting for all of them to complete before you continue execution, or in our case, if you are waiting for an asynchronous event to fire. We can rewrite the Windows Phone method and make it fit nicely in our async/await execution flow.

// Downloader for WP8 using the TaskCompletionSource
public Task<string> GetRssFeed(string feedUrl)
{
    var tcs = new TaskCompletionSource<string>();
 
    var client = new WebClient();
    client.DownloadStringCompleted += (s, e) =>
    {
        if (e.Error == null)
        {
            tcs.SetResult(e.Result);
        }
        else
        {
            tcs.SetException(e.Error);
        }
    };
 
    client.DownloadStringAsync(new Uri(feedUrl));
 
    return tcs.Task;
}

As soon as the method is executed, the TaskCompletionSource gets created, and then its Task property is returned on line 21. However the execution of the caller does not continue just yet. Instead, the caller waits (because it uses the await keyword) until the TaskCompletionSource’s SetResult (or, in case of error, SetException) method is called. This allows us to use the Downloader in the RssService in the exact same manner both in Windows RT and Windows Phone, and thus to share more code than if we had to use the callback.

Other usages for TaskCompletionSource

Like I mentioned above, TaskCompletionSource can also be used to synchronize multiple asynchronous operations. By definition, asynchronous operations are unpredictable. You can send the same sequence of requests multiple times and get results in a different order every time. One way to avoid that is to send the requests sequentially: Wait for a response before sending the next request. This simplifies the flow of the execution, but it also takes longer than sending the requests in parallel. Instead, it is better to take advantage of the architecture of the web, and to send the requests in parallel. Then, we wait until all the responses are back before returning the result to the caller.

For instance, the following code aggregates multiple RSS feeds. Each request is sent in parallel, which speeds up getting results by optimizing the usage of the server (or servers). However, to keep implementing the IRssService interface, we keep everything in one method and wait until all the responses get back to the client (or until an Exception is caught).

public class MultiRssService : IRssService
{
    private const string NewsPageUrl = "http://rss.cnn.com/rss/edition.rss";
    private const string TechPageUrl = "http://rss.cnn.com/rss/edition_technology.rss";
    private const string SpacePageUrl = "http://rss.cnn.com/rss/edition_space.rss";

    public Task<IList<RssArticle>> GetArticles()
    {
        var tcs = new TaskCompletionSource<IList<RssArticle>>();

        var list = new List<string>
        {
            NewsPageUrl,
            TechPageUrl,
            SpacePageUrl,
        };

        var completed = 0;
        var result = new List<RssArticle>();

        foreach (var url in list)
        {
            try
            {
                GetOneFeed(
                    url,
                    articles =>
                    {
                        result.AddRange(articles);

                        if (++completed == list.Count)
                        {
                            tcs.SetResult(result.ToList());
                        }
                    });
            }
            catch (Exception ex)
            {
                tcs.SetException(ex);
            }
        }

        return tcs.Task;
    }

    public async void GetOneFeed(string url, Action<IEnumerable<RssArticle>> callback)
    {
        var downloader = new Downloader();
        var feed = await downloader.GetRssFeed(url);
        callback(ParseFeed(feed));
    }

    private IEnumerable<RssArticle> ParseFeed(string feed)
    {
        var reader = new StringReader(feed);
        var document = XDocument.Load(reader);

        // Assume that feed is always well formed

        var list = document
            .Element("rss")
            .Element("channel")
            .Descendants("item")
            .Select(
                itemXml => new RssArticle
                {
                    Link = new Uri(itemXml.Element("link").Value),
                    Title = itemXml.Element("title").Value,
                    Summary = itemXml.Element("description").Value
                });

        return list;
    }
}

Conclusion

Asynchronous programming with async/await is more comfortable than the « old way » with callbacks, but it requires that the asynchronous method returns a Task in order to be awaitable. Since that is not always possible, especially when using old APIs that have not been updated yet. TaskCompletionSource, when available, offers a possibility to abstract the old APIs away and return the Task needed for the new methods to be awaitable. This can be used when sharing code (for instance between Windows RT and Windows Phone 8), or to synchronize multiple requests and return only one result.

Happy coding
Laurent

GalaSoft Laurent Bugnion
Laurent Bugnion (GalaSoft)


Share on Facebook

This post was imported from my old blog. Original comments screenshot: 2013111503

One Response to “Using async/await with WebClient in Windows Phone 8 (or: TaskCompletionSource saves the day)”

  1. Amol Says:

    Thank you Laurent,

    its really needful to me

Leave a Reply