Home > Articles > Programming > Visual Basic

  • Print
  • + Share This
This chapter is from the book

This chapter is from the book

Cancellation and Progress

Because the Async pattern relies on the Task class, implementing cancellation is something similar to what you have already studied back in Chapter 41, thus the CancellationTokenSource and CancellationToken classes are used. In this section you see how to implement cancellation both for asynchronous methods and for Task.Run operations. Next, you learn how to report the progress of an operation, which is common in asynchronous programming and improves the user experience.

Implementing Cancellation

Let’s look at the WPF sample application created in the section “Getting Started with Async/Await.” Imagine you want to give users the ability of cancelling the download of the RSS feed from the Microsoft Channel9 website. First, make a slight modification to the user interface so that the main Grid is divided into two rows, and in the first row add a Button like this:

<Grid.RowDefinitions>
    <RowDefinition Height="40"/>
    <RowDefinition/>
</Grid.RowDefinitions>
<Button Width="120" Height="30" Name="CancelButton"
        Content="Cancel"/>

Do not forget to add the Grid.Row="1" property assignment for the ListBox control. Double-click the new button so that you can quickly access the code editor. Declare a new CancellationTokenSource object that will listen for cancellation requests. The event handler for the new button’s Click event will invoke the Cancel method on the instance of the CancellationTokenSource:

Private tokenSource As CancellationTokenSource

Private Sub CancelButton_Click(sender As Object, e As RoutedEventArgs) _
    Handles CancelButton.Click
    'If Me.tokenSource IsNot Nothing Then
    '    Me.tokenSource.Cancel()
    'End If
    Me.tokenSource?.Cancel()
End Sub

The user can now request cancellation by clicking this button. Next, you need to make a couple of edits to the QueryVideosAsync method created previously. The first edit is making this method receive a CancellationToken object as an argument. This object will handle cancellation requests during the method execution. The second edit requires replacing the WebClient class with a new class called HttpClient. The reason for this change is that the WebClient’s asynchronous methods no longer support cancellation as in the first previews of the Async library, although asynchronous methods in System.Net.Http.HttpClient do. Among the others, this class exposes a method called GetAsync that retrieves contents from the specified URL and receives the cancellation token as the second argument. The result is returned under the form of a System.Net.Http.HttpResponseMessage class. As the name implies, this class represents an HTTP response message including the status of the operation and the retrieved data. The data is represented by a property called Content, which exposes methods to convert data into a stream (ReadAsStreamAsync), into an array of bytes (ReadAsByteArrayAsync), and into a string (ReadAsStringAsync). Other than changing the code to use HttpClient and to receive the cancellation token, you only need to handle the OperationCanceledException, which is raised after the cancellation request is received by the asynchronous method. The following code demonstrates the QueryVideosAsync method:

'The following implementation with HttpClient supports Cancellation
Private Async Function QueryVideosAsync(token As CancellationToken) As  _
        Task(Of IEnumerable(Of Video))
    Try
        Dim client As New HttpClient

        'Get the feed content as an HttpResponseMessage
        Dim data = Await client.GetAsync(New Uri(Video.FeedUrl), token)

        'Parse the content into a String
        Dim actualData = Await data.Content.ReadAsStringAsync

        Dim doc = XDocument.Parse(actualData)

        Dim query = From video In doc...<item>
                    Select New Video With {
                    .Title = video.<title>.Value,
                    .Speaker = video.<dc:creator>.Value,
                    .Url = video.<link>.Value,
                    .Thumbnail = video...<media:thumbnail>.
                        FirstOrDefault?.@url,
                    .DateRecorded = String.Concat("Recorded on ",
                        Date.Parse(video.<pubDate>.Value,
                        Globalization.CultureInfo.InvariantCulture).
                        ToShortDateString)}

        Return query
    Catch ex As OperationCanceledException
        MessageBox.Show("Operation was canceled by the user.")
        Return Nothing
    Catch ex As Exception
        MessageBox.Show(ex.Message)
        Return Nothing
    End Try
End Function

The very last edit to the application is changing the LoadVideosAsync method to launch the query passing a cancellation token:

Private Async Function LoadVideosAsync() As Task
    Me.tokenSource = New CancellationTokenSource

    Me.DataContext = Await QueryVideosAsync(Me.tokenSource.Token)
End Function

If you now run the application, not only will the user interface remain responsive, but you will be also able to click the Cancel button to stop the query execution. Notice that in a synchronous approach, implementing cancellation has no benefits. In fact, if on one side writing code to support cancellation is legal, on the other side the user would never have a chance to click a button because the UI thread would be blocked until the completion of the task. Similarly, you can add cancellation to tasks running in a separate thread and started with Task.Run. By continuing the example shown previously about this method, you can first rewrite the SimulateIntensiveWork method as follows:

Private Function SimulateIntensiveWork(token As CancellationToken) _
         As Integer
    Dim delay As Integer = 5000
    Threading.Thread.Sleep(delay)

        token.ThrowIfCancellationRequested()

    Return delay
End Function

You should be familiar with this approach because it has been discussed in Chapter 41. The method receives the cancellation token and checks for cancellation requests. If any exist, it throws an OperationCanceledException. Next, you add support for cancellation by passing an instance of the CancellationTokenSource class to the method invocation inside Task.Run:

Private cancellationToken As CancellationTokenSource

Private Async Sub RunIntensiveWorkAsync()
    cancellationToken = New CancellationTokenSource
    'This runs on the UI thread
    Console.WriteLine("Starting...")

    Try
        'This runs on a Thread Pool thread
        Dim result As Integer = Await Task.Run(Function()
                                                   Dim workResult As Integer = _
                                                   SimulateIntensiveWork( _
                                                   cancellationToken.Token)
                                                   Return workResult
                                               End Function)

        'This runs again on the UI thread
        Console.WriteLine("Finished")
    Catch ex As OperationCanceledException
        Console.WriteLine("Canceled by the user.")
    Catch ex As Exception

    End Try
    Console.ReadLine()
End Sub

To request cancellation, you should call the cancellationToken.Cancel method. At that point, the request is intercepted and an OperationCanceledException is thrown.

Reporting Progress

Reporting the progress of an asynchronous method execution is a common requirement. There is a pattern that you can use and that makes things easier. This pattern relies on the System.IProgress(Of T) interface and the System.Progress(Of T) class, which expose a ProgressChanged event that must be raised when the asynchronous operation is in progress. To provide an example that is easy to understand, imagine you still want to download the content of some feeds from the Microsoft Channel9 website and refresh the progress every time a site has been downloaded completely. The current example is based on a Console application. Consider the following code:

Private progress As Progress(Of Integer)
Private counter As Integer = 0

Sub Main()
    Try
        progress = New Progress(Of Integer)
        AddHandler progress.ProgressChanged, Sub(sender, e)
                                                 Console.
                                                 WriteLine _
                                                 ("Download progress: " & _
                                                 CStr(e))
                                             End Sub

        DownloadAllFeedsAsync(progress)

    Catch ex As Exception
        Console.WriteLine(ex.Message)
    Finally
        Console.ReadLine()
    End Try
End Sub

You first declare an object of type Progress(Of Integer) and a counter. The first object will receive the progress value when the ProgressChanged event is raised. In this case, the code uses the Integer type to pass a simple number, but you can pass more complex information with a different or custom type. Then the code specifies a handler for the ProgressChanged event, with type inference for the lambda’s parameters. Sender is always Object, whereas e is of the same type as the generic type you assigned to Progress. So, in this case it is of type Integer. Here you are working in a Console application and are thus displaying the value as a text message. But in real-world applications, this is the value that you could assign to a ProgressBar control to report the progress in the user interface. The instance of the Progress class must be passed to the asynchronous method that performs the required tasks. The Progress class has just one method called Report; you invoke it after an Await invocation. The following code demonstrates how to report the progress of downloading a number of feeds:

Private Async Sub DownloadAllFeedsAsync(currentProgress As IProgress(Of Integer))
    Dim client As New System.Net.WebClient

    Dim feeds As New List(Of Uri) From
        {New Uri("http://channel9.msdn.com/Tags/windows+8/RSS"),
         New Uri("http://channel9.msdn.com/Tags/windows+phone"),
         New Uri("http://channel9.msdn.com/Tags/visual+basic/RSS")}
    For Each URL In feeds
        Await client.DownloadStringTaskAsync(URL)
        counter += 1
        If currentProgress IsNot Nothing Then currentProgress.Report(counter)
    Next
End Sub

Report receives as an argument an object of the same type that you assigned as the generic argument of the Progress class declaration in this case a counter of type Integer that is incremented every time a feed is downloaded. If you run this code, every time a feed is returned, the progress is also shown in the user interface, as demonstrated in Figure 42.6.

FIGURE 42.6

FIGURE 42.6 Reporting the progress of an asynchronous method.

This pattern makes reporting the progress of a task and in real-world applications, such as WPF and Windows apps, easy. It also makes updating controls like the ProgressBar incredibly simple by assigning to such controls the value stored in the instance of the Progress class.

  • + Share This
  • 🔖 Save To Your Account