3

I'm trying to evolve the code from this Progress bar tutorial with a cancel button but so far without success:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private CancellationTokenSource cts;

    private void Calculate(int i)
    {
        Math.Pow(i, i);
    }

    public void DoWork(IProgress<int> progress, CancellationToken cancelToken)
    {
        for (int j = 0; j < 100000; j++)
        {
            if (cancelToken.IsCancellationRequested)
                cancelToken.ThrowIfCancellationRequested();
            Calculate(j);
            progress?.Report((1 + j) * 100 / 100000);
        }
    }

    private async void run_Click(object sender, EventArgs e)
    {
        cts = new CancellationTokenSource();
        var cancelToken = cts.Token;
        progressBar.Maximum = 100;
        progressBar.Step = 1;
        var progress = new Progress<int>(v => progressBar.Value = v);

        try
        {
            await Task.Run(() => { DoWork(progress, cancelToken); }, cts.Token);
        }
        catch (OperationCanceledException ex)
        {
            Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {ex.Message}");
        }
        finally
        {
            cts = null;
        }
    }

    private void cancel_Click(object sender, EventArgs e)
    {
        Console.WriteLine("Cancel");
        cts?.Cancel();
    }
}

After clicking run, the UI froze and I can't click on the Cancel button. I've read those blogs:

But couldn't find an answer, I have also tried this variation:

    await Task.Factory.StartNew(() => { DoWork(progress, cancelToken); }, cts.Token);

And it didn't work, can't click on the cancel button during the loading. Any idea? (I'm sure it's ridiculously simple).

Hydraxize
  • 161
  • 10
  • 1
    It looks like your reporting progress with every iteration and with no delay. Your loop would then be running too fast to let the ui update and respond. Add something like `await Task.Delay(TimeSpan)` to your `DoWork` – JSteward Jan 14 '20 at 16:58
  • 2
    See the documentation of [Progress<>](https://learn.microsoft.com/en-us/dotnet/api/system.progress-1): the Progress class captures the synchronization context (here UI thread) on its creation and then switches to this sync. context when you call `Report`. Thus for very tight loops - like here - you are constantly switching to the UI thread and considering that updating the progress is more computationally expensive than your `Compute` function your `DoWork` method is mostly running on the UI thread and blocking it. You should only report every 1000 items or so. – ckuri Jan 14 '20 at 16:59
  • Thank you both, I have simply added `if (j % 500 == 0)` before `progress?.Report((1 + j) * 100 / 100000000);` and increased the loop to 100000000 and it works like a charm. The code was actually correct, as you said, the `DoWork` wasn't doing enough work. – Hydraxize Jan 14 '20 at 21:34

1 Answers1

2

can't click on the cancel button during the loading. Any idea? (I'm sure it's ridiculously simple).

As others have noted, your DoWork needs to do more work before interrupting the UI thread with another update. So something like this should work:

private void Calculate(int i)
{
  for (int j = 0; j != 10000; ++j)
    Math.Pow(i, i);
}

Currently, the UI thread is swamped with progress updates, so it doesn't have time to respond to user input.

If your real-world code is not easily split into larger chunks, you can use a rate-throttling IProgress<T> implementation like one I wrote when updating my book:

public static class ObservableProgress
{
  public static (IObservable<T> Observable, IProgress<T> Progress) CreateForUi<T>(TimeSpan? sampleInterval = null)
  {
    var (observable, progress) = Create<T>();
    observable = observable.Sample(sampleInterval ?? TimeSpan.FromMilliseconds(100))
        .ObserveOn(SynchronizationContext.Current);
    return (observable, progress);
  }

  public static (IObservable<T> Observable, IProgress<T> Progress) Create<T>()
  {
    var progress = new EventProgress<T>();
    var observable = Observable.FromEvent<T>(handler => progress.OnReport += handler, handler => progress.OnReport -= handler);
    return (observable, progress);
  }

  private sealed class EventProgress<T> : IProgress<T>
  {
    public event Action<T> OnReport;
    void IProgress<T>.Report(T value) => OnReport?.Invoke(value);
  }
}

Usage:

private async void run_Click(object sender, EventArgs e)
{
  cts = new CancellationTokenSource();
  var cancelToken = cts.Token;
  progressBar.Maximum = 100;
  progressBar.Step = 1;
  var (observable, progress) = ObservableProgress.CreateForUi<int>();

  try
  {
    using (observable.Subscribe(v => progressBar.Value = v))
      await Task.Run(() => { DoWork(progress, cancelToken); }, cts.Token);
  }
  catch (OperationCanceledException ex)
  {
    Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {ex.Message}");
  }
  finally
  {
    cts = null;
  }
}

The rate-throttling IProgress<T> will throw away extra progress updates, only sending one progress update every 100ms to the UI thread, which it should easily be able to handle and remain responsive to user interaction.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810