Chapter 37. Responsive UI During Intensive Processing

 

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.

Implementing the Worker Logic

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

CancelAsync

Invoking this method will cancel the progressing task.

CancellationPending

Invoking CancelAsync will set this property to true, signifying that the user has requested cancellation of the task.

DoWork

This event is fired when RunWorkerAsync is invoked.

ProgressChanged

This event is fired when ReportProgress is invoked.

ReportProgress

Invoking this method will fire the ProgressChanged event, updating the progress of the operation.

RunWorkerAsync

Executes the task asynchronously on a worker thread.

RunWorkerCompleted

This event is fired when the task is completed or cancelled, or when an unhandled exception is thrown within the DoWork event.

WorkerReportsProgress

Boolean property that specifies whether or not to report progress.

WorkerSupportsCancellation

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

e.Argument

Defined as an object, so any arbitrary data type can be used as an input argument for the DoWork event. This parameter is passed into the RunWorkerAsync method.

e.Cancel

This property allows you to cancel the progressing task. Setting this property to true will cancel the task and move the context to the RunWorkerCompleted event with a cancelled status. This property is used in conjunction with the CancellationPending property to determine whether or not the user has issued a cancellation request.

e.Result

Defined as an object, so any arbitrary data type can be used as an output result to the RunWorkerCompleted event. This property is a way to communicate the result or status back to the user interface.

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

Note

The DoWork method can complete in three ways: the process completes successfully, the user requests cancellation, or an unhandled exception occurs.

Reporting Operation Progress

The ProgressChanged is used to report status to the user interface. This event is fired whenever the ReportProgress method is invoked.

Note

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.

Supporting User Cancellation

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

Note

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.

Executing the Worker Thread

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

Conclusion

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.

Screenshot of the demo application on the Web site.

Figure 37.1. Screenshot of the demo application on the 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.100.237