1. Problem
You need to update the UI from a background thread so that the UI can be responsive.
2. Solution
The Dispatcher class
offers a safe way to call a method that updates the UI asynchronously
from a background thread by providing services for managing the queue of
work items for a thread. Both the Dispatcher and the BackgroundWorker classes can perform work on a separate thread. The BackgroundWorker class supports progress reporting and cancellation. The Dispatcher class is useful when you need a simple way to queue up background work without progress reporting or cancellation.
3. How It Works
The .NET Framework for Silverlight includes the System.Threading
namespace, which contains classes needed to manage a thread pool,
launch threads, and synchronize threads, just like the full version of
the .NET Framework.
As with most UI programming
models such as Visual Basic 6, .NET Windows Forms, or WPF, it is not
safe to access UI objects from a background thread. UI objects, such as Button, TextBox, and TextBlock objects, can only be safely accessed on the UI thread.
The role of the Dispatcher
is to provide a way for a background thread to invoke a method that
runs on the main thread so that it can safely update the UI. This
approach is useful when you're retrieving data from the server using the
asynchronous WebRequest class, as demonstrated in this recipe. Figure 1 shows the UI for the application after the data is downloaded.
4. The Code
The sample application for this recipe contains a button titled Retrieve XML and Load that when clicked fires the event RetrieveXMLandLoad_Click. This event creates an HttpWebRequest object that points to the location where Recipe 2-5's ApressBooks.xml file was copied:
Uri location =
new Uri("http://localhost:9090/xml/ApressBooks.xml",UriKind.Absolute);
WebRequest request = HttpWebRequest.Create(location);
request.BeginGetResponse(
new AsyncCallback(this.RetrieveXmlCompleted), request);
When the asynchronous web request completes, the code in the callback method RetrieveXmlCompleted executes. The following code retrieves the XML document from the response stream and stores it in an XDocument object:
HttpWebRequest request = ar.AsyncState as HttpWebRequest;
WebResponse response = request.EndGetResponse(ar);
Stream responseStream = response.GetResponseStream();
using (StreamReader streamreader = new StreamReader(responseStream))
{
XDocument xDoc = XDocument.Load(streamreader);
...
The rest of the code in the callback method RetrieveXmlCompleted executes the same LINQ to XML as in Recipe 5 to obtain a List of ApressBook objects. The last line of code calls the Dispatcher object to queue UI work by calling BeginInvoke to execute the delegate and passing in the method DataBindListBox on the UI thread passing in the List of ApressBook objects:
Dispatcher.BeginInvoke(() => DataBindListBox(_apressBookList));
The syntax looks a bit strange if
you are not familiar with C# lambda expressions. The syntax is
shorthand for creating a delegate object and mashing the parameters into
the call. The method DataBindListBox has a single line of code to assign the ItemsSource property on the BooksListBox object:
BooksListBox.ItemsSource = list;
If you skip using the Dispatcher in the callback method RetrieveXmlCompleted for the HttpWebRequest and instead put the line of code to assign the ItemsSource
property in the callback method directly, the UI will not be updated
because the callback method returns on the background thread of the HttpWebRequest, not the UI thread. By calling Dispatcher.BeginInvoke to update the UI from the HttpWebRequest callback background thread, you queue the work to assign the List object to the ItemsSource so that it safely executes when the main UI thread literally has cycles available. Listings 1 and 2 show the source code for this recipe's test application.
Listing 1. Recipe 8's MainPage.xaml File
<UserControl x:Class="Ch02_ProgrammingModel.Recipe2_8.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400"> <Grid x:Name="LayoutRoot" Background="White" Margin="6,6,6,6"> <StackPanel> <Button Content="Retrieve XML and Load" Click="RetrieveXMLandLoad_Click"></Button> <ListBox x:Name="BooksListBox" Margin="4,4,4,4" Height="452" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel Margin="2,2,2,2"> <TextBlock Text="{Binding Path=ISBN}" Margin="0,0,0,2"/> <TextBlock Text="{Binding Path=Title}" Margin="0,0,0,2"/> <TextBlock Width="550" Text="{Binding Path=Description}" TextWrapping="Wrap" Margin="0,0,0,10"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel> </Grid> </UserControl>
|
Listing 2. Recipe 8's MainPage.xaml.cs File
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Xml.Linq;
namespace Ch02_ProgrammingModel.Recipe2_8
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
}
private void RetrieveXMLandLoad_Click(object sender, RoutedEventArgs e)
{
Uri location =
new Uri("http://localhost:9090/xml/ApressBooks.xml", UriKind.Absolute);
WebRequest request = HttpWebRequest.Create(location);
request.BeginGetResponse(
new AsyncCallback(this.RetrieveXmlCompleted), request);
}
void RetrieveXmlCompleted(IAsyncResult ar)
{
List<ApressBook> _apressBookList;
HttpWebRequest request = ar.AsyncState as HttpWebRequest;
WebResponse response = request.EndGetResponse(ar);
Stream responseStream = response.GetResponseStream();
using (StreamReader streamreader = new StreamReader(responseStream))
{
XDocument xDoc = XDocument.Load(streamreader);
_apressBookList =
(from b in xDoc.Descendants("ApressBook")
select new ApressBook()
{
Author = b.Element("Author").Value,
Title = b.Element("Title").Value,
ISBN = b.Element("ISBN").Value,
Description = b.Element("Description").Value,
PublishedDate = Convert.ToDateTime(b.Element("DatePublished").Value),
NumberOfPages = b.Element("NumPages").Value,
Price = b.Element("Price").Value,
ID = b.Element("ID").Value
}).ToList();
}
//Could use Anonymous delegate (does same as below line of code)
//Dispatcher.BeginInvoke(
// delegate()
// {
// DataBindListBox(_apressBookList);
// }
// );
//Use C# 3.0 Lambda
Dispatcher.BeginInvoke(() => DataBindListBox(_apressBookList));
}
void DataBindListBox(List<ApressBook> list)
{
BooksListBox.ItemsSource = list;
}
}
public class ApressBook
{
public string Author { get; set; }
public string Title { get; set; }
public string ISBN { get; set; }
public string Description { get; set; }
public DateTime PublishedDate { get; set; }
public string NumberOfPages { get; set; }
public string Price { get; set; }
public string ID { get; set; }
}
}