12

I want to execute a long running task after clicking a wpf button. Here what I did.

private void Start(object sender, RoutedEventArgs e)
{
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(2000); // simulate task
    }
}

Problem is, this will make wpf gui unresponsive. I also would like to allow cancellation and report progress every 1 second. I expand the code as below.

    DispatcherTimer dispatcherTimer = new DispatcherTimer(); // get progress every second
    private int progress = 0; // for progress reporting
    private bool isCancelled = false; // cancellation

    private void Start(object sender, RoutedEventArgs e)
    {
        InitializeTimer(); // initiallize interval timer 
        Start(10); // execute task
    }

    private void InitializeTimer()
    {
        dispatcherTimer.Tick += dispatcherTimer_Tick;
        dispatcherTimer.Interval = new TimeSpan(0,0,1);
        dispatcherTimer.Start();
    }

    private void dispatcherTimer_Tick(object sender, EventArgs e)
    {
        Logger.Info("Current loop progress " + progress); // report progress
    }

    private void Cancel(object sender, RoutedEventArgs e) // cancel button
    {
        isCancelled = true;
    }

    private int Start(int limit)
    {
        isCancelled = true;
        progress = 0;

        for (int i = 0; i < limit; i++)
        {
            Thread.Sleep(2000); // simulate task
            progress = i; // for progress report
            if (isCancelled) // cancellation
            {
                break;
            }
        }
        return limit;
    }

My target platform is .NET 4.5. What is the recommended way to do this?

Thanks.

Syaiful Nizam Yahya
  • 4,196
  • 11
  • 51
  • 71
  • 2
    You'll likely want to use Tasks and async/await. They were built with exactly this in mind. – Cory Nelson Jan 25 '14 at 07:56
  • 1
    @publicENEMY, as you tagged your question with `task-parallel-library`, you should specific if you can target .NET 4.5 (or .NET 4.0 + [Microsoft.Bcl.Async](http://www.nuget.org/packages/Microsoft.Bcl.Async)). – noseratio Jan 26 '14 at 08:12
  • possible duplicate of [Execute task in background in WPF application](http://stackoverflow.com/questions/21326601/execute-task-in-background-in-wpf-application) – Michael Edenfield Jan 26 '14 at 12:44
  • @MichaelEdenfield Since that other question didnt emphasize cancellation and progress reporting, none of the answer provide solution to cancellation and progress reporting. This question is basically a rewrite that other questions. – Syaiful Nizam Yahya Jan 27 '14 at 05:54
  • 1
    @publicENEMY since that other question is *your* question, you could have edited your question to emphasize what you needed instead of asking essentially the same question again, which will only result in diluting the answers. – Michael Edenfield Jan 27 '14 at 13:23
  • @MichaelEdenfield Okay. What should i do now? Should I delete the other question? – Syaiful Nizam Yahya Jan 28 '14 at 01:02

2 Answers2

26

I thought I answered your question here. If you need more sample code on how to do this using Task Parallel Library, with CancellationTokenSource and IProgress<T>, here it is:

Action _cancelWork;

private async void StartButton_Click(object sender, RoutedEventArgs e)
{
    this.StartButton.IsEnabled = false;
    this.StopButton.IsEnabled = true;
    try
    {
        var cancellationTokenSource = new CancellationTokenSource();

        this._cancelWork = () => 
        {
            this.StopButton.IsEnabled = false;
            cancellationTokenSource.Cancel();
         };

        var limit = 10;

        var progressReport = new Progress<int>((i) => 
            this.TextBox.Text = (100 * i / (limit-1)).ToString() + "%");

        var token = cancellationTokenSource.Token;

        await Task.Run(() =>
            DoWork(limit, token, progressReport), 
            token);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
    this.StartButton.IsEnabled = true;
    this.StopButton.IsEnabled = false;
    this._cancelWork = null;
}

private void StopButton_Click(object sender, RoutedEventArgs e)
{
    this._cancelWork?.Invoke();
}

private int DoWork(
    int limit, 
    CancellationToken token,
    IProgress<int> progressReport)
{
    var progress = 0;

    for (int i = 0; i < limit; i++)
    {
        progressReport.Report(progress++);
        Thread.Sleep(2000); // simulate a work item
        token.ThrowIfCancellationRequested();
    }
    return limit;
}
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    `IProgress` interface is available from .Net 4.5 and not before that. – Rohit Vats Jan 26 '14 at 07:33
  • 3
    @RohitVats, `IProgress` is available for .NET 4.0 with [Microsoft.Bcl.Async](http://www.nuget.org/packages/Microsoft.Bcl.Async), which is a production quality library. It can also be easily replaced with a `Dispatcher.BeginInvoke` one-liner. – noseratio Jan 26 '14 at 07:55
  • 1
    Right. Needs NuGet library to get it work. But how it can be replaced by `Dispatcher.BeginInvoke`? That is used to put delegates on dispatcher asynchronously. How that is related to report Progress? – Rohit Vats Jan 26 '14 at 07:58
  • 3
    @RohitVats, `IProgress.Report` is **asynchronous**. When created, `Progress` captures the synchronization context of the UI thread. Then, when `Report` is called from the worker thread, it uses `SynchronizationContext.Post` internally, not `SynchronizationContext.Send`. In case with `DispatcherSynchronizationContext`, the `Post` is a wrapper around `Dispatcher.BeginInvoke`. The implementation of `Progress` is trivial, if you check it with Reflector or a similar tool. – noseratio Jan 26 '14 at 08:08
  • 1
    Fair enough. Now I get it what you are trying to say. It got me thinking that how `Progress` can be replaced by `Dispatcher.BeginInvoke`. This line `Post is a wrapper around Dispatcher.BeginInvoke` makes perfect sense. :) – Rohit Vats Jan 26 '14 at 08:12
  • 1
    +1 for hiding cancellation into `cancelWork` lambda. – avo Jan 27 '14 at 14:37
  • What is the recommended way to handle ThrowIfCancellationRequested? – Syaiful Nizam Yahya Jan 28 '14 at 09:27
  • 1
    @publicENEMY, check [this](http://stackoverflow.com/a/21392669/1768303), it shows how to differentiate a cancellation exception from others. – noseratio Jan 28 '14 at 09:29
  • @Noseratio So basically, catch OperationCanceledException and do nothing/something and carry on. By the way, +1 on Action for cancelling work. I now dont have to reset cancellation token values everytime I started Task. – Syaiful Nizam Yahya Jan 28 '14 at 09:41
  • 1
    @publicENEMY, basically yes, but it totally depends on your ViewModel. You need to make sure that the model (and hence the UI) remains in the integral state. – noseratio Jan 28 '14 at 09:48
  • In the statement, await Task.Run(() => DoWork(limit, token, progressReport), token);, why token is passed twice? why progressReport isnt passed twiced? Thanks. – Syaiful Nizam Yahya Jan 28 '14 at 10:36
  • 1
    @publicENEMY: 1) it's passed to `DoWork` so it can do `ThrowIfCancellationRequested`. 2) It's also passed to `Task.Run` itself. The goal for this is that if `token` is already in cancelled state, the task won't even start, `Task.Run` will throw `OperationCanceledException` instantly. – noseratio Jan 28 '14 at 10:40
3

BackgroundWorker is what you are looking for instead of DispatcherTimer.

It provides support of Cancellation (via WorkerSupportsCancellation) and reporting progress back on UI thread (via WorkerReportsProgress).

Refer to excellent detailed article here - How to Use BackgroundWorker.

Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • 3
    `BackgroundWorker` can get the job done, but it's superseded with TPL: [Task.Run vs BackgroundWorker](http://blog.stephencleary.com/search/label/Task.Run%20vs%20BackgroundWorker). – noseratio Jan 26 '14 at 02:53
  • 1
    That's completely dependent on how complex is code. Underneath both are using ThreadPool. Also `IProgress` interface is available from .Net 4.5 and not before that. – Rohit Vats Jan 26 '14 at 07:32
  • 1
    I've addressed the same point about `IProgress` in the [comments to my answer](http://stackoverflow.com/a/21357567/1768303). – noseratio Jan 26 '14 at 07:58
  • Is this Silverlight dependent? – FizxMike Jul 11 '17 at 20:08
  • 1
    @FizxMike - BackgroundWorker, IProgress and TPL are independent of Silverlight and WPF. – Rohit Vats Jul 12 '17 at 06:34