It was few weeks ago when I was assigned a task to implement a post install step for a financial application. It required retrieving thousands of values from a database, sorting them, and reinserting them back into the database. It was obvious that it would be a time consuming operation. After implementing a single threaded solution, I realized that the operation runtime was taking longer than anticipated. It would take over a minute to complete. During the operation execution, the UI would stop responding since a single thread was performing operations on the database records. The user would be left wondering whether the application had locked up.
This is a common scenario for Windows Forms or WPF applications. During a long running operation the UI is not updated until the operation has completed. There are few different approaches I could have taken to mitigate the problem.
The BackgroundWorker class
For my solution, I opted for the simplest one, BackgroundWorker class, which is a member of System.ComponentModel namespace. It was specifically designed to solve this type of problems. The idea is to run the long running operation on a separate thread and listen for events that report progress and operation completion. The easiest way to understand how it works is to look at the example. It is based on the final solution I implemented for the application I worked on.
There are three assemblies involved: UI, BLL, and DTO.
I have implemented the Consumer class in the UI assembly.
using System; using System.ComponentModel; using System.Windows; namespace UI { public partial class Consumer : Window { private BackgroundWorker backgroundWorker1; private const int itemsCount = 10; public Consumer() { InitializeComponent(); InitializeBackgroundWorker(); progressBar.Maximum = itemsCount; progressBar.Minimum = 0; } // Set up the BackgroundWorker object by attaching event handlers. private void InitializeBackgroundWorker() { backgroundWorker1 = new BackgroundWorker(); backgroundWorker1.WorkerReportsProgress = true; backgroundWorker1.WorkerSupportsCancellation = true; backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); backgroundWorker1.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted); backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); } private void start_Click(object sender, RoutedEventArgs e) { if (0 == string.Compare("Start", start.Content.ToString(), true)) { start.Content = "Cancel"; // Start the asynchronous operation. backgroundWorker1.RunWorkerAsync(itemsCount); } else { start.Content = "Start"; // Cancel the asynchronous operation. this.backgroundWorker1.CancelAsync(); } } public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { // Get the BackgroundWorker that raised this event. BackgroundWorker worker = sender as BackgroundWorker; BLL.Producer producer = new BLL.Producer(); // Assign the result of the computation to the Result property of the DoWorkEventArgs object. // This is will be available to the RunWorkerCompleted eventhandler. e.Result = producer.Execute((int)e.Argument, worker, e); } // This event handler updates the progress bar. private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.progressBar.Value = e.ProgressPercentage; DTO.DataTransferObject dto = e.UserState as DTO.DataTransferObject; this.percentage.Content = (dto.ProcessedItems * 100) / dto.TotalItems; this.percentage.Content += "%"; } // This event handler deals with the results of the background operation. private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // First, handle the case where an exception was thrown. if (e.Error != null) { MessageBox.Show(e.Error.Message); } else if (e.Cancelled) { // Next, handle the case where the user canceled the operation. // Note that due to a race condition in the DoWork event handler, the Cancelled // flag may not have been set, even though CancelAsync was called. MessageBox.Show("Operation cancelled."); } else { // Finally, handle the case where the operation succeeded. if (Convert.ToBoolean(e.Result)) { MessageBox.Show("Operation completed sucessfully."); } } start.Content = "Start"; } } }
The class contains a window with a progress bar and a label controls that report the progress. There is also a start/cancel button.
When the button is pressed, it kicks off the time consuming operation. At the same time the button text changes from “start” to “cancel”. Pressing the “cancel” button terminates the operation.
The BLL assembly implements the producer class. It contains the function that performs the operation and reports the progress back to UI.
using System; using System.ComponentModel; namespace BLL { public class Producer { private DTO.DataTransferObject _dto; public Producer() { _dto = new DTO.DataTransferObject(); } public bool Execute(int itemCount, BackgroundWorker worker, DoWorkEventArgs e) { bool result = true; _dto.TotalItems = itemCount; for (int item = 1; item <= itemCount; item++) { // Abort the operation if the user has canceled. // Note that a call to CancelAsync may have set CancellationPending to true just after the // last invocation of this method exits, so this code will not have the opportunity to set the // DoWorkEventArgs.Cancel flag to true. This means that RunWorkerCompletedEventArgs.Cancelled will // not be set to true in your RunWorkerCompleted event handler. This is a race condition. if (worker.CancellationPending) { result = false; e.Cancel = true; break; } _dto.ProcessedItems = item; // Report progress by passing the DTO worker.ReportProgress(item,_dto); System.Threading.Thread.Sleep(500); } return result; } } }
In order to be able to report the progress and communicate information from BLL to UI layer, I created another assembly that is exclusively used as data transfer object (DTO). During each loop iteration the DTO is passed from BLL to UI.
using System; namespace DTO { public class DataTransferObject { private double _processedItems; private int _totalItems; public DataTransferObject() { } public double ProcessedItems { set { _processedItems = value; } get { return _processedItems; } } public int TotalItems { set { _totalItems = value; } get { return _totalItems; } } } }
For explanation of how all the classes work together we will analyze each one individually referring to the code in pictures above.
Consumer object
Even though it may seem counterintuitive, we will analyze the Consumer object first. The constructor initializes the window controls, instantiates the BackgroundWorker class, and three relevant event handlers. More on the event handlers later. The start_Click method is invoked when the start button is clicked. The button text alternates as described above. This method either starts the asynchronous operation (line 33) or sends the cancel signal (line 39). This is the thread controller. The RunWorkerAsync mehod triggers the DoWork event and has capability to pass a single object argument (itemsCount in our case). In the DoWork event handler the long running operation is executed. In our case we are instantiating a Producer object (line 46) that will perform some work and report back its progress. Three parameters are passed to the Producer Execute method (line 49). The first argument specifies how many items to produce, the second one is a thread reference, and the third one is a reference to DoWorkEventArgs object that will be used by Producer to check if operation needs to be terminated prematurely because of user pressing the cancel button. Once the operation (producer.Execute)has completed it is capable or returning an object which is assigned to DoWorkEventArgs object’s Result property (line 49). The Result property will be available to the RunWorkerCompleted event handler. ProgressChanged event handler is triggered by the Producer when it wants to communicate to the Consumer by passing it one integer and one object (DTO). In the ProgressCahnged method ProgressPercentage property contains the integer value and UserState property contains DTO object. The last relevant event handler is RunWorkerCompleted. It is triggered as the name implies on the successful or unsuccessful operation (producer.Execute) completion. RunWorkerCompletedEventArgs object contains properties that can be queried in order to determine the causes of operation termination which can be exception (line 63), cancellation (line 67), or just to retrieve the result of operation (line 77) that was assigned in DoWork event handler when the operation (producer.Execute) has completed.
Producer object
Producer contains the method (Execute) in which the long running operation is performed. For simplicity purposes I kept this method as basic as possible. The single “for loop” iterates itemCount times which was passed as the argument from the Consumer. After each iteration the thread sleeps for half a second (line 33). The CancellationPending property of the worker thread object is checked (line 24). The property is true if CancelAsync method in the Consumer (line 39) was previously invoked. This would only occur if the user has pressed the cancel button during operation execution. On each iteration Producer notifies Consumer about its progress by passing it an integer value (item) and an object (_dto) (line 32).
DataTransferObject object
Its sole purpose is to be used for data transfer. It contains two properties only. As we have mentioned above Producer marshals relevant information into a DTO and sends it to Consumer. The Consumer unmarshals the DTO and consumes the information.
Initial issues
When I wrote this sample code I experienced few glitches. The progressBar control would fill only one tenth on operation completion. The culprit was the default value (100) of progressBar’s Maximum property. The Maximum property and the highest value you expect to assign to progressBar’s Value property during runtime need to be equal. In my case the default Maximum value was 100 and the highest value of Value property was 10. Another issue I run into was inability to cancel the operation. I was not checking the CancellationPending property in Producer’s Execute method and exiting the method when the property was set.
Conclusion
Providing a pleasant user experience is crucial to a well implemented application. At no point user should have doubts of application stability due to its unresponsiveness. BackgroundWorker class is just one of few possible solutions that is also arguably the simplest one.
In next week’s post “Gmail as a cloud clipboard” I will demonstrate how I have been using Gmail for banned file transfer and temporary storage.
In principle, a good happen, support the views of the author