How to: Wrap EAP Patterns in a Task

The following example shows how to expose an arbitrary sequence of Event-Based Asynchronous Pattern (EAP) operations as one task by using a TaskCompletionSource<TResult>. The example also shows how to use a CancellationToken to invoke the built-in cancellation methods on the WebClient objects.

Example

class WebDataDownloader
{

    static void Main()
    {
        WebDataDownloader downloader = new WebDataDownloader();
        string[] addresses = { "http://www.msnbc.com", "http://www.yahoo.com",
                                 "http://www.nytimes.com", "http://www.washingtonpost.com",
                                 "http://www.latimes.com", "http://www.newsday.com" };
        CancellationTokenSource cts = new CancellationTokenSource();

        // Create a UI thread from which to cancel the entire operation
        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("Press c to cancel");
            if (Console.ReadKey().KeyChar == 'c')
                cts.Cancel();
        });

        // Using a neutral search term that is sure to get some hits.
        Task<string[]> webTask = downloader.GetWordCounts(addresses, "the", cts.Token);

        // Do some other work here unless the method has already completed.
        if (!webTask.IsCompleted)
        {
            // Simulate some work.
            Thread.SpinWait(5000000);
        }

        string[] results = null;
        try
        {
            results = webTask.Result;
        }
        catch (AggregateException e)
        {
            foreach (var ex in e.InnerExceptions)
            {
                OperationCanceledException oce = ex as OperationCanceledException;
                if (oce != null)
                {
                    if (oce.CancellationToken == cts.Token)
                    {
                        Console.WriteLine("Operation canceled by user.");
                    }
                }
                else
                    Console.WriteLine(ex.Message);
            }
        }
        finally
        {
            cts.Dispose();
        }
        if (results != null)
        {
            foreach (var item in results)
                Console.WriteLine(item);
        }
        Console.ReadKey();
    }
           
    Task<string[]> GetWordCounts(string[] urls, string name, CancellationToken token)
    {
        TaskCompletionSource<string[]> tcs = new TaskCompletionSource<string[]>();
        WebClient[] webClients = new WebClient[urls.Length];

        // If the user cancels the CancellationToken, then we can use the
        // WebClient's ability to cancel its own async operations.
        token.Register(() =>
            {
                foreach (var wc in webClients)
                {
                    if (wc != null)
                        wc.CancelAsync();
                }                    
            });

        object m_lock = new object();
        int count = 0;
        List<string> results = new List<string>();
        for (int i = 0; i < urls.Length; i++)
        {
            webClients[i] = new WebClient();

            #region callback
            // Specify the callback for the DownloadStringCompleted
            // event that will be raised by this WebClient instance.
            webClients[i].DownloadStringCompleted += (obj, args) =>
             {
                 if (args.Cancelled == true)
                 {
                     tcs.TrySetCanceled();
                     return;
                 }
                 else if (args.Error != null)
                 {
                     // Pass through to the underlying Task
                     // any exceptions thrown by the WebClient
                     // during the asynchronous operation.
                     tcs.TrySetException(args.Error);
                     return;
                 }
                 else
                 {
                     // Split the string into an array of words,
                     // then count the number of elements that match
                     // the search term.
                     string[] words = null;
                     words = args.Result.Split(' ');
                     string NAME = name.ToUpper();
                     int nameCount = (from word in words.AsParallel()
                                      where word.ToUpper().Contains(NAME)
                                      select word)
                                     .Count();

                     // Associate the results with the url, and add new string to the array that 
                     // the underlying Task object will return in its Result property.
                     results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, name));
                 }

                 // If this is the last async operation to complete,
                 // then set the Result property on the underlying Task.
                 lock (m_lock)
                 {
                     count++;
                     if (count == urls.Length)
                     {
                         tcs.TrySetResult(results.ToArray());
                     }
                 }
             };
            #endregion

            // Call DownloadStringAsync for each URL.
            Uri address = null;
            try
            {
                address = new Uri(urls[i]);
                // Pass the address, and also use it for the userToken 
                // to identify the page when the delegate is invoked.
                webClients[i].DownloadStringAsync(address, address);
            }

            catch (UriFormatException ex)
            {
                // Abandon the entire operation if one url is malformed.
                // Other actions are possible here.
                tcs.TrySetException(ex);
                return tcs.Task;
            }
        }

        // Return the underlying Task. The client code
        // waits on the Result property, and handles exceptions
        // in the try-catch block there.
        return tcs.Task;
    }
Class WebDataDownLoader

    Dim tcs As New TaskCompletionSource(Of String())
    Dim nameToSearch As String
    Dim token As CancellationToken
    Dim results As New List(Of String)
    Dim m_lock As Object
    Dim count As Integer
    Dim addresses() As String

    Shared Sub Main()

        Dim downloader As New WebDataDownLoader()
        downloader.addresses = {"http://www.msnbc.com", "http://www.yahoo.com", _
                                     "http://www.nytimes.com", "http://www.washingtonpost.com", _
                                     "http://www.latimes.com", "http://www.newsday.com"}
        Dim cts As New CancellationTokenSource()

        ' Create a UI thread from which to cancel the entire operation
        Task.Factory.StartNew(Sub()
                                  Console.WriteLine("Press c to cancel")
                                  If Console.ReadKey().KeyChar = "c"c Then
                                      cts.Cancel()
                                  End If
                              End Sub)

        ' Using a neutral search term that is sure to get some hits on English web sites.
        ' Please substitute your favorite search term.
        downloader.nameToSearch = "the"
        Dim webTask As Task(Of String()) = downloader.GetWordCounts(downloader.addresses, downloader.nameToSearch, cts.Token)

        ' Do some other work here unless the method has already completed.
        If (webTask.IsCompleted = False) Then
            ' Simulate some work
            Thread.SpinWait(5000000)
        End If

        Dim results As String() = Nothing
        Try
            results = webTask.Result
        Catch ae As AggregateException
            For Each ex As Exception In ae.InnerExceptions
                If (TypeOf (ex) Is OperationCanceledException) Then
                    Dim oce As OperationCanceledException = CType(ex, OperationCanceledException)
                    If oce.CancellationToken = cts.Token Then
                        Console.WriteLine("Operation canceled by user.")
                    End If
                Else
                    Console.WriteLine(ex.Message)
                End If

            Next
        Finally
           cts.Dispose()
        End Try

        If (Not results Is Nothing) Then
            For Each item As String In results
                Console.WriteLine(item)
            Next
        End If

        Console.WriteLine("Press any key to exit")
        Console.ReadKey()
    End Sub

    Public Function GetWordCounts(ByVal urls() As String, ByVal str As String, ByVal token As CancellationToken) As Task(Of String())

        Dim webClients() As WebClient
        ReDim webClients(urls.Length)
        m_lock = New Object()

        ' If the user cancels the CancellationToken, then we can use the
        ' WebClient's ability to cancel its own async operations.
        token.Register(Sub()
                           For Each wc As WebClient In webClients
                               If Not wc Is Nothing Then
                                   wc.CancelAsync()
                               End If
                           Next
                       End Sub)


        For i As Integer = 0 To urls.Length - 1
            webClients(i) = New WebClient()

            ' Specify the callback for the DownloadStringCompleted
            ' event that will be raised by this WebClient instance.
            AddHandler webClients(i).DownloadStringCompleted, AddressOf WebEventHandler

            Dim address As Uri = Nothing
            Try
                address = New Uri(urls(i))
                ' Pass the address, and also use it for the userToken 
                ' to identify the page when the delegate is invoked.
                webClients(i).DownloadStringAsync(address, address)
            Catch ex As UriFormatException
                tcs.TrySetException(ex)
                Return tcs.Task
            End Try

        Next

        ' Return the underlying Task. The client code
        ' waits on the Result property, and handles exceptions
        ' in the try-catch block there.
        Return tcs.Task
    End Function



    Public Sub WebEventHandler(ByVal sender As Object, ByVal args As DownloadStringCompletedEventArgs)

        If args.Cancelled = True Then
            tcs.TrySetCanceled()
            Return
        ElseIf Not args.Error Is Nothing Then
            tcs.TrySetException(args.Error)
            Return
        Else
            ' Split the string into an array of words,
            ' then count the number of elements that match
            ' the search term.
            Dim words() As String = args.Result.Split(" "c)
            Dim NAME As String = nameToSearch.ToUpper()
            Dim nameCount = (From word In words.AsParallel()
                            Where word.ToUpper().Contains(NAME)
                            Select word).Count()

            ' Associate the results with the url, and add new string to the array that 
            ' the underlying Task object will return in its Result property.
            results.Add(String.Format("{0} has {1} instances of {2}", args.UserState, nameCount, nameToSearch))
        End If

        SyncLock (m_lock)
            count = count + 1
            If (count = addresses.Length) Then
                tcs.TrySetResult(results.ToArray())
            End If
        End SyncLock
    End Sub

See Also

TPL and Traditional .NET Framework Asynchronous Programming