Using Tasks in .NET 4.0 to retrieve an RSS feed

Building on my last post, I am going to use Tasks to retrieve and display RSS feeds in a WPF application. Since we are retrieving the feeds over the internet, we really don't need to use threads, as it isn't a CPU bound operation. Instead, we can leverage the existing .NET asynchronous event pattern that classes like WebClient already implement.

The guys over at Parallel Programming with .NET Blog have a great write-up about how to encapsulate this pattern into a Task. It seems they've also written extension methods for common BCL objects that implement this pattern in the Parallel Programming Samples.

Instead of constructing a Task directly, using this pattern, we retrieve a Task from a TaskCompletionSource. This object contains methods for controlling a Task object that it manages. After creating a TaskCompletionSource, we execute the asynchronous operation, and report to the TaskCompletionSource when the operation is finished, cancelled, or has an error. By putting all this logic into a static function, we make it really easy to create a Task to asynchronously retrieve a Stream from a Uri:

private static Task<Stream> StartReadTask(Uri address)  
{
    WebClient webClient = new WebClient();
    var tcs = new TaskCompletionSource<Stream>(address, 
        TaskCreationOptions.DetachedFromParent);

    OpenReadCompletedEventHandler handler = null;
    handler = (sender, e) =>
    {
        if (e.UserState == tcs)
        {
            if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(e.Result);
        }

        webClient.OpenReadCompleted -= handler;
    };

    webClient.OpenReadCompleted += handler;
    try 
    {
        webClient.OpenReadAsync(address, tcs); 
    }
    catch
    {
        webClient.OpenReadCompleted -= handler;
        tcs.TrySetCanceled();
        throw;
    }

    return tcs.Task;
}

As you can see, this function is responsible for actually calling the methods on the WebClient class that we are using to retrieve a Stream to the given Uri. It returns a Task, but it is slightly different from other Tasks that we are used to using. For instance, if you try to call Start on it, an exception will be thrown. This is because the Task isn't encapsulating work, it is more like a reporting mechanism for work being done. But, we can still use it to determine when the work has completed, like so:

Uri url = new Uri(uri);  
Task<Stream> t = StartReadTask(url);  
t.ContinueWith(ReadRssStream, TaskContinuationOptions.DetachedFromParent,  
    TaskScheduler.FromCurrentSynchronizationContext());

Since we are using the TaskScheduler from our current synchronization context (our UI thread), the ReadRssStream method will be run on our UI thread. In this method, we take the Stream that was retrieved in our Task, and attempts to parse it into RSS or Atom, and adds the items to the ObservableCollection of SyndicationItems:

private void ReadRssStream(Task<Stream> task)  
{
    if (task.Status == TaskStatus.RanToCompletion)
    {
        XmlReader rdr = XmlReader.Create(task.Result);
        IEnumerable<SyndicationItem> items;
        if (!TryReadRss(rdr, out items))
            TryReadAtom(rdr, out items);

        if (items == null)
        {
            Error = "No Rss feed found at the given url.";
        }
        else
        {
            _rssItems.Clear();
            foreach (var item in items)
                _rssItems.Add(item);
        }
    }
    else if (task.Exception != null)
    {
        Error = "Address Not Found.";
    }
}

In our Window, we just bind a ListBox to the list of feeds. The DataTemplate could probably be improved to show a prettier representation of the content, but this worked for this sample app:

<ListBox x:Name="_feeds"  
         DockPanel.Dock="Left" 
         Width="350"
         ItemsSource="{Binding RssItems}"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"
         HorizontalContentAlignment="Stretch"
         SelectionChanged="OnSelectedFeedChanged">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Vertical">
                <TextBlock FontSize="14"
                           FontWeight="Bold"
                           TextTrimming="CharacterEllipsis"
                           Text="{Binding Title.Text}"
                           Margin="0,0,0,4"/>
                <TextBlock TextWrapping="Wrap"
                           Text="{Binding Summary.Text}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>  

I'm really starting to like the new Task API. It definitely seems to make writing parallel / asynchronous code easier. Maybe after playing with it a bit more I'll have some complaints, but it certainly has been pretty nice so far!

Since this post is using features new to .NET 4.0, it was built using VS 2010 Beta 1.

Here is the source: RssViaTasks.zip

-AH


View or Post Comments