4

I have a backgroundworker that runs a single process. I want to be able to cancel the processing while it's going, but when I call the CancelAsync() method, it never actually cancels. Where am I wrong?

Here's the DoWork() method:

        private void bgw_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker b = sender as BackgroundWorker;

        if (b != null)
        {
             if (!b.CancellationPending)
            {
                try
                {
                    // Let's run the process as a backgroundworker so we have the ability to cancel the search, and/or be able to view results while it's still searching
                    ProcessParameters pp = e.Argument as ProcessParameters;

                    if (pp.DoReplace)
                        results = FindReplace.FindReplace.FindAndReplace(pp.PathToSearch, pp.FindText, pp.ReplaceText, pp.UseRegularExpressions, pp.IncludeList, pp.ExcludeList, pp.RecurseSubdirectories, pp.IgnoreCase);
                    else
                        results = FindReplace.FindReplace.Find(pp.PathToSearch, pp.FindText, pp.UseRegularExpressions, pp.IncludeList, pp.ExcludeList, pp.RecurseSubdirectories, pp.IgnoreCase);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.ToString());
                }
            }
            else
            {
                // Cancel was clicked
                e.Cancel = true;
            }
        }
    }

Here's the method that starts the processing:

        private void btnGo_Click(object sender, EventArgs e)
    {
        if (btnGo.Text == "Cancel")
        {
            if (DialogResult.Yes == MessageBox.Show("Are you sure you wish to cancel?", "Cancel Requested", MessageBoxButtons.YesNo, MessageBoxIcon.Question))
                bgw.CancelAsync();

            return;
        }

        if (tbFind.Text.Length == 0)
        {
            MessageBox.Show("Find text is not valid.");
            return;
        }

        tbFound.Text = String.Empty;
        tbFoundInThisFile.Text = String.Empty;
        lvResults.Items.Clear();
        includeList = null;
        excludeList = null;
        results = null;

        if (radDirectory.Checked && !radFile.Checked)
        {
            includeList = BuildIncludeExcludeList(tbIncludeFiles.Text);
            excludeList = BuildIncludeExcludeList(tbExcludeFiles.Text);
        }

        ProcessParameters pp = null;

        if (chkReplace.Checked)
            pp = new ProcessParameters(tbPath.Text, tbFind.Text, tbReplace.Text, chkUseRegEx.Checked, includeList, excludeList, chkRecursion.Checked, chkIgnoreCase.Checked, true);
        else
            pp = new ProcessParameters(tbPath.Text, tbFind.Text, chkUseRegEx.Checked, includeList, excludeList, chkRecursion.Checked, chkIgnoreCase.Checked, false);

        bgw.RunWorkerAsync(pp);

        // Toggle fields to locked while it's running
        btnGo.Text = "Cancel";
    }

And here's the WorkerCompleted() event:

        private void bgw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        btnGo.Text = "Go";

        string message = String.Empty;
        const string caption = "FindAndReplace is Complete";

        if (!e.Cancelled)
        {
            if (results != null)
            {
                tbFound.Text = results.Found.ToString();
                tbSearched.Text = results.FilesSearched.ToString();
                tbSkipped.Text = results.FilesSkipped.ToString();

                message = String.Format("Search finished resulting in {0} match(es).", results.Found);
            }
            else
                message = "The FindAndReplace results were empty. The process was cancelled or there was an error during operation.";
        }
        else
            message = "The FindAndReplace process was cancelled.";

        if (e.Error != null)
            message += String.Format("{0}{0}There was an error during processing: {1}", Environment.NewLine, e.Error);

        MessageBox.Show(message, caption);
    }
ganders
  • 7,285
  • 17
  • 66
  • 114
  • 1
    You only have a very tiny window that checks for CancellationPending. You won't hot it. – H H May 07 '12 at 13:40
  • 1
    OT but in your Completed handler the check for e.Error should go first. – H H May 07 '12 at 13:41

3 Answers3

8

CancelAsync doesn't actually abort your thread or anything like that. It sends a message to the worker thread that work should be cancelled via BackgroundWorker.CancellationPending. Your DoWork delegate that is being ran in the background must periodically check this property and handle the cancellation itself.

Read more here

Community
  • 1
  • 1
zimdanen
  • 5,508
  • 7
  • 44
  • 89
5

You don't really have a way to cancel the operation. The problem is that this code

               if (pp.DoReplace)
                    results = FindReplace.FindReplace.FindAndReplace(pp.PathToSearch, pp.FindText, pp.ReplaceText, pp.UseRegularExpressions, pp.IncludeList, pp.ExcludeList, pp.RecurseSubdirectories, pp.IgnoreCase);
                else
                    results = FindReplace.FindReplace.Find(pp.PathToSearch, pp.FindText, pp.UseRegularExpressions, pp.IncludeList, pp.ExcludeList, pp.RecurseSubdirectories, pp.IgnoreCase);

doesn't have any way to break once it starts running. So, what winds up happening is that you hit cancel, but the cancel never registers unless you've canceled before the action begins. Once that action is complete, the DoWork method returns successfully and the backgroundworker never triggers the cancellation.

EDIT: If you have a way to break the text up into smaller chunks that can then be "searched and replaced", you could loop through those segments and perform a cancellation check on each loop. You'd need to make sure that you account for the search string being across those break boundaries, though, so it may actually take LONGER to allow for cancellation.

saluce
  • 13,035
  • 3
  • 50
  • 67
  • that's what I was afraid of. My "background" process is one LONG process, so I wasn't sure how to kill that single process. Is there a way at all to be able to do that? – ganders May 07 '12 at 13:42
  • 3
    Since your DoWork method basically just runs a single command, then no, there isn't really a way to cancel that one command. You can only check for cancellations between commands (especially if you are looping). You could, perhaps, break the text up into smaller segments, then loop through the `FindReplace`, checking for the cancellation on each loop. – saluce May 07 '12 at 13:45
  • So, it sounds like I shouldn't be using a backgroundworker, but use just a separate thread (maybe called from the backgroundworker so I can still report progress?) Then when my cancel is clicked, I can call a thread.Abort(), or something like that? Is that "smart" to do? – ganders May 07 '12 at 13:48
  • 2
    You could modify the WorkerCompleted to do nothing if the operation was cancelled...or does that even register if the worker completes successfully? I'd be leery of aborting threads, but that may just be me. – saluce May 07 '12 at 13:51
  • I just noticed your edit to your answer and that's the perfect idea. Since I'm recursing over directories and sub-directories, I can just check for a cancel parameter that I can set each time it goes to a new file. I'm wondering how much that will hurt performance though... – ganders May 07 '12 at 13:59
  • 1
    By the way, I implemented a "CheckForCancel" method call in each of the loops/recursion steps and it SIGNIFICANTLY reduced performance, it does work though. – ganders Dec 27 '12 at 13:58
3

Your code is right, but if you carefully read it again, you will see that once the background worker starts, soon it goes beyond the cancel check. After that even if you try to cancel, it won't work any more.

You have to redesign your search and replace algorithm to include the cancel check too, so as to support cancellation as you wished.

Lex Li
  • 60,503
  • 9
  • 116
  • 147