This section covers asynchronous
programming, which is the preferred method do to work in Silverlight.
Asynchronous programming is preferred, because it takes work off of the
UI thread, which should be a priority in order to maximize UI
performance for animation and transitions.
Rendering performance can be improved two ways: pushing work from the UI thread to the Render
thread and pushing work such as processing remote data to a separate
background thread. We cover various ways to push work off of the UI
thread in this section.
1. Background Threads
You can use the standard .NET multithreading class Thread.Start and ThreadPool.QueueUserWorkItem
to perform background work that writes data to isolated storage, and it
will work fine. If you try to access the XAML from the standard classes
without taking extra steps, it will throw an exception. Silverlight
includes classes that make it easier to perform background work that
interacts with the user interface.
1.1. Fire and Forget Background Processing
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 classsupports progress reporting and cancellation, which we cover in detail in the next section. The Dispatcher class is useful when you need a simple way to queue up background work without progress reporting or cancellation.
You can create a delegate and then user Dispatcher.BeginInvoke to fire the delegate, which then updates the UI. As an example, if you have a TextBlock named TextBlock1 that you need to update from a background thread, obtain the Dispatcher from that control and perform the update. Here is an example of using C# lambda syntax (=>).
TextBlock1.Dispatcher.BeginInvoke(() =>
{
TextBlock1.Text = "Data Updated";
};
You can call Dispatcher.CheckAccess to determine if the calling thread is on the same thread as the control or the UI thread. Use BeginInvoke it if returns false. It is recommended to obtain the Dispatcher
instance from the control closest to the controls being updated. So if
multiple controls need to be updated and they are contained in a Grid panel, obtain the Dispatcher from the Grid.
A
file named ApressBooks.xml is added to the AsynchronousProgramming project. This XML file contains a simple xml schema with a few book titles in it. Here is one record from the XML file:
<ApressBook>
<ID>4</ID>
<ISBN>1-4302-2435-5</ISBN>
<Author>Jit Ghosh and Rob Cameron</Author>
<Title>Silverlight Recipes: A Problem-Solution Approach, Second Edition</Title>
<Description>Silverlight Recipes: A Problem-Solution Approach, Second Edition is your
practical
companion to developing rich, interactive web applications with Microsoft's latest
technology.
</Description>
<DatePublished>2010-07-15T00:00:00</DatePublished>
<NumPages>1056</NumPages>
<Price>$49.99</Price>
</ApressBook>
The UI for the DispatcherPage.xaml contains a ListBox with an ItemTemplate to display the above data and an application bar with one button to load the data. When the button is clicked, the LoadDataAppBarButton_Click event handler spins up a WebRequest object that points to the local developer web server from the WcfRemoteServices Project to retrieve the XML file. Here is the code snippet for the application bar button event handler:
private void LoadDataAppBarButton_Click(object sender, EventArgs 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);
}
All remote service calls MUST be executed asynchronously, so the callback function named RetrieveXmlCompleted is where the results are actually returned to the application. Here is the RetrieveXmlCompleted method:
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)
// BooksListBox.Dispatcher.BeginInvoke(
// delegate()
// {
// DataBindListBox(_apressBookList);
// }
// );
//Use C# 3.0 Lambda
BooksListBox.Dispatcher.BeginInvoke(() => DataBindListBox(_apressBookList));
}
The xml file is received and then loaded into an XDocument object for some basic Linq to XML manipulation to turn it into a collection of APressBook
.NET objects. Once that little bit of work is completed, the collection
needs to be pushed back to the UI thread. This is where the BooksListBox.Dispatcher is finally used to fire the DataBindListBox method to perform the data binding.
The previous code snippet includes an alternative
method of passing the _apressBookList to the UI thread and databind. It
could be reduced further to the following:
BooksListBox.Dispatcher.BeginInvoke(() =>
{
BooksListBox.ItemsSource = _apressBookList;
});
To test the Dispatcher using WebRequest, both the WCFRemoteServices project and the AsynchronousProgramming project must be running.Right-click on the Ch04_WP7ProgrammingModel Solution and configure it to have multiple startup projects, as shown in Figure 1.
If you still want to use standard .NET Framework threading, You can call SynchronizationContext.Current to get the current DispatcherSynchronizationContext, assign it to a member variable on the Page, and call Post(method, data) to fire the event back on the UI thread. Calling Send(method, data) instead of Post will make a synchronous call, which you should avoid doing if possible as it could affect UI performance.
1.2. Supporting Progress Reporting and Cancellation
For long running processes, having the ability to
cancel work as well as show work progress is necessary for a good user
experience. A convenient class that provides a level of abstraction as
well as progress updates is the System.ComponentModel.BackgroundWorker class. The BackgroundWorker
class lets you indicate operation progress, completion, and
cancellation in the Silverlight UI. For example, you can check whether
the background operation is completed or canceled and display a message
to the user.
To use a background worker thread, declare an instance of the BackgroundWorker class at the class level, not within an event handler:
BackgroundWorker bw = new BackgroundWorker();
You can specify whether you want to allow cancellation and progress reporting by setting one or both of the WorkerSupportsCancellation and WorkerReportsProgress properties on the BackgroundWorker object to true. The next step is to create an event handler for the BackgroundWorker.DoWork event. This is where you put the code for the time-consuming operation. Within the DoWork event, check the CancellationPending property to see if the user clicked the Cancel button. You must set e.Cancel = true in DoWork so that WorkCompleted can check the value and finish correctly if the work was completed.
If the operation is not cancelled, call the ReportProgress method to pass a percentage complete value that is between 0 and 100. Doing this raises the ProgressChanged event on the BackgroundWorker object. The UI thread code can subscribe to the event and update the UI based on the progress. If you call the ReportProgress method when WorkerReportsProgress is set to false, an exception will occur. You can also pass in a value for the UserState parameter, which in this case is a string that is used to update the UI.
Once the work is completed successfully, pass the data back to the calling process by setting the e.Result property of the DoWorkerEventArgs object to the object or collection containing the data resulting from the work. The DoWorkerEventArgs.Result is of type object and can therefore be assigned any object or collection of objects. The value of the Result property can be read when the RunWorkerCompleted event is raised upon completion of the operation and the value can be safely assigned to UI object properties. Listing 1 shows the XAML modifications in the ContentPanelGrid.
Example 1. The BackgroundWorkerPage.xamlContentPanel XAML
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel Orientation="Vertical" d:LayoutOverrides="Height">
<StackPanel x:Name="StatusStackPanel" Orientation="Vertical">
<StackPanel Orientation="Horizontal" d:LayoutOverrides="Width">
<TextBlock x:Name="processingStateTextBlock" TextWrapping="Wrap"
VerticalAlignment="Top" Width="190" Margin="12,34,0,0"/>
<Button x:Name="cancelButton" Content="Cancel Operation"
VerticalAlignment="Top" Click="cancelButton_Click" Width="254" />
</StackPanel>
<ProgressBar x:Name="BookListDownloadProgress" Width="456"
HorizontalAlignment="Left" />
</StackPanel>
<ListBox x:Name="BooksListBox" ItemsSource="{Binding ApressBookList}"
Height="523" ItemTemplate="{StaticResource BookListBoxDataTemplate}" />
</StackPanel>
</Grid>
|
The StatusStackPanel container that has the status info is made visible when the work is started, and is then hidden since the work is completed. Figure 2 has the UI.
One additional wrinkle is that the code overrides OnNavigateFrom. If the BackgroundWorker thread is busy, the code cancels the operation, since the user navigated away. Listing 2 has the full source code.
Example 2. The BackgroundWorkerPage.xaml.cs Code File
using System.ComponentModel;
using System.Windows;
using Microsoft.Phone.Controls;
namespace AsynchronousProgramming.pages
{
public partial class BackgroundWorkerPage : PhoneApplicationPage
{
private BackgroundWorker _worker = new BackgroundWorker();
public BackgroundWorkerPage()
{
InitializeComponent();
//Configure BackgroundWorker thread
_worker.WorkerReportsProgress = true;
_worker.WorkerSupportsCancellation = true;
_worker.DoWork +=
new DoWorkEventHandler(worker_DoWork);
_worker.ProgressChanged +=
new ProgressChangedEventHandler(worker_ProgressChanged);
_worker.RunWorkerCompleted +=
new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
//Kick off long running process
//Make status visible
_worker.RunWorkerAsync();
StatusStackPanel.Visibility = Visibility.Visible;
}
protected override void OnNavigatedFrom(
System.Windows.Navigation.NavigationEventArgs e)
{
//Cancel work if user navigates away
if (_worker.IsBusy)
_worker.CancelAsync();
base.OnNavigatedFrom(e);
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
ApressBooks books = new ApressBooks();
books.LoadBooks();
int progress;
string state = "initializing...";
//Do fake work to retrieve and process books
for (int i = 1; i <= books.ApressBookList.Count;i++ )
{
if (_worker.CancellationPending == true)
{
e.Cancel = true;
break;
}
else
{
progress = (int)System.Math.Round((double)i /
books.ApressBookList.Count * 100d);
if ((progress > 15) && (progress < 90))
state = "processing..." ;
if (progress > 85)
state = "finishing..." ;
if (progress == 95)
state = "Loading complete.";
_worker.ReportProgress(progress, state);
System.Threading.Thread.Sleep(250);
}
}
e.Result = books;
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
BookListDownloadProgress.Value = e.ProgressPercentage;
processingStateTextBlock.Text = e.UserState as string;
}
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled == true)
MessageBox.Show("Operation cancelled.","Cancelled",MessageBoxButton.OK);
else
LayoutRoot.DataContext = e.Result as ApressBooks;
//Clean up status UI
BookListDownloadProgress.Value = 0;
processingStateTextBlock.Text = "";
StatusStackPanel.Visibility = Visibility.Collapsed;
}
private void cancelButton_Click(object sender, RoutedEventArgs e)
{
_worker.CancelAsync();
}
}
}
|
Silverlight includes the standard .NET locking primitives, such as Monitor or lock, as well as the ManualResetEvent
class where deadlocks can occur. A deadlock occurs when two threads
each hold on to a resource while requesting the resource that the other
thread is holding. A deadlock will cause the application to hang. It is
easy to create a deadlock with two threads accessing the same resources
in an application.
The BackgroundWorker class tries to prevent deadlocks or cross-thread invocations that could be unsafe.
Any exceptions that can occur must be caught
within the background thread, because they will not be caught by the
unhandled exception handler at the application level. If an exception
occurs on the background thread, one option is to catch the exception
and set Result to null as a signal that there was an error. Another option is to set a particular value to Result as a signal that a failure occurred.