MULTIMEDIA

Executing Work on a Background Thread with Updates

9/15/2010 11:41:11 AM
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.

Figure 1. Recipe 7s test 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.

Figure 2. The cancel operation dialog, where the user makes the choice.

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
}
}


Other  
 
Top 10
Next – Gen Broadband – Optimizing Your Current Broadband Connection (Part 4)
Next – Gen Broadband – Optimizing Your Current Broadband Connection (Part 3)
Next–Gen Broadband – Optimizing Your Current Broadband Connection (Part 2)
Next–Gen Broadband – Optimizing Your Current Broadband Connection (Part 1)
Side Channel Attacks Explained
Canon EOS M With Wonderful Touchscreen Interface (Part 3)
Canon EOS M With Wonderful Touchscreen Interface (Part 2)
Canon EOS M With Wonderful Touchscreen Interface (Part 1)
Canon Powershot G15 With Immaculate Photos And Superb Controls
Fujifilm XF1 - Compact Camera With Retro Design
Most View
Advice Centre by Photography Experts (Part 1) - Nikon D300
Toshiba Stor.E Steel S Titanium 500GB
Useful apps for iPad (Part 1) : Bento for iPad & Numbers 1.5 for iPad
MasterClass: How To Automate Your Life (Part 1)
Blind SQL Injection Exploitation : Using Time-Based Techniques
New Restrictions On Old Office Software (Part 1)
iOS Tips (Part 1) - Proof of industry shift
Jexaa Jex Tab Evo: Slow and Bad
Windows Vista : Recovering Systems (part 1) - Dealing with system instability
Combine Multiple Events into a Single Event
Windows Server 2003 : Clustering Technologies - Command-Line Utilities
System Center Configuration Manager 2007 : Developing the Solution Architecture (part 3) - Developing the Server Architecture
Nokia 808 Pureview - Best For Convergists
Next – Gen Broadband – Optimizing Your Current Broadband Connection (Part 4)
100 Ways To Speed Up Windows (Part 1)
Secure Your Smartphone (Part 1)
IIS 7.0 : Managing Administration Extensions
Programming .NET Components : Remoting - Application Domains (part 2) - The AppDomain Class, The Host App Domain
Sageone Payroll : The rock of wages
Fine-Tuning Windows 7’s Appearance and Performance : Balancing Appearance and Performance