How to execute task in the wpf background while able to provide report and allow cancellation?
Asked Answered
G

2

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.

Goff answered 25/1, 2014 at 7:53 Comment(6)
You'll likely want to use Tasks and async/await. They were built with exactly this in mind.Haldi
@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).Treenware
possible duplicate of Execute task in background in WPF applicationBought
@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.Goff
@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.Bought
@MichaelEdenfield Okay. What should i do now? Should I delete the other question?Goff
T
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;
}
Treenware answered 25/1, 2014 at 23:24 Comment(12)
IProgress<T> interface is available from .Net 4.5 and not before that.Pawsner
@RohitVats, IProgress<T> is available for .NET 4.0 with Microsoft.Bcl.Async, which is a production quality library. It can also be easily replaced with a Dispatcher.BeginInvoke one-liner.Treenware
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?Pawsner
@RohitVats, IProgress<T>.Report is asynchronous. When created, Progress<T> 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<T> is trivial, if you check it with Reflector or a similar tool.Treenware
Fair enough. Now I get it what you are trying to say. It got me thinking that how Progress<T> can be replaced by Dispatcher.BeginInvoke. This line Post is a wrapper around Dispatcher.BeginInvoke makes perfect sense. :)Pawsner
+1 for hiding cancellation into cancelWork lambda.Uphroe
What is the recommended way to handle ThrowIfCancellationRequested?Goff
@publicENEMY, check this, it shows how to differentiate a cancellation exception from others.Treenware
@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.Goff
@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.Treenware
In the statement, await Task.Run(() => DoWork(limit, token, progressReport), token);, why token is passed twice? why progressReport isnt passed twiced? Thanks.Goff
@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.Treenware
P
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.

Pawsner answered 25/1, 2014 at 9:17 Comment(5)
BackgroundWorker can get the job done, but it's superseded with TPL: Task.Run vs BackgroundWorker.Treenware
That's completely dependent on how complex is code. Underneath both are using ThreadPool. Also IProgress<T> interface is available from .Net 4.5 and not before that.Pawsner
I've addressed the same point about IProgress<T> in the comments to my answer.Treenware
Is this Silverlight dependent?Manic
@Manic - BackgroundWorker, IProgress<T> and TPL are independent of Silverlight and WPF.Pawsner

© 2022 - 2024 — McMap. All rights reserved.