WPF: display a progress dialog only when its background task takes longer than a specified interval

时间:2018-02-01 18:09:32

标签: c# wpf mvvm task

I am executing a potentially long running operation in the background thread of a modal dialog. The problem is that, when the operation takes a short time, the dialog is shown and closed almost instantaneously, which annoys the users. I would like to show the dialog only if the operation takes longer than, say, 2s.

The dialog is a WPF Window and the long running operation code is in the ViewModel. The ViewModel creates a Task that runs the operation in the background.

Here is a relevant snippet:

public Task StartAction() {
    var mainTask = Task.Factory.StartNew(InternalAction);
    MainTask = mainTask;
    mainTask.ContinueWith(_ => { IsFinished = true; });
    return mainTask;
}   

InternalAction is the potentially long running operation.

This is how I am trying to introduce the delay. I am using Sriram Sakthivel's suggestions from a different answer, but the code is not exactly the same:

var viewModel = ... // Creates the ViewModel
var dialogWindow = ... // Creates the Window and starts the operation by calling viewModel.StartAction();

var delayTask = Task.Delay(2000);
if (viewModel.MainTask != null) {
    Task.WaitAny(delayTask, viewModel.MainTask);
}

if (viewModel.IsFinished) {
    return;
}

ShowDialog(dialogWindow); // this code calls dialogWindow.ShowDialog() eventually

I am not using await because I do not want to yield control to the caller (COM) because the caller expects the result to be ready when it gets the control back.

I have been experimenting with different timeouts, e.g., 5000ms, and I do not see any difference in the behavior. The dialog windows still "blink" (are shown and closed immediately). I am sure I am doing something wrong, but I cannot understand my mistake.

3 个答案:

答案 0 :(得分:0)

You're waiting on MainTask, but MainTask isn't the task that sets IsFinished. You may be returning from WaitAny after InternalAction completes but before the IsFinished = true continuation completes.

Try setting MainTask to the continuation rather than its antecedent:

public Task StartAction() {
    var mainTask = Task.Factory.StartNew(InternalAction);
    var continuation = mainTask.ContinueWith(_ => { IsFinished = true; });

    MainTask = continuation;

    return mainTask;
}  

Note that continuation cannot begin until mainTask has completed, so with this change you'll be waiting on mainTask and continuation.

Note, however, that if IsFinished is being read from the UI thread, you'll want to also set it from the UI thread. That, or make it backed by a volatile field.

答案 1 :(得分:0)

There used to be a 3rd party Library called "Busy Indicator". Maybe you could enable it to only appear if the busy condition is met for a certain time? (https://github.com/xceedsoftware/wpftoolkit/wiki/Xceed-Toolkit-Plus-for-WPF).

Basically it comes down to the ViewModel exposing a "busy" property (or any property that can be converted into a boolean value representing "busy"). And the View reacting to the change on a delay (if any).

I am not sure if XAML itself can do that, as you need to show a window. A bit of code behind might be nesseary here. How about you register a custom ChangeNotification handler that starts a timer, with the timer re-checking if the condition is still met in the "tick" event?

Here is some code, made largely from memory:

//custom ChangeNofiticationHander
busyChangeHanlder(object sender, PropertyChangedEventArgs e){
  if(e.PropertyName == "BusyBoolean"){
    if(BusyBoolean)
      //Start the timer
    else
      //Stop the timer
  }
}

timerTickHandler(object sender, TimerTickEventArgs e){
  if(BusyBoolean){
     //create and Dispaly the Dialog here
  }
}

答案 2 :(得分:0)

        var mainTask = Task.Delay(5000); // your long running task

        if(Task.WaitAny(mainTask, Task.Delay(2000)) == 1){ // if the delay enden first, show dialog
            showDialog();
            await mainTask;
            closeDialog();
        }
        await mainTask; // this will just skip, if mainTask is already done

尝试这种方法 - 如果操作时间超过2秒,它将只显示对话窗口。你也可以将所有这些包装在另一个任务中,然后调用者可以等待整个事情,无论对话框是否显示都没有区别。