System.ComponentModel
namespace for managing a worker thread. It can be considered a general-purpose implementation of the EAP, and provides the following features:- A cooperative cancellation model
- The ability to safely update WPF or Windows Forms controlswhen the worker completes
- Forwarding of exceptions to the completion event
- A protocol for reporting progress
- An implementation of
IComponent
allowing it to be sited in Visual Studio’s designer
BackgroundWorker
uses the thread pool, which means you should never call Abort
on a BackgroundWorker
thread.Using BackgroundWorker
Here are the minimum steps in using
BackgroundWorker
:- Instantiate
BackgroundWorker
and handle theDoWork
event. - Call
RunWorkerAsync
, optionally with anobject
argument.
This then sets it in motion. Any argument passed to
RunWorkerAsync
will be forwarded to DoWork
’s event handler, via the event argument’s Argument
property. Here’s an example:class Program { static BackgroundWorker _bw = new BackgroundWorker(); static void Main() { _bw.DoWork += bw_DoWork; _bw.RunWorkerAsync ("Message to worker"); Console.ReadLine(); } static void bw_DoWork (object sender, DoWorkEventArgs e) { // This is called on the worker thread Console.WriteLine (e.Argument); // writes "Message to worker" // Perform time-consuming task... } }
BackgroundWorker
has a RunWorkerCompleted
event that fires after the DoWork
event handler has done its job. Handling RunWorkerCompleted
is not mandatory, but you usually do so in order to query any exception that was thrown in DoWork
. Further, code within a RunWorkerCompleted
event handler is able to update user interface controls without explicit marshaling; code within the DoWork
event handler cannot.
To add support for progress reporting:
- Set the
WorkerReportsProgress
property totrue
. - Periodically call
ReportProgress
from within theDoWork
event handler with a “percentage complete” value, and optionally, a user-state object. - Handle the
ProgressChanged
event, querying its event argument’sProgressPercentage
property. - Code in the
ProgressChanged
event handler is free to interact with UI controls just as withRunWorkerCompleted
. This is typically where you will update a progress bar.
To add support for cancellation:
- Set the
WorkerSupportsCancellation
property totrue
. - Periodically check the
CancellationPending
property from within theDoWork
event handler. If it’strue
, set the event argument’sCancel
property totrue
, and return. (The worker can also setCancel
and exit withoutCancellationPending
beingtrue
if it decides that the job is too difficult and it can’t go on.) - Call
CancelAsync
to request cancellation.
Here’s an example that implements all the preceding features:
var bgWorker = new BackgroundWorker { WorkerReportsProgress = true };
bgWorker.DoWork += (o, e) =>
{
//Worker thread code. Gets called in a Non-UI thread.
};
bgWorker.ProgressChanged += (o, e) =>
{
//Progress change gets called on the UI thread. Controls can be accessed safely
};
bgWorker.RunWorkerCompleted += (o, e) =>
{
//gets called when worker thread finishes. UI thread. Controls can be accessed safely
};
bgWorker.RunWorkerAsync();
using System; using System.Threading; using System.ComponentModel; class Program { static BackgroundWorker _bw; static void Main() { _bw = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _bw.DoWork += bw_DoWork; _bw.ProgressChanged += bw_ProgressChanged; _bw.RunWorkerCompleted += bw_RunWorkerCompleted; _bw.RunWorkerAsync ("Hello to worker"); Console.WriteLine ("Press Enter in the next 5 seconds to cancel"); Console.ReadLine(); if (_bw.IsBusy) _bw.CancelAsync(); Console.ReadLine(); } static void bw_DoWork (object sender, DoWorkEventArgs e) { for (int i = 0; i <= 100; i += 20) { if (_bw.CancellationPending) { e.Cancel = true; return; } _bw.ReportProgress (i); Thread.Sleep (1000); // Just for the demo... don't go sleeping } // for real in pooled threads! e.Result = 123; // This gets passed to RunWorkerCompleted } static void bw_RunWorkerCompleted (object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled) Console.WriteLine ("You canceled!"); else if (e.Error != null) Console.WriteLine ("Worker exception: " + e.Error.ToString()); else Console.WriteLine ("Complete: " + e.Result); // from DoWork } static void bw_ProgressChanged (object sender, ProgressChangedEventArgs e) { Console.WriteLine ("Reached " + e.ProgressPercentage + "%"); } }
Press Enter in the next 5 seconds to cancel Reached 0% Reached 20% Reached 40% Reached 60% Reached 80% Reached 100% Complete: 123 Press Enter in the next 5 seconds to cancel Reached 0% Reached 20% Reached 40% You canceled!
Subclassing BackgroundWorker
BackgroundWorker
is not sealed and provides a virtual OnDoWork
method, suggesting another pattern for its use. In writing a potentially long-running method, you could write an additional version returning a subclassedBackgroundWorker
, preconfigured to perform the job concurrently. The consumer then needs to handle only theRunWorkerCompleted
and ProgressChanged
events. For instance, suppose we wrote a time-consuming method called GetFinancialTotals
:public class Client { Dictionary <string,int> GetFinancialTotals (int foo, int bar) { ... } ... } We could refactor it as follows: public class Client { public FinancialWorker GetFinancialTotalsBackground (int foo, int bar) { return new FinancialWorker (foo, bar); } } public class FinancialWorker : BackgroundWorker { public Dictionary <string,int> Result; // You can add typed fields. public readonly int Foo, Bar; public FinancialWorker() { WorkerReportsProgress = true; WorkerSupportsCancellation = true; } public FinancialWorker (int foo, int bar) : this() { this.Foo = foo; this.Bar = bar; } protected override void OnDoWork (DoWorkEventArgs e) { ReportProgress (0, "Working hard on this report..."); // Initialize financial report data // ... while (!<finished report>) { if (CancellationPending) { e.Cancel = true; return; } // Perform another calculation step ... // ... ReportProgress (percentCompleteCalc, "Getting there..."); } ReportProgress (100, "Done!"); e.Result = Result = <completed report data>; } }
Whoever calls
GetFinancialTotalsBackground
then gets a FinancialWorker
: a wrapper to manage the background operation with real-world usability. It can report progress, can be canceled, is friendly with WPF and Windows Forms applications, and handles exceptions well.BackgroundWorker and ProgressBar demo
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Shown += new EventHandler(Form1_Shown);
// To report progress from the background worker we need to set this property
backgroundWorker1.WorkerReportsProgress = true;
// This event will be raised on the worker thread when the worker starts
backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
// This event will be raised when we call ReportProgress
backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
}
void Form1_Shown(object sender, EventArgs e)
{
// Start the background worker
backgroundWorker1.RunWorkerAsync();
}
// On worker thread so do our thing!
void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// Your background task goes here
for (int i = 0; i <= 100; i++)
{
// Report progress to 'UI' thread
backgroundWorker1.ReportProgress(i);
// Simulate long task
System.Threading.Thread.Sleep(100);
}
}
// Back on the 'UI' thread so we can update the progress bar
void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// The progress percentage is a property of e
progressBar1.Value = e.ProgressPercentage;
}
}
No comments:
Post a Comment