1. Problem
You need to execute work in the background that provides updates on progress so that the UI can be responsive.
2. Solution
Use a background worker thread to execute work in the background.
3. How It Works
Silverlight 4 includes the System.Threading.Thread and System.Threading.ThreadPool classes as part of the .NET Framework for Silverlight. However, we recommend that you instead use the System.ComponentModel.BackgroundWorker
class to execute work in the background of the UI, such as loading or
saving data to isolated storage, accessing a remote service, etc. The BackgroundWorker
class provides a nice abstraction layer over the gory details of safely
synchronizing with the UI thread when using one of the lower-level
classes like Thread and ThreadPool.
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, call the ReportProgress method to pass a percentage complete value that is between 0 and 100, which 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.
Check the CancellationPending property of the BackgroundWorker object to determine if there is a pending request to cancel the background operation within the worker_DoWork member function. If CancellationPending is true, set BackgroundWorker.Cancel to true, and stop the operation. To pass data back to the calling process upon completion, set the Result property of the DoWorkerEventArgs object that is passed into the event handler to the object or collection containing the data. 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.
The BackgroundWorker
class tries to prevent deadlocks or cross-thread invocations that could
be unsafe. There are some calls that are always assumed to be called on
the UI thread, such as calling into the HTML Document Object Model
(DOM) or a JavaScript function, so you are not allowed to call them
from a BackgroundWorker class.
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 browser to hang.
It is easy to create a deadlock with two threads accessing the same
resources in an application. Silverlight includes locking primitives,
such as Montior or lock, as well as the ManualResetEvent class.
Exceptions 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.
2.8.4. The Code
In the sample code, you start with the code from Recipe 3,
which includes a form that saves and loads data from isolated storage.
You will save and load data from isolated storage while the background
worker thread is executing to prove that the UI is not locked up by the
long-running operation. You'll modify the UI to include a button to
start the long-running operation as well as a bit of UI work to show
what is going on. Figure 1 shows the UI.
To help keep things clean, the code that was copied from Recipe 2-3 is located in #region
blocks so that it is not a distraction. There is a bit more code in
this recipe, so let's walk through the major code sections. First, you
declare a BackGroundWorker object named worker and initialize it in the constructor Page() for the Page class:
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.ProgressChanged +=
new ProgressChangedEventHandler(worker_ProgressChanged);
worker.RunWorkerCompleted += new
RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
You configure the BackgroundWorker to support cancellation and progress reporting so that you can provide a simple UI to give status. Next, you wire up the DoWork, ProgressChanged, and RunWorkerCompleted events to handlers.
The DoWork event contains the code that the BackgroundWorker thread executes. This is where the long-running operation goes. ProgressChanged and RunWorkerCompleted are events where the UI thread can update status in the UI while the background work is safely executing.
In your DoWork event, you first check to see if there is a cancel request pending and break out of the loop if there is. Otherwise, you call Thread.Sleep to delay execution and ReportProgress
to provide an updated percentage complete. The results of the
background worker thread's effort are passed back to the main thread as
the value of e.Result:
e.Result = Environment.NewLine + "Completed: " + DateTime.Now.ToString();
In your case, you simply pass
back a string, but in a scenario with real background work, this could
be a collection of data or objects received over the network. It is not
safe to update the UI from DoWork, so that is why you must pass back results via the events.
To get the work started from the UI, you have a Kick Off Work button that has an event handler with the name DoWorkButton_Click. The code checks to see if the worker is already busy. If not, you set the status by adding text to the WorkResultsTextData TextBox to indicate that work has started, and you call worker.RunWorkerAsync to kick off the work.
To display a dynamic status in the UI, you have a simple ellipse with a Storyboard named AnimateStatusEllipse.
In the button event handler, you call Begin on this object and set it
to run continuously. The animation changes the color from green to
yellow and then back to green, over and over, to indicate that work is
in progress.
In the worker_ProgressChanged event handler, the UI thread receives the latest status from the background worker, available in the e.ProgressPercentage value. It is safe to update the UI in this method, so you set the tooltip on the status ellipse with the latest value.
The worker_RunWorkerCompleted
event fires when the work successfully completes as well as when the
background worker is cancelled by the UI thread, so you first check to
see if e.Cancelled is not true.
If the work successfully completes, you set the ellipse to green,
update the tooltip to indicate that it is complete, and take the value
passed in as e.Result and add it to the TextBox.Text value.
When the user clicks the
ellipse, a dialog is displayed with two buttons so that the user can
click Yes to cancel or decide not to cancel, as shown in Figure 2.
The StatusEllipse_MouseLeftButtonDown event checks to see if the background worker thread is actually running and then sets PromptCancelCanvas.Visibility to Visibility.Visible.
That displays the dialog that simply consists of a large rectangle with
a transparent look and a rounded white rectangle with the two buttons.
Clicking Yes fires the ButtonConfirmCancelYes_Click event handler that calls the worker.CancelAsync method.
That completes the walkthrough of the code. Most of the other UI code is generated using Expression Blend. We recommend playing with the UI a bit to understand what it does and then reviewing the corresponding code. Listings 2-13 and 2-14 list the code for this recipe's test application. We don't show the keyframe animation in the AnimateStatusEllipse to make Listing 2-14 easier to navigate.
Listing 1. Recipe 7's MainPage.xaml File
<UserControl x:Class="Ch02_ProgrammingModel.Recipe2_7.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"> <UserControl.Resources> <Storyboard x:Name="AnimateStatusEllipse"> .... </Storyboard> </UserControl.Resources> <Grid x:Name="LayoutRoot" Background="#FFFFFFFF"> <Grid.ColumnDefinitions> <ColumnDefinition Width="0.068*"/> <ColumnDefinition Width="0.438*"/> <ColumnDefinition Width="0.495*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="0.08*"/> <RowDefinition Height="0.217*"/> <RowDefinition Height="0.61*"/> <RowDefinition Height="0.093*"/> </Grid.RowDefinitions> <Button HorizontalAlignment="Stretch" Margin="5,8,5,8" VerticalAlignment="Stretch" Grid.Column="1" Grid.Row="1" Content="Save Form Data" Click="SaveFormData_Click"/> <StackPanel HorizontalAlignment="Stretch" Margin="5,8,6,8" Grid.Column="1" Grid.Row="2"> <TextBlock Height="Auto" Width="Auto" Text="Work Results Appear Below" TextWrapping="Wrap" Margin="4,4,4,4"/> <TextBox Height="103" Width="Auto" Text="" TextWrapping="Wrap" Margin="4,4,4,4" x:Name="WorkResultsTextData"/>
</StackPanel> <Button HorizontalAlignment="Stretch" Margin="12,8,8,8" VerticalAlignment="Stretch" Grid.Column="2" Grid.Row="1" Content="Load Form Data" Click="ReadFormData_Click"/> <Button HorizontalAlignment="Stretch" Margin="10,2,8,6" VerticalAlignment="Stretch" Grid.Column="1" Grid.Row="3" Content="Kick Off Work" x:Name="DoWorkButton" Click="DoWorkButton_Click"/> <Border Grid.Column="2" Grid.Row="2" Grid.RowSpan="2" CornerRadius="10,10,10,10" Margin="1.80200004577637,2,2,2"> <Border.Background> <LinearGradientBrush EndPoint="0.560000002384186,0.00300000002607703" StartPoint="0.439999997615814,0.996999979019165"> <GradientStop Color="#FF586C57"/> <GradientStop Color="#FFA3BDA3" Offset="0.536"/> <GradientStop Color="#FF586C57" Offset="0.968999981880188"/> </LinearGradientBrush> </Border.Background> <StackPanel Margin="4,4,4,4" x:Name="FormData"> <TextBlock Height="Auto" Width="Auto" Text="First Name:" TextWrapping="Wrap" Margin="2,2,2,0"/> <TextBox Height="Auto" Width="Auto" Text="" TextWrapping="Wrap" x: Name="Field1" Margin="2,0,2,4"/> <TextBlock Height="Auto" Width="Auto" Text="Last Name:" TextWrapping="Wrap" Margin="2,4,2,0"/> <TextBox Height="Auto" x:Name="Field2" Width="Auto" Text="" TextWrapping="Wrap" Margin="2,0,2,4"/> <TextBlock Height="Auto" Width="Auto" Text="Company:" TextWrapping="Wrap" Margin="2,4,2,0"/> <TextBox Height="Auto" x:Name="Field3" Width="Auto" Text="" TextWrapping="Wrap" Margin="2,0,2,2"/> <TextBlock Height="22.537" Width="182" Text="Title:" TextWrapping="Wrap" Margin="2,4,2,0"/> <TextBox Height="20.772" x:Name="Field4" Width="182" Text="" TextWrapping="Wrap" Margin="2,0,2,2"/> </StackPanel> </Border> <Ellipse x:Name="StatusEllipse" Margin="4,2,2,2" Grid.Row="3" Stroke="#FF000000" Fill="#FF2D4DE0" MouseLeftButtonDown="StatusEllipse_MouseLeftButtonDown" RenderTransformOrigin="0.5,0.5" > <Ellipse.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/>
<RotateTransform/> <TranslateTransform/> </TransformGroup> </Ellipse.RenderTransform> <ToolTipService.ToolTip> <ToolTip Content="Click button to start work." /> </ToolTipService.ToolTip> </Ellipse> <Canvas HorizontalAlignment="Stretch" Margin="0,0,2,8" Grid.RowSpan="4" Grid.ColumnSpan="3" x:Name="PromptCancelCanvas" Visibility="Collapsed"> <Rectangle Height="300" Width="400" Fill="#FF808080" Stroke="#FF000000" Stretch="Fill" Opacity="0.6"/> <Canvas Height="106" Width="289" Canvas.Left="46" Canvas.Top="85"> <Rectangle Height="106" Width="289" Fill="#FFFFFFFF" Stroke="#FF000000" RadiusX="23" RadiusY="23" Opacity="0.85"/> <Button Height="34" x:Name="ButtonConfirmCancelYes" Width="100" Canvas.Left="15" Canvas.Top="49" Content="Yes" Click="ButtonConfirmCancelYes_Click"/> <Button Height="34" x:Name="ButtonConfirmCancelNo" Width="100" Canvas.Left="164" Canvas.Top="49" Content="No" Click= "ButtonConfirmCancelNo_Click"/> <TextBlock Width="134.835" Canvas.Left="75" Canvas.Top="12.463" Text="Cancel Operation?" TextWrapping="Wrap"/> </Canvas> </Canvas> <TextBlock Margin="67.8270034790039,0,−88.802001953125,0" Grid.Column="1" Grid.ColumnSpan="1" Text="BackgroundWorker Thread" TextWrapping="Wrap"/> </Grid> </UserControl>
|
Listing 2. Recipe 7's MainPage.xam.cs File
using System; using System.ComponentModel; using System.IO; using System.IO.IsolatedStorage; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation;
namespace Ch02_ProgrammingModel.Recipe2_7
{ public partial class MainPage : UserControl { private int WorkLoops=30; private BackgroundWorker worker = new BackgroundWorker(); #region Recipe 2-3 Declarations private IsolatedStorageSettings settings = IsolatedStorageSettings.ApplicationSettings; private string FormDataFileName = "FormFields.data"; private string FormDataDirectory = "FormData"; #endregion public MainPage() { 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); }
void worker_DoWork(object sender, DoWorkEventArgs e) { for (int i = 1; i <= WorkLoops; i++) { //Check to see if the work has been canceled if ((worker.CancellationPending == true)) { e.Cancel = true; break; } else { // Perform a time consuming operation and report progress. System.Threading.Thread.Sleep(1000); worker.ReportProgress((int) System.Math.Floor((double)i / (double)WorkLoops * 100.0)); } } e.Result = Environment.NewLine + "Completed: " + DateTime.Now.ToString(); }
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { AnimateStatusEllipse.Stop(); if (!e.Cancelled) { StatusEllipse.Fill = new SolidColorBrush(Color.FromArgb(255, 0, 255, 0)); WorkResultsTextData.Text = WorkResultsTextData.Text + e.Result.ToString(); ToolTipService.SetToolTip(StatusEllipse, "Work Complete."); } else { StatusEllipse.Fill = new SolidColorBrush(Color.FromArgb(255, 255, 255, 0)); WorkResultsTextData.Text = WorkResultsTextData.Text + Environment.NewLine + "Canceled @: " + DateTime.Now.ToString(); ToolTipService.SetToolTip(StatusEllipse, "Operation canceled by user."); }
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e) { if (PromptCancelCanvas.Visibility == Visibility.Collapsed) ToolTipService.SetToolTip(StatusEllipse, e.ProgressPercentage.ToString() + "% Complete. Click to cancel..."); }
private void DoWorkButton_Click(object sender, RoutedEventArgs e) { if (worker.IsBusy != true) { WorkResultsTextData.Text = "Started: "+DateTime.Now.ToString(); worker.RunWorkerAsync(WorkResultsTextData.Text); AnimateStatusEllipse.RepeatBehavior = RepeatBehavior.Forever; AnimateStatusEllipse.Begin(); } }
private void StatusEllipse_MouseLeftButtonDown (object sender, MouseButtonEventArgs e) { if (worker.IsBusy) PromptCancelCanvas.Visibility = Visibility.Visible; }
private void ButtonConfirmCancelYes_Click(object sender, RoutedEventArgs e)
{ worker.CancelAsync(); PromptCancelCanvas.Visibility = Visibility.Collapsed; }
private void ButtonConfirmCancelNo_Click(object sender, RoutedEventArgs e) { PromptCancelCanvas.Visibility = Visibility.Collapsed; } #region Recipe 2-3 Event Handlers private void SaveFormData_Click(object sender, RoutedEventArgs e) { try { using (var store = IsolatedStorageFile.GetUserStoreForApplication()) { //Use to control loop for finding correct number of textboxes int TotalFields = 4; StringBuilder formData = new StringBuilder(50); for (int i = 1; i <= TotalFields; i++) { TextBox tb = FindName("Field" + i.ToString()) as TextBox; if (tb != null) formData.Append(tb.Text); //If on last TextBox value, don't add "|" character to end of data if (i != TotalFields) formData.Append("|"); } store.CreateDirectory(FormDataDirectory); IsolatedStorageFileStream fileHandle = store.CreateFile(System.IO.Path. Combine(FormDataDirectory, FormDataFileName));
using (StreamWriter sw = new StreamWriter(fileHandle)) { sw.WriteLine(formData); sw.Flush(); sw.Close(); } } } catch (IsolatedStorageException ex) { WorkResultsTextData.Text = "Error saving data: " + ex.Message; } }
private void ReadFormData_Click(object sender, RoutedEventArgs e) { using (var store = IsolatedStorageFile.GetUserStoreForApplication()) { //Load form data using private string values for directory and filename string filePath = System.IO.Path.Combine(FormDataDirectory, FormDataFileName); //Check to see if file exists before proceeding if (store.FileExists(filePath)) { using (StreamReader sr = new StreamReader( store.OpenFile(filePath, FileMode.Open, FileAccess.Read))) { string formData = sr.ReadLine(); //Split string based on separator used in SaveFormData method string[] fieldValues = formData.Split('|'); for (int i = 1; i <= fieldValues.Count(); i++) { //Use the FindName method to loop through TextBoxes TextBox tb = FindName("Field" + i.ToString()) as TextBox; if (tb != null) tb.Text = fieldValues[i − 1]; } sr.Close(); } } } } #endregion } }
|