Complexity is a sign of technical immaturity. Simplicity of use is the real sign of a well designed product whether it is an ATM or a Patriot missile. | ||
--Daniel T. Ling |
It is fairly common to use applications that fail to repaint their windows, displaying an empty or partially empty frame on the screen. Or you may use applications that execute a long-running task, ignoring you until the task completes. In some instances, you may even wish to abort the task rather than wait for completion, which is not supported by these applications. A responsive user interface is very important, so it is crucial that you design your application so the user knows the current state of the application: whether a message sent to the application has been received, and that the application has not stalled when processing a complex task or operation. Users want to feel in control of the application, so be sure that the user can always control the flow of the application.
To fully understand the importance of a responsive user interface, a few common problems will be addressed that users typically come across. The first problem is a window that takes a long time to repaint during a time-consuming operation. During this operation, the application does not give any CPU cycles to the user interface, which results in the user waiting for the window to update or for keyboard and mouse events to be processed. These wait times make the user interface seem sluggish, or even cause the application to be unusable.
Another problem occurs when the application performs a long-running task but does not provide any control to the user during this period. Many times these tasks are developed to execute in entirety and then return, maybe updating the progress and displaying it to the user, but the user will still not be able to interact with the application until the task completes. This can lead to a few problems, such as the user being unable to cancel the task if the need arises, and keeping the user from taking advantage of other application features that logically should be available during the long process.
While performing a long task, you should make sure the application informs the user of the progress by periodically updating the window with a progress bar or similar control. Let the user know that the application is executing normally and that the task is progressing. Additionally, you should also support interaction by the user or the ability to access logical features while the task is processing.
For years, one of the most difficult and time-consuming tasks in Windows programming has been the development and debugging of multi-threaded solutions. Developers using .NET typically write asynchronous code using an asynchronous pattern that returns an IAsyncResult
using Begin
and End
methods. Otherwise, developers use delegates or an explicit threading technique such as thread pools. Some developers even resort to writing custom threading systems for various reasons, despite the pain and suffering that occurs during the development of such solutions. Generally, it is better to have an intrinsic approach, based on infrastructure, than to build a custom approach from the ground up. Additional patterns have been introduced in version 2.0 of the .NET framework. One of these new patterns is AsyncOperationManager
and AsyncOperationFactory
, coupled with a set of custom events and delegates. While this approach is fairly straightforward to use, advanced tasks will not benefit from this new mechanism.
An excellent component introduced in .NET 2.0 is the BackgroundWorker
class, which is a convenient way to start and monitor asynchronous operations, with the ability to cancel the operation and report progress to the user. This chapter shows how to use BackgroundWorker
safely and how to correctly marshal control between the worker thread and the Windows Forms thread in a thread-safe fashion. The demo presented in this solution calculates Fibonacci numbers, and will be used to introduce you to the implementation details of BackgroundWorker
.
The first step is to create a new BackgroundWorker
object, specify the appropriate settings, and rig the instance up with event handlers. Table 37.1 shows the important members of the BackgroundWorker
class.
Table 37.1. Important Members of BackgroundWorker
Member | Description |
---|---|
| Invoking this method will cancel the progressing task. |
| Invoking |
| This event is fired when |
| This event is fired when |
| Invoking this method will fire the |
| Executes the task asynchronously on a worker thread. |
| This event is fired when the task is completed or cancelled, or when an unhandled exception is thrown within the |
| Boolean property that specifies whether or not to report progress. |
| Boolean property that specifies whether or not the task can be cancelled. |
You can create BackgroundWorker
programmatically or by dragging it onto your form from the Components tab of the Visual Studio toolbox. The example in this chapter shows programmatic instantiation and configuration. The following code shows how to instantiate the BackgroundWorker
.
backgroundWorker = new BackgroundWorker(); backgroundWorker.WorkerReportsProgress = true; backgroundWorker.WorkerSupportsCancellation = true; backgroundWorker.DoWork += new DoWorkEventHandler(backgroundWorker_DoWork); backgroundWorker.ProgressChanged += new ProgressChangedEventHandler (backgroundWorker_ProgressChanged); backgroundWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler (backgroundWorker_RunWorkerCompleted);
The DoWork
event provides an instance of DoWorkEventArgs
as a parameter, which handles the input, output, and cancellation properties of the worker thread. Table 37.2 shows the properties of DoWorkEventArgs
.
Table 37.2. Properties of DoWorkEventArgs
Property | Description |
---|---|
| Defined as an object, so any arbitrary data type can be used as an input argument for the |
| This property allows you to cancel the progressing task. Setting this property to true will cancel the task and move the context to the |
| Defined as an object, so any arbitrary data type can be used as an output result to the |
The following code shows the implementation behind the DoWork
event for the Fibonacci calculator. Notice the exception that is thrown when the compute number has an invalid value. This is because any values higher than 91 will result in an overflow with the long data type.
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { if (((int)e.Argument < 0) || ((int)e.Argument > 91)) { throw new ArgumentException("Compute number must be >= 0 and <= 91"); } e.Result = ComputeFibonacci((int)e.Argument, (BackgroundWorker)sender, e); }
The following code shows the actual processing logic behind the Fibonacci calculations. This logic is in its own method because it calculates the numbers using recursion.
private long ComputeFibonacci(int computeNumber, BackgroundWorker worker, DoWorkEventArgs e) { long result = 0; if (worker.CancellationPending) { e.Cancel = true; } else { if (computeNumber < 2) { result = 1; } else { result = ComputeFibonacci(computeNumber - 1, worker, e) + ComputeFibonacci(computeNumber - 2, worker, e); } int percentComplete = (int)((float)computeNumber / (float)((int)e.Argument) * 100); if (percentComplete > percentageReached) { percentageReached = percentComplete; worker.ReportProgress(percentComplete); } } return result; }
The ProgressChanged
is used to report status to the user interface. This event is fired whenever the ReportProgress
method is invoked.
Do not make excessive calls to the ReportProgress
method, because each call adds additional overhead to your background processing, taking longer to complete; however, it is also important to enable users to witness the current progress of the tasks, making it tricky to find the right balance of use.
The PercentageProgress
property of the ProgressChanged
event arguments will return the percentage completed value, set by the ReportProgress
method. You can also access the user state within the ProgressChanged
event handler arguments. The following code shows the implementation details for the progress changed event.
private void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { OperationProgressBar.Value = e.ProgressPercentage; ResultLabel.Text = String.Format("Calculating: {0}%", e.ProgressPercentage.ToString()); }
BackgroundWorker
events are not marshaled across AppDomain
boundaries; therefore you must not use BackgroundWorker
to process tasks in more than one AppDomain
. You must be careful not to manipulate the user interface in your DoWork
event handler. The proper way is to communicate with the user interface through the ProgressChanged
and RunWorkerCompleted
events.
Allowing the user to cancel the progressing task is extremely easy. Just invoke the CancelAsync
method on the BackgroundWorker
instance and then handle the CancellationPending
property in the DoWork
event appropriately.
private void CancellationButton_Click(object sender, EventArgs e) { backgroundWorker.CancelAsync(); CancellationButton.Enabled = false; }
It is important to know that if a call to CancelAsync
sets CancellationPending
to true just after the last invocation of the DoWork
event, then the code will not have the opportunity to set the DoWorkEventArgs.Cancel
flag to true. This results in the Cancelled
flag being set to false in the RunWorkerCompleted
event. This problem occurs because of a race condition.
BackgroundWorker
executes the DoWork
event in a separate thread so the user interface remains responsive. Executing the worker thread is very easy; it’s done by invoking the RunWorkerAsync
method on the BackgroundWorker
. This method optionally allows you to pass in an argument that the worker logic can use during processing. The following code shows the implementation details behind the demo that is available on the Companion Web site for this book.
private void ComputeButton_Click(object sender, EventArgs e) { ResultLabel.Text = string.Empty; ComputeNumberField.Enabled = false; ComputeButton.Enabled = false; CancellationButton.Enabled = true; int computeNumber = (int)ComputeNumberField.Value; percentageReached = 0; backgroundWorker.RunWorkerAsync(computeNumber); }
No matter how the DoWork
event completes, whether successfully or in an erroneous manner, the RunWorkerCompleted
event will always fire, providing an instance of RunWorkerCompletedEventArgs
that contains the status and result of the operation. This event will allow you to respond appropriately to whatever result is returned by the worker thread. When an error occurs, you can retrieve the exception object from the Error
property. This property will be null if no errors occurred during processing. When a cancellation occurs at the request of the user, the Cancellation
property will be set to true. Otherwise, you can retrieve the result from the Result
property if there is one. The following code shows the completed event handler that is fired when the processing task is finished.
private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { ResultLabel.Text = String.Format("Error: {0}", e.Error.Message); } else if (e.Cancelled) { ResultLabel.Text = "Cancelled"; } else { ResultLabel.Text = e.Result.ToString(); } ComputeNumberField.Enabled = true; ComputeButton.Enabled = true; CancellationButton.Enabled = false; }
Creating a user interface that is responsive is not that difficult, provided that you know the techniques required to do so. Your code must divide time between processing a long-running task and interacting with the user; one should not be sacrificed for the other. You cannot think in a linear fashion when building a long-running task; your application cannot wait around for the task to complete. Think about a good place in the processing logic to stop and report status back to the user. Thankfully, you do not have to worry about processing application events while using the BackgroundWorker
object, because this is done behind the scenes for you.
Figure 37.1 shows a screenshot of the demo application provided on the Companion Web site.
Asynchronous processing can drastically improve the responsiveness of your application. However, do not assume that an asynchronous model is always the best approach; sometimes a synchronous model is a better choice. Thankfully, .NET 2.0 simplifies the tasks related to using either execution model.
3.14.131.47