0

I am trying to understand what I can and cant do with background workers. I have seen a fair amount of posts on this but it all seems to involve some operation with loops and you cancel an operation within a loop. I am wanting to find out if I can cancel some operation on a background worker without a loop.
I have the following simple form that I'm playing with:

enter image description here

which contains the following code:

string[,] TestData = new string[300000, 100];
List<string> TestDataList;
private static Random random = new Random();

public Form1()
{
    InitializeComponent();
    // Loading up some fake data
    for (int i = 0; i < 300000; i++)
    {
        for (int j = 0; j < 100; j++)
        {
            this.TestData[i, j] = RandomString(10) + j.ToString();
        }
    }
}
public static string RandomString(int length)
{
    const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    return new string(Enumerable.Repeat(chars, length)
      .Select(s => s[random.Next(s.Length)]).ToArray());
}

which loads a string array with a lot of dummy data. The start button method is as follows:

private void StartWork_Click(object sender, EventArgs e)
{
    try
    {
        System.Threading.SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
        BackgroundWorker bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_Complete);
        bw.RunWorkerAsync();
    }
    catch (Exception ex)
    {
        MessageBox.Show("Something went wrong.\nError:" + ex.Message);
    }
}

And I also have:

private void bw_DoWork(object sender, DoWorkEventArgs e)
{
    this.TestDataList = this.TestData.Cast<string>()
    .Select((s, i) => new { GroupIndex = i / 100, Item = s.Trim().ToLower() })
    .GroupBy(g => g.GroupIndex)
    .Select(g => string.Join(",", g.Select(x => x.Item))).ToList();

}

private void bw_Complete(object sender, RunWorkerCompletedEventArgs e)
{
    this.showWorkingLabel.Text = "Work done";
}

private void btnCancel_Click(object sender, EventArgs e)
{
    // I want to cancel the work with this button


    // Then show
    this.showWorkingLabel.Text = "Work Cancelled";
}

So you'll notice that my bw_DoWork method does not contain any loops, just a single operation and I want to know if:

  1. If I can kill/cancel the background worker by clicking the Cancel button while the following code is being executed:
    .Select((s, i) => new { GroupIndex = i / 100, Item = s.Trim().ToLower() })
    .GroupBy(g => g.GroupIndex)
    .Select(g => string.Join(",", g.Select(x => x.Item))).ToList();
  1. Can I update the label showWorkingLabel while the background work is happening so that it continuously shows ".", "..", "..." and then back to "." like a progress bar to indicate work is still happening
Giulio Caccin
  • 2,962
  • 6
  • 36
  • 57
user3042151
  • 113
  • 1
  • 10
  • Can you add the .net version you are using please? – Giulio Caccin Sep 21 '19 at 05:29
  • Possible duplicate of [Cancelling a BackgroundWorker](https://stackoverflow.com/questions/24481879/cancelling-a-backgroundworker) – Giulio Caccin Sep 21 '19 at 05:38
  • https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.backgroundworker?view=netframework-4.8 – Chetan Sep 21 '19 at 06:53
  • You need to declare `BackgroundWorker bw` as `Form` Level.. – Chetan Sep 21 '19 at 06:54
  • It is not unusual at all that you can't find a way to cancel an operation. Which tends to be okay, it doesn't matter that much to a human that it trundles on for a while, as long as the RunWorkerCompleted event knows that it shouldn't use the result of the operation. So, say, 5 seconds is okay. But if it takes longer then it gets to be a problem, hard to close the window for one. You'll then need to find a way to break up the operation so you can insert the cancellation check. So not use Linq for example. Googling "plinq cancel" might help. – Hans Passant Sep 21 '19 at 08:22

3 Answers3

2

You need first to support cancellation

bw.WorkerSupportsCancellation = true;

Then you need to share a cancellation token at form level

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken cancellationToken;

Inside your work you need to throw the cancellation:

cancellationToken.ThrowIfCancellationRequested();

Or handle it gracefully with the background worker even for pending cancellations: BackgroundWorker.CancellationPending

And in the cancell button you can call the cancellation like this:

cts.Cancel();

Using your code it will become something similar to the following indication, you should handle graceful cancellations:

    string[,] TestData = new string[30000, 100];
    List<string> TestDataList;
    private static Random random = new Random();
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken cancellationToken;

    private void BtnStart_Click(object sender, EventArgs e)
    {
        try
        {
            this.showWorkingLabel.Text = "Work start";
            System.Threading.SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
            BackgroundWorker bw = new BackgroundWorker();
            bw.WorkerSupportsCancellation = true;
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);

            cancellationToken = cts.Token;
            cancellationToken.Register(bw.CancelAsync);

            bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_Complete);
            bw.RunWorkerAsync();

        }
        catch (Exception ex)
        {
            MessageBox.Show("Something went wrong.\nError:" + ex.Message);
        }
    }

    private void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        cancellationToken.ThrowIfCancellationRequested();
        this.TestDataList = this.TestData
            .Cast<string>()
            .Select((s, i) => new { GroupIndex = i / 100, Item = s.Trim().ToLower() })
            .GroupBy(g => g.GroupIndex)
            .Select(g =>
            {
                cancellationToken.ThrowIfCancellationRequested();
                return string.Join(",", g.Select(x => x.Item));
            })
            .ToList();
    }

    private void btnCancel_Click(object sender, EventArgs e)
    {
        cts.Cancel();

        this.showWorkingLabel.Text = "Work Cancelled";
    }
Giulio Caccin
  • 2,962
  • 6
  • 36
  • 57
  • The BackgroundWorker has a builtin strategy for cancellation - no need for CancellationToken. This is an example how **not** to handle cancellation on a BackgroundWorker – Sir Rufo Sep 21 '19 at 06:00
  • This approach introduce the user to a more modern approach to await async. I've also tested it before posting so it's working. Also, it would be nice to see other approaches on other answers! – Giulio Caccin Sep 21 '19 at 06:02
  • BTW Setting BackgroundWorker.SupportsCancellation to true and calling BackgrioundWorker.CancelAsync() is useless in this case. – Sir Rufo Sep 21 '19 at 06:49
  • It is necessary to set supports cancellation otherwise this approach won't work. I repeat that I've run this exact code on my machine before posting it. – Giulio Caccin Sep 21 '19 at 06:54
  • remove `bw.WorkerSupportsCancellation = true;` and `cancellationToken.Register(bw.CancelAsync);` and try it again. It will work too. These lines of code are useless, because you did not use the builtin BackgroudWorker cancellation support – Sir Rufo Sep 21 '19 at 06:57
  • As soon as I'll be at home I'll try that, thanks. But this answer it's valid and it's working, also by spporting cancellation token it's possible to pass it to more complex stacks of code/logics so I think I will keep this answer. – Giulio Caccin Sep 21 '19 at 07:16
2

Here is a working example using the BackgroundWorker builtin cancellation support.

// We need to remember the BackgroundWorker
private BackgroundWorker bw;

private void StartWork_Click( object sender, EventArgs e )
{
    bw = new BackgroundWorker
    {
        WorkerSupportsCancellation = true,
    };

    bw.DoWork += Bw_DoWork;
    bw.RunWorkerCompleted += Bw_RunWorkerCompleted;
    bw.RunWorkerAsync();

    showWorkingLabel.Text = "Work started ...";
}

private void Bw_RunWorkerCompleted( object sender, RunWorkerCompletedEventArgs e )
{
    if ( e.Cancelled ) // was it cancelled?
    {
        showWorkingLabel.Text = "Work cancelled.";
        return;
    }

    if ( e.Error != null ) // any error?
    {
        showWorkingLabel.Text = "Work faulted - " + e.Error.Message;
        return;
    }
    // assign the bw Result to the field
    this.TestDataList = (List<string>)e.Result;
    showWorkingLabel.Text = "Work completed.";
}

private void Bw_DoWork( object sender, DoWorkEventArgs e )
{
    try
    {
        e.Result = this.TestData
            .Cast<string>()
            .Select( ( s, i ) =>
            {
                // check for cancellation
                if ( bw.CancellationPending )
                    throw new OperationCanceledException();
                return new
                {
                    GroupIndex = i / 100,
                    Item = s.Trim().ToLower()
                };
            } )
            .GroupBy( g => g.GroupIndex )
            .Select( g =>
            {
                // check for cancellation
                if ( bw.CancellationPending )
                    throw new OperationCanceledException();
                return string.Join( ",", g.Select( x => x.Item ) );
            } )
            .ToList();
    }
    catch ( OperationCanceledException )
    {
        e.Cancel = true;
    }
}

private void btnCancel_Click( object sender, EventArgs e )
{
    // request cancellation
    bw.CancelAsync();
    showWorkingLabel.Text = "Work cancellation requested ...";
}

and another doing exactly the same with the modern async/await Task and CancellationToken

private CancellationTokenSource cts;

private async void StartWork_Click( object sender, EventArgs e )
{
    showWorkingLabel.Text = "Work started ...";
    cts = new CancellationTokenSource();
    var token = cts.Token;

    try
    {
        TestDataList = await Task.Run( () =>
        {
            return this.TestData
                .Cast<string>()
                .Select( ( s, i ) =>
                {
                    token.ThrowIfCancellationRequested();
                    return new
                    {
                        GroupIndex = i / 100,
                        Item = s.Trim().ToLower()
                    };
                } )
                .GroupBy( g => g.GroupIndex )
                .Select( g =>
                {
                    token.ThrowIfCancellationRequested();
                    return string.Join( ",", g.Select( x => x.Item ) );
                } )
                .ToList();
        }, token );
        showWorkingLabel.Text = "Work completed.";
    }
    catch ( OperationCanceledException )
    {
        showWorkingLabel.Text = "Work canceled.";
    }
    catch ( Exception ex )
    {
        showWorkingLabel.Text = "Work faulted - " + ex.Message;
    }

}

private void btnCancel_Click( object sender, EventArgs e )
{
    cts.Cancel();
    showWorkingLabel.Text = "Work cancellation requested ...";
}
Sir Rufo
  • 18,395
  • 2
  • 39
  • 73
1

As per the MSDN page for BackgroundWorker:

When creating the worker, you can make it support cancellation by setting

backgroundWorker.WorkerSupportsCancellation = true;

You can request cancellation by calling CancelAsync() on the BackgroundWorker.

Then your BackgroundWorker should periodically check the BackgroundWorker.CancellationPending property, and if set to true, it should cancel its operation. As Sir Rufo pointed out in a comment, don't forget to inside the DoWork delegate you have to set DoWorkEventArgs.Cancel to true.

The MSDN page I linked has additional examples of usage in real code.

Daniel Crha
  • 675
  • 5
  • 13