10

I have a long-running operation that have to be done in UI thread (involves UI elements that cannot be freezed). I want to display a busy indicator before running the operation.

busyIndicator.Visibility = Visibility.Visible;
LongRunningMethod();
busyIndicator.Visibility = Visibility.Collapsed;

Of course this does not work because rendering does not occur until the operation finishes. I tried to use Task.Yield() to run the rest of method asynchronously:

busyIndicator.Visibility = Visibility.Visible;
await Task.Yield();
LongRunningMethod();

This also does not work, as far as I understand, because the rest of the method is prioritized higher than rendering operation.

How can I do it using TPL?

UPD: LongRunningMethod cannot be run in a separate thread by its nature (works with complex WPF 3D models), and anyway I cannot afford to make changes in it now. So please don't offer solutions based on running it completely or partially on a separate thread.

Aleksey Shubin
  • 1,960
  • 2
  • 20
  • 39

3 Answers3

13

If you want to break UI method execution, then you have to use async/await.

Something like (untested)

busyIndicator.Visibility = Visibility.Visible;
await Task.Run(() => await Task.Delay(1)); // here method will exit and repaint will occurs
LongRunningMethod();
busyIndicator.Visibility = Visibility.Collapsed;

But depending on how long method runs, you may want to put it completely into another thread (Task, BackgroundWorker) and only invoke methods when you need to be in UI thread.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Thanks! Though it seems like a hack, but anyway it works. I think I will go with your solution for now. Will not mark as answer yet in case someone will advice more straightforward way. – Aleksey Shubin Dec 11 '14 at 12:04
  • I had a compiler error on the await before Task.Delay(). Removing it solved the error & works as expected. – dlchambers Jan 14 '21 at 16:01
  • Thanks for the code: I got it to work by adding async : await Task.Run(async () => await Task.Delay(1)); – CodingSoft Jun 16 '23 at 22:08
2

This is quite trivial when you learn how to async and await work. You can read about that on msdn

Your problem is that you are never starting any asynchronous work - Task.Yield is already called from the UI context and will do nothing.

Try this:

async Task MyProcess() //the Task is returned implicitly
{
    busyIndicator.Visibility = Visibility.Visible;
    await LongRunningMethod(); //the work here is done in a worker thread
    busyIndicator.Visibility = Visibility.Collapsed; //this line executes back on the ui context
}

Task LongRunningMethod() //the Async suffix is a convention to differentiate overloads that return Tasks
{
    var result1 = await Task.Run(() => /* do some processing */ ); //we are actually starting an asynchronous task here.

    //update some ui elements here

    var result2 = await Task.Run(() => /* do some more processing */ );

    //update some ui elements here
}
Gusdor
  • 14,001
  • 2
  • 52
  • 64
  • 2
    Creating an`async` method with a name ending in `Async` that just calls `Task.Run` is an anti-pattern because most `async` methods do not use a thread whereas `Task.Run` does so it is a bit misleading – NeddySpaghetti Dec 11 '14 at 08:49
  • As far as I understand, Task.Run() will run the method in another thread. As I mentioned, I cannot run LongRunningMethod in another thread (because it uses UI elements). – Aleksey Shubin Dec 11 '14 at 08:54
  • @NedStoyanov in this sample `LongRunningMethodAsync` is not an `async` method. If you had two methods with the same parameters but one returns a task, the suffix allows you to compile. You should take it up with Microsoft's design team. Arguably, you should have two overloads but this pattern allowed me to show common `async/await` usage without changing the existing worker method (which may be immutable in his code). Don't be a hater dear! ;) – Gusdor Dec 11 '14 at 09:08
  • @AlekseyShubin can `LongRunningMethod` be changed to resemble `MyProcess()` as above? This pattern is the correct way to run asynchronous work and update the without freezing the ui. `async` methods can be nested. – Gusdor Dec 11 '14 at 09:17
  • 1
    @Gusdor, definitiely not a hater, just mentioning what I have learned from others. Sorry if it came across the wrong way it was definitely not intended that way – NeddySpaghetti Dec 11 '14 at 09:18
1

Assuming your long running method is not async you should be able to call it inside Task.Run and await the result, or even better use TaskFactory.StartNew and pass in TaskCreationOptions.LongRunning.

busyIndicator.Visibility = Visibility.Visible;
await TaskFactory.StartNew(() => LongRunningMethod(),TaskCreationOptions.LongRunning);
busyIndicator.Visibility = Visibility.Collapsed;

On a side note, you need to be careful with Task.Yield because in some scenarios such as with WindowsForms the task scheduled on the SynchronizationContext has a higher priority than repainting the window so you can make your UI non-responsive if you repeatedly call Task.Yield, such as in a while loop.

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61