Search code examples
c#winformsbackgroundworkercancellation

BackgroundWorker cancellation


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


Solution

  • 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.