BackgroundWorker cancellation

2k Views Asked by At

I am using a BackgroundWorker in my winforms app to perform a long running task that occurs in another class (performing database operations). Since all the work is done in another class the cancellation is not as simple. I am using an event in the other class (GenerateStats) to update the progress as the background operation completes. I want to do a similar thing with cancelling the operation. I can't just call cancellationPending in the DoWork function because the method would never see it until it finishes and that defeats the purpose. I want the cancellation functionality without having the pass the BackgroundWorker to generateForSubject(). Is there anyway that this can support cancellation from the generateForSubject() method in GenerateStats class. This is the instantiation of the class that the operation performs in:

GenerateStats genStats = new GenerateStats();

This is my DoWork function, which calls the ReportProgress method anytime the event in the other class gets called. It also calls the method from the other class generateForSubject() that performs the operations.

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;

    genStats.ProgressChanged += (s, pe) => worker.ReportProgress(pe.ProgressPercentage, pe.UserState);
    genStats.generateForSubject();
}

This is the button click event handler that should initiate the cancellation and run CancelAsync()

private void btnStop_Click(object sender, EventArgs e)
{
    if (backgroundWorker.IsBusy)
    {
        backgroundWorker.CancelAsync();
    }
}

This is my separate class that performs the operation, along with creating the ProgressChanged event handler so my class can updated the form with info on progress. It would be awesome if cancellation can perform similarly.

public event ProgressChangedEventHandler ProgressChanged;

protected virtual void OnProgressChanged(int progress, string message)
{
    if (ProgressChanged != null)
    {
        ProgressChanged(this, new ProgressChangedEventArgs(progress, message));
    }
}

public void generateForSubject()
{
    //Perform db operation not important, but it takes time

    OnProgressChanged(33, "Finished 1st set of stats");
    //I hope to check for cancellation here

    //Perform db operation not important, but it takes time

    OnProgressChanged(66, "Finished 2nd set of stats");
    //I hope to check for cancellation here        

    //Perform db operation not important, but it takes time

    OnProgressChanged(99, "Finished 3rd set of stats");
    //I hope to check for cancellation here
}

Just to clarify if there is any uncertainty as to what I am asking, is there any way for me to support cancellation of my backgroundWorker in the other class without passing the backgroundWorker to the method. If there is absolutely no way and I have to, then I will pass the backgroundWorker

2

There are 2 best solutions below

0
On BEST ANSWER

It would be helpful if you could be more specific about your reluctance to pass the BackgroundWorker instance. Knowing why that's a design requirement could help produce a better answer to your question.

That said, you can apply the same philosophy you did for the ProgressChanged event and delegate the cancellation check as well. For example:

class GenerateStats
{
    public event EventHandler<CancelEventArgs> CheckCancel;

    private bool OnCheckCancel()
    {
        EventHandler<CancelEventArgs> handler = CheckCancel;

        if (handler != null)
        {
            CancelEventArgs e = new CancelEventArgs();

            handler(this, e);

            return e.Cancel;
        }

        return false;
    }

    public void generateForSubject()
    {
        //Perform db operation not important, but it takes time

        OnProgressChanged(33, "Finished 1st set of stats");
        if (OnCheckCancel())
        {
            // Or other cancellation logic here
            return;
        }

        //Perform db operation not important, but it takes time

        OnProgressChanged(66, "Finished 2nd set of stats");
        if (OnCheckCancel())
        {
            // Or other cancellation logic here
            return;
        }

        //Perform db operation not important, but it takes time

        OnProgressChanged(99, "Finished 3rd set of stats");
        if (OnCheckCancel())
        {
            // Or other cancellation logic here
            return;
        }
    }
}

private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;

    genStats.ProgressChanged += (s, pe) => worker.ReportProgress(pe.ProgressPercentage, pe.UserState);
    genStats.CheckCancel += (sender1, e1) => e1.Cancel = worker.CancellationPending;
    genStats.generateForSubject();
}

This allows the GenerateStats class to check for pending cancellation without having direct access to the BackgroundWorker instance, just as the ProgressChanged event allows it to report progress via the BackgroundWorker without direct access to the BackgroundWorker.

3
On

You method should periodically check for CancellationPending property to see if the request was made for cancellation. And then take a decision to cancel the operation. See MSDN link for this.

From MSDN:

Be aware that your code in the DoWork event handler may finish its work as a cancellation request is being made, and your polling loop may miss CancellationPending being set to true. In this case, the Cancelled flag of System.ComponentModel.RunWorkerCompletedEventArgs in your RunWorkerCompleted event handler will not be set to true, even though a cancellation request was made. This situation is called a race condition and is a common concern in multithreaded programming. For more information about multithreading design issues, see Managed Threading Best Practices.

EDIT:

If you do not wish to pass the backgroundworker to the generateForSubject method. Set a property in the class when CancelAsync is received. And check the value of this property from generateForSubject method before each operation.