Implement cancellation ability on BackgroundWorker

Standard
Share

As I mentioned in my article on implementing the BackgroundWorker to keep your WPF user interface responsive, the BackgroundWorker class supports cancellation. I will illustrate here how easy it is to accomplish.

I’ll build this example by continuing the example in the previous article. First, let’s add a BackgroundWorker property to our DataModel class.

public BackgroundWorker worker { get; set; }

internal void RunBackgroundWorker()
{
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += new DoWorkEventHandler(BackgroundWorker_DoWork);
    worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker_WorkCompleted);
    worker.RunWorkerAsync();
}
 
internal void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    // This is the method that now contains the time-consuming task
    if( IsInProgress )
    {
        return;
    }
    else
    {
        IsInProgress = true;
        RetrieveDataFromURL();
    }
}
 
internal void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    IsInProgress = false;
}

You’ll note that line #1 shows an added automatic property of type BackgroundWorker called “worker”. Now we need to change our RunBackgroundWorker method to assign the new BackgroundWorker created to this “worker” property.

internal void RunBackgroundWorker()
{
    worker = new BackgroundWorker();
    worker.DoWork += new DoWorkEventHandler(BackgroundWorker_DoWork);
    worker.WorkerSupportsCancellation = true;
    worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(BackgroundWorker_WorkCompleted);
    worker.RunWorkerAsync();
}

In line #5, we’re assigning a new BackgroundWorker to the DataModel’s “worker” property. This will allow us to access the worker (to issue the cancellation request) from outside the thread.

In line #7, we’re setting the worker’s WorkerSupportsCancellation property to true. Without this property set to true, our worker simply won’t respond to cancellation requests.

We’ll need to modify our long-running process to periodically check to see if our worker has a pending cancellation request. The RetrieveDataFromUrl method in the original example didn’t lend itself to easily check this property, so I’ll provide a different long-running process example, then show an alternative implementation of the download accomplished in the original article.

public void LongRunningProcess(DoWorkEventArgs e)
{
    for(int i = 0; i++; i < 60)
    {
        if(worker != null && worker.CancellationPending)
        {
            e.Cancel = true;
            break;
        }
        Thread.Sleep(500)
    }
    MessageBox.Show("Loop was interrupted at iteration " + i);
}

Line #1 declares our LongRunningProcess method. Note that we accept the DoWorkEventArgs type argument. It is this object’s Cancel property that triggers the BackgroundWorker to run the RunWorkerCompletedEventHandler event handler. If this long-running thread were to run through several different methods, you may want to expose your DoWorkEventArgs in another fashion – perhaps as a ViewModel property.

If you don’t expose your DoWorkEventArgs object in some fashion, then its scope is limited to the DoWorkEventHandler event handler, and that method is the only location that the thread-cancelling call can be made.

In this example, we only need access to the DoWorkEventArgs object in LongRunningProcess(), so we’ll simply pass it in to the method.

Line #3 starts a for loop, and runs no more than 60 times. In line #5, we check to see if the worker property is not null and if its CancellationPending property is set to true. If so, line #7 sets the DoWorkEventArgs object’s Cancel property to true and line #8 breaks out of the loop. Line #10 simply pauses the thread for 1/2 second (500 milliseconds). In a nutshell, this LongRunningProcess should take about 30 seconds (60 iterations @ .5 seconds each), or less if a pending cancellation request is found.

Now let’s define a command that will interrupt our LongRunningProcess.

private RelayCommand interruptLongRunningProcessCommand;
public ICommand InterruptLongRunningProcessCommand
{
    get
    {
        if(interruptLongRunningProcessCommand == null)
            interruptLongRunningProcessCommand = new RelayCommand(param => InterruptLongRunningProcess());
        return interruptLongRunnningProcessCommand;
    }
}

private void InterruptLongRunningProcess()
{
    worker.CancelAsync();
}

In lines #1-10, we’re simply defining a RelayCommand called InterruptLongRunningProcessCommand. When this command is invoked, it will run the InterruptLongRunningProcess method. Lines #12-15 define the InterruptLongRunningProcess method, which simply calls the worker’s CancelAsync method. For more information on implementing CommandBinding, read my article here.

When the worker’s CancelAsync method is called, the worker’s CancellationPending property is set to true. In our LongRunningProcess method, line #5 checks the worker’s CancellationPending property. If it’s true, then line #7 sets the DoWorkEventArgs object’s Cancel property to true, which fires the worker’s RunWorkerCompleted event.

internal void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if(e.Error != null)
    {
        // insert exception handling code here.
        // the e.Error property contains any unhandled exceptions encountered by BackgroundWorker
    }

    if(e.Cancelled)
    {
        // insert any cancellation code here
        MessageBox.Show("The BackgroundWorker was cancelled.");
    }

    // insert any BackgroundWorker completed code here
    IsInProgress = false;
}

In line #27, we first check the RunWorkerCompletedEventArgs object’s Error property. This property contains any unhandled exception encountered by the BackgroundWorker. If Error is not null, then you may optionally implement some exception handling.

Line #33 checks the Cancelled property of the RunWorkerCompletedEventArgs object. If the process was cancelled the RunWorkerCompletedEventArgs object’s Cancelled property will be true. You may optionally implement any post-cancellation code here. I chose to display a dialog alerting the user of the cancellation (line #36).

And of course, line #40 sets the IsInProgress boolean back to false. Any code that should run at the end of any BackgroundWorker should start at line #39 (in this example), leaving the last line as the IsInProgress reset call.

EDIT
Since the worker in this example is available at the ViewModel level, we can (and should) eliminate the IsInProgress boolean altogether and simply refer to worker’s IsBusy property.
END EDIT

Now we simply need to modify the BackgroundWorker_DoWork method to call our modified LongRunningProcess method instead of the RetrieveDataFromURL method.

internal void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    if( IsInProgress )
    {
        return;
    }
    else
    {
        IsInProgress = true;
        LongRunningProcess(e);
    }
}

Note line #20 now calls the LongRunningProcess method instead of the RetrieveDataFromURL method, passing in the DoWorkEventArgs object. It is through this object that cancellation flows, so we’ll need access to it anywhere we might want to cancel the BackgroundWorker.

To summarize,

  1. Add a BackgroundWorker property to your DataModel for thread access outside of the thread.
  2. Set this property to a new BackgroundWorker.
  3. Set the worker’s WorkerSupportsCancellation property to true.
  4. In your long-running process, check the worker’s CancellationPending property. If worker.CancellationPending is true, set the DoWorkEventArgs object’s Cancel property to true.
  5. In the worker’s RunWorkerCompleted event handler
    1. Check if e.Error is not null, and implement any necessary exception handling.
    2. Check if e.Cancelled is true to determine if the BackgroundWorker was cancelled.
    3. Implement any BackgroundWorker completed code necessary.

Check back soon for my altered RetrieveDataFromURL method that supports cancelling.

Leave a Reply

Your email address will not be published. Required fields are marked *