- Overview of Asynchrony
- The Old-Fashioned Way: Event-Based Asynchrony
- The Old-Fashioned Way: The Asynchronous Programming Model
- The Modern Way: The Async Pattern
- Getting Started with Async/ Await
- Exception Handling in Async
- Implementing Task-Based Asynchrony
- Cancellation and Progress
- Asynchronous Lambda Expressions
- Asynchronous I/O File Operations in .NET 4.6
- Debugging Tasks
- Summary
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 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.