Exercise 2: Concurrent Download

In this exercise, you will enhance the solution from Exercise 1 by enabling users to interact with the application while the contents are being downloaded. You will learn how to use the Task object in order to prevent blocking the user interface (UI).

After completing the exercise, the application will remain responsive while it downloads the full web page.

Task 1 – Adding Concurrent Download in a Separate Method

  1. Open Visual Studio 11 and load AsyncLab-Ex2-Begin.sln solution located in the Source\[CS|VB]\Ex2-Concurrency\Begin folder of this lab. You can also continue working with the solution you’ve obtained after completing Exercise 1.
  2. Open MainWindow.xaml.cs or MainWindow.xaml.vb code-behind and import the System.Collection.ObjectModel namespace.

    C#

    using System.Collections.ObjectModel;

    Visual Basic

    Imports System.Collections.ObjectModel

  3. In startButton_Click event handler, replace the uris local variable type with an ObservableCollection type.

    C#

    private async void startButton_Click(object sender, RoutedEventArgs e)
    FakePre-d8c173741ffc4cfc8abda3f32bd8a56e-132622f77d074595895302cc4bb7becdFakePre-a5f86588f52446c0a417e0a26b442b24-004a098295bf450daf2c2ca1839237dc ObservableCollection<string> uris = new ObservableCollection<string>();FakePre-f35d5f6381ce4733a872e01c1692cff0-cf4859c628a14ff9a87b75f21cd24586FakePre-d879bf70219b41859a6a54c8f330b201-ab330e171b7247d2ba5f8d03be643689FakePre-d37af3c9c7da45a48ccef1f0ba41a6f0-94b214586ba74d91bfb0a4c32d3b838eFakePre-7315bac8ecdd44a3a26aeed1b5143ca3-52f09ca3884c4edabe98c57e2d3f7f3eFakePre-8afd3e36b31f4a94b2abc65ee9590598-8d8cb9f1f67c429fa27116e3970dd5ae

    Visual Basic

    Private Async Sub startButton_Click(sender As System.Object, 
    FakePre-9f57f41f038c43e4814668a5d095ae58-7065eafba23d4541a435530305ab8af6FakePre-7a9845ab537c4eb0a8a2f000cf246b7b-c27744d8cff64c1ab23b0f6c061163ddFakePre-869c6cafa8014198abdfb33e3dfad6be-294b79615d38404e9d4784a470bc655f Dim uris As New ObservableCollection(Of String)()FakePre-690f7322a6ca4f2dace5d8991d487b2e-91462f773a254f3da98efa0131a586c1

  4. Send the URI processing to the background in order to improve the application responsiveness. Enclose the filter and list manipulation logic into a lambda expression, which will be executed in background thread when calling Task.Run(). This task will be completed asynchronously, as it includes the await keyword.

    (Code Snippet – Async Lab - Ex02 - AwaitBlock - CS)

    C#

    private async void startButton_Click(object sender, RoutedEventArgs e)
    FakePre-b098cf30961d49409dade5783a219120-d10f0ad5b34449208ecfd5ed78e43b3fFakePre-54d55e3d048d442b821de51bde023155-488fa2bd0cef42adb3bc2545e28c12ceFakePre-f92a1a6ebaf744beb93790ae07f4e966-d6c75916fb094c67b168cc4aadd121f0FakePre-d4e9c73d9e7a456ea8c9586f317f94fe-2ccc2b169cc94ea9a41e17df40c54365FakePre-d4cb22b6a2e8430d9ab4bd2200c7b56f-e1c68142763e449baf15cea086a150d3FakePre-fbd19aed090d403ba997e86162d84e76-b9622ca019954aa59fb72518c6f2c02bFakePre-65509761826c429c8c3f18b412157ea3-d8378cdb09734d659446d4fff07de1d6 await Task.Run(() => { MatchCollection mc = Regex.Matches( result, "href\\s*=\\s*(?:\"(?<1>https://[^\"]*)\")", RegexOptions.IgnoreCase); foreach (Match m in mc) { uris.Add(m.Groups[1].Value); } });

    (Code Snippet – Async Lab - Ex02 - AwaitBlock - VB)

    Visual Basic

    Private Async Sub startButton_Click(sender As System.Object, 
    FakePre-1cdd9eb7598942c89449ad6a005c2928-ccd46d23c0d844dd83bdfa53a60d1fb0FakePre-85e8288d27ae4bef82afe0c6c1fb5a96-53db5d88d684449a872fb3932133c4b6FakePre-b8dbcdd84252473489ad3bbdb9024e1b-e05b84bf36694ee2a36103d6b673022cFakePre-286f101a6ef944999408682fe26f6cb0-2f62f73ffe8e40ae85096e93397aae32FakePre-0e437dc23cc84ee98b1b0132efc12580-61b6f2c14f45415184982dd83eff31bd Await Task.Run(Sub() Dim mc As MatchCollection = Regex.Matches(result, "href\s*=\s*(?:\""(?<1>https://[^""]*)\"")", RegexOptions.IgnoreCase) For Each m As Match In mc uris.Add(m.Groups(1).Value) Next End Sub)

    Note:
     By using Task.Run, you are explicitly running your logic on a different thread from the thread pool.

  5. Create a new class in the root of AsyncLab project and name it LinkInfo. This class will contain information about the links of the page. Insert the following code in the LinkInfo class.

    (Code Snippet – Async Lab - Ex02 - LinkInfo - CS)

    C#

    namespace AsyncLab
    FakePre-711b4a84b6be4289b0be3113ff2d8061-2d07288c66054e9caa3d3a0bf15937ea public class LinkInfo { public string Title { get; set; } public string Html { get; set; } public int Length { get; set; } }FakePre-874d9233731146eabe00bdb94b554d8f-35c418ba903746acaf36a0fd5e2143fbFakePre-167a3ace77d1463f8f831888aa856334-6c70e9086d394b66b7d0ab336447b33bFakePre-b47975963beb4f1a8aa4f69453de438c-4b5a47eb2fa244bcaea49536aa9bc5e9

    (Code Snippet – Async Lab - Ex02 - LinkInfo - VB)

    Visual Basic

    Public Class LinkInfo Public Property Title() As String Public Property Html() As String Public Property Length As Integer End Class
    FakePre-8612c7a5456742f6a063492c19055e5d-5bf6adb8fa0e4472a4db638d837f938d
    

  6. Create a new method to download a page in the MainWindow.xaml.cs or MainWindow.csml.vb code-behind class. Name this method DownloadItemAsync and declare it with the Async prefix. This method will return an empty string in case the HTTP GET operation cannot be resolved.

    (Code Snippet – Async Lab - Ex02 - DownloadItemAsync - CS)

    C#

    private static async Task<LinkInfo> DownloadItemAsync(Uri itemUri) { string item; try { HttpClient httpClient = new HttpClient(); httpClient.MaxResponseContentBufferSize = 1000000; var response = await httpClient.GetAsync(itemUri); item = await response.Content.ReadAsStringAsync(); } catch { item = string.Empty; } LinkInfo linkInfo = new LinkInfo { Length = item.Length, Title = GetTitle(item), Html = item }; return linkInfo; }
    FakePre-772bfa2da10348c58062e421ab95c0fb-f169874984204cf19bc486bafcc93b67
    

    (Code Snippet – Async Lab - Ex02 - DownloadItemAsync - VB)

    Visual Basic

    Private Shared Async Function DownloadItemAsync( ByVal itemUri As Uri) As Task(Of LinkInfo) Dim item As String Try Dim httpClient As New HttpClient httpClient.MaxResponseContentBufferSize = 1000000 Dim response = Await httpClient.GetAsync(itemUri) item = Await response.Content.ReadAsStringAsync() Catch item = String.Empty End Try Dim linkInfo As LinkInfo = New LinkInfo() With { .Length = item.Length, .Html = item, .Title = GetTitle(item)} Return linkInfo End Function
    FakePre-5cd45e6cd49c4bdb98dacc45a73dd569-91c6a9210b1049ef918c74feaf69e966FakePre-6bef2063e16d472cbeae189616816d19-c4daa1ffe6fa44f79f3b821689fdf9aaFakePre-14982d9eee1e4f828815a7c9028d9535-a85a3f9c856b4238b42864e60cb6a5ba
    

    Note:
    Notice we are setting an arbitrary HTTP client buffer size, which is higher than the default value (~65Kb). This is because many web messages may exceed this value and generate an application error.

  7. Create a GetTitle method after the DownloadItemAsync method to return the text located inside the title tag.

    (Code Snippet – Async Lab - Ex02 - GetTitle - CS)

    C#

    private static string GetTitle(string html) { if (html.Length == 0) { return "Not Found"; } Match m = Regex.Match(html, @"(?<=<title.*>)([\s\S]*)(?=</title>)", RegexOptions.IgnoreCase); return m.Value; }

    (Code Snippet – Async Lab - Ex02 - GetTitle - VB)

    Visual Basic

    Private Shared Function GetTitle(ByVal html As String) As String If (html.Length.Equals(0)) Then Return "Not Found" End If Dim m As Match = Regex.Match(Html, "(?<=<title.*>)([\s\S]*)(?=</title>)", RegexOptions.IgnoreCase) Return m.Value End Function

  8. Modify the startButton_Click event handler logic to use the DownloadItemAsync method and populate the ListBox with the retrieved items.

    (Code Snippet – Async Lab - Ex02 - DownloadCompleted - CS)

    C#

    private async void startButton_Click(object sender, RoutedEventArgs e)
    FakePre-46469e9ce73c4c5b8d738ef124e25e46-af9613e745e14cfe9326d46741cc3ff5FakePre-9ac660f879e54831942eac1beb2f0641-4aae459beba5418a92c5bd47edd532fcFakePre-87a17723431e463dbaa988a3a59dd35e-59bc1588eead45e9bb925922d57ef41bFakePre-79a5b88416e6416996d3b25988244496-39205f26118f457999a15972b10cedafFakePre-5a01ae16b1284bdf9e494976a1f98d96-0f3abba28ebb41f6bee4e0e747d46b84FakePre-8aa5bf638ae34daab6a0650b431c522d-80331e7058994ee193d3e76d5a94f9b3FakePre-baef0f24955f422b8f3406b280bfa8c6-d46f0df348444797bca562ab4bea9cecFakePre-fc622dc07f9f46e8bbf69edd999aad4a-2a7402421e944779b09bee7770688dd6FakePre-7e5264fa121a45f7a841a6b7da23814e-ccccd167a1674692982ae5314747a26bFakePre-dc8a8912d0064c6ca3ef403bd7956270-6321ca482db3497e8168517d48d99e88FakePre-3cdd662817ae4bee918241d021c1b5de-5afbca77a44049998d38e766f1ffdd5cFakePre-d9bed5a9728943b8b5044240017a69b3-a1f0b666592e48db8eeb9da446a64f98FakePre-9f2dfabe1b804583a696d14bfe6b00d0-158e3abcd8cc45b5b8b5b2435239ffb3FakePre-ec131c383c604f2dbe0d42c46a05dd54-e85f449cba4346d3a3ce776e5da026fdFakePre-4b92980774084fb0bf980729b6326efb-c6457a20df3f480d8efd4cdc5d5c9cbbFakePre-9acb546cdab648d4b11d4c5d60b44e60-2c04f6475ddc4f80a5b3c227b8e30219FakePre-66b4543700f74781a467b9e4c2e115df-3b62ce0ec300415d83cb177e8b61db07 listBox1.ItemsSource = await Task.WhenAll( from uri in uris select DownloadItemAsync(new Uri(uri)));FakePre-dd7c82f3648d425792833dadab3b204c-2435db674ec84090a71241dcb4658dbcFakePre-a7eef729d3ba4cb4b9c0650aedb300f8-d2930f5d992f42d89a3604cf671331eeFakePre-ca68b3e469f5426991b2dc2e2a690678-e4f1df4986be4b59acf883e800c7afadFakePre-65b81ea242994ccc9c7ef156b2b50e74-e215643142934975bf3e063ee4c257a7FakePre-1c9a0309aa004d64b12bf71bc3e08bca-d105d141427d4e619c8dae0398b30b01FakePre-c5df8722d1864ca9ad24a81f5a71de1e-58447318da524329bc62539456007d2c

    (Code Snippet – Async Lab - Ex02 - DownloadCompleted - VB)

    Visual Basic

    Private Async Sub startButton_Click(sender As System.Object, 
    FakePre-bba60ce789204744a9165873c254b38f-c6e349947f4a491f9ef47693a60bddebFakePre-ef5a53002edc4e0492c308f498548b70-f6f71fc987354cce9d68d69b7962aeafFakePre-0994634ac1b14344a9b1c106f3af9635-2f9e43e9a8f34b78aebe2f4e5ad8572fFakePre-a77eef02be7046b4b1f62f5fd54e9352-4bd23c9388934b019c3e4ac8bd57c9d9FakePre-e60493ddffa948be88b1011d7b5da4d8-1fba7c8610e64730926a97f45ee21bc8FakePre-94431a49883e4e6ba871c1f94fa828d3-914637e2742e48b283c648b4d80ede8fFakePre-d56fb71b983845bf825631afe6206a57-d45b7cbd617e4084b203d33746d8ef87FakePre-b8ae4a197e76431bb9ea48da5dd927e7-d43b90dc95c34296a81fd68410be06d4FakePre-fde6bb62bbfd4f7eb788d7c7961cb2d4-3f2079f054d047f8bfa4118db9565147FakePre-d17c6f01b4014ddeae5399314c6d6b17-3fdcceb9d5d24366902382869ad53969FakePre-638abf4d78014e14bb9f54811de6bb65-59dae67b1f2f422caff1f3e74f78db90FakePre-397f3b8ebc6744de83df3481472a674d-67cfeb0075a143f3baa7b49e2715fc18FakePre-91ee54d2520b48eb82c841489265ffc7-5237d77fdc4848ff93e5fc84c6677cd0 listBox1.ItemsSource = Await Task.WhenAll( _ From uri In uris _ Select DownloadItemAsync(New Uri(uri)))FakePre-cd16eb64849b4f038314dc7215e0b6b5-bdff8c1a1f1442bfade2ec0ffa96c7b0FakePre-f43b055ed6e6480880b63870a1a90473-9549650cea524dfdb11505b499001f74FakePre-b39d6d23c36343d4953b630c73044182-429de424e9044bae865b7c32228db20fFakePre-763805feaa394e62964b9905cf4da32d-2a9d9691ff3f48fda2f96f89b6d13c07FakePre-34c4ebbd4c474806bd0343732168fdd7-d4d086bc5c5d4fc5907b154f5fe25d66FakePre-49ca062d0239441383eb58efbfc5b2a1-c50548c294044ee3a0786b36906c74a0

    Note:
     Notice that:

    - When using Task.WhenAll, a task completes when all of the constituent tasks have completed. In this example, the await keyword before Task.WhenAll is forcing it to asynchronously wait for all of the items to complete before continuing the execution of the next instruction.

    - When using Task.WhenAny, the returned task completes when any of the tasks completes.

  9. Open MainWindow.xaml XAML view and insert the following code before the Grid to show the link title and the length of the linked page.

    XAML

    <Window x:Class="MainWindow"
    FakePre-62434bb4d4994a5aa876dc53ea588673-bc8705efd9e84720a6381aa38aaaac18FakePre-c30695ab9a144802980ea4776c47237d-24a91aa058e34fa4b9f2b61560eb6080FakePre-38dac755ec974a4dbcd9ee559fe7293c-d0015a476297495180dc7df52a3d97d6 <Window.Resources> <DataTemplate x:Key="DataTemplateItem"> <StackPanel Orientation="Horizontal"> <TextBlock> <TextBlock.Text> <MultiBinding StringFormat=" {0} - {1} "> <Binding Path="Length"/> <Binding Path="Title"/> </MultiBinding> </TextBlock.Text> </TextBlock> </StackPanel> </DataTemplate> </Window.Resources>FakePre-577c31616b0b4b918c00ad46d05719c7-0fed0d546c894d89ad64e9ef7207a25bFakePre-059ae171099f44489d179c7aea3b2852-7a5c8fba6a4a406d838627900091d8a2

  10. Modify the ListBox to use the data template from the previous step.

    XAML

    <Grid>
    FakePre-dabdb047277a4acb8653c8b27aff2a3c-febe146e78e94312aa5d4f9616f3cfd5FakePre-9f9739427a24484ebce6f3f21b18b034-befe61a253c7487c84efe33204ec770a <ListBox Height="379" HorizontalAlignment="Right" Margin="0,12,12,0" Name="listBox1" VerticalAlignment="Top" Width="294" ItemTemplate="{DynamicResource DataTemplateItem}"/>FakePre-197859df54b44172807a919b6fc12963-13d0546242854b1383e2b3a5303108b9FakePre-e58a4a80903345228cf6fd636a67808b-f0921fc20755499884cdeb22b29b3529FakePre-21f78a73724f490aa4f34fea5972bd78-f3fdb7cc8a7f4a67a0968895a1d435b0FakePre-8d854a62fbc24c5492b3865c8d15309f-75204a153afc41d2b186e86c409f1222

Task 2 – Verification

  1. Press F5 to run the application.
  2. Click the Start button to begin downloading the linked pages. Note that the application is still responsive while the download executes in the background.
  3. After the download completes, the application will show the list of pages with their length and title.

    Figure 2

    Running the application