简体   繁体   中英

async WPF multiple status updates on UI thread

I want status updates from a long running method. Usually I'd use the dispatcher to post back to the UI thread, but I'm curious about using async await.

To keep it simple:

Create a window, add a button

<Button Name="ButtonWithCodeBehind" Height="25" Click="ButtonWithCodeBehindOnClick"/>

add some code behind onClick handler

private async void ButtonWithCodeBehindOnClick(object sender, RoutedEventArgs e)
{
    await Task.Factory.StartNew(() =>
    {
        ButtonWithCodeBehind.Content = "First";
        Thread.Sleep(1000);
        ButtonWithCodeBehind.Content = "Second";
        Thread.Sleep(1000);
        ButtonWithCodeBehind.Content = "Third";
    });
}

This will obviously break because ButtonWithCodeBehind.Content will be accessed on the wrong thread.

Is there a way to make this work without doing something like:

Deployment.Current.Dispatcher.BeginInvoke(()=>ButtonWithCodeBehind.Content = "Second");

The critical thing here is the long running task will be generating updates as it progresses, I can refactor the code to something like this:

private async void ButtonWithCodeBehindOnClick(object sender, RoutedEventArgs e)
{
    var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
    await Task.Factory.StartNew(() =>
    {
        Task.Factory.StartNew(() => Thread.Sleep(1000))
            .ContinueWith(t => ButtonWithCodeBehind.Content = "First", scheduler)
            .ContinueWith(t => Thread.Sleep(1000))
            .ContinueWith(t => ButtonWithCodeBehind.Content = "Second", scheduler)
            .ContinueWith(t => Thread.Sleep(1000))
            .ContinueWith(t => ButtonWithCodeBehind.Content = "Third", scheduler);
    });
}

But this is fugly. Also, if you took out the async and await keywords and replaced them with Task.WaitAll, it would still execute as expected.

Note: If you're wondering why I'm using Thread.Sleep instead of Task.Delay, I'm actually testing this in Silverlight as well and the async await support doesn't include .Delay (or at least not where I expect it to be).

If you can split up your long running task into two distinct long running actions (like two Thread.Sleeps in your example above) you can await each long running task on it's own. So the UI updates will be performed on the UI thread.

private async void ButtonWithCodeBehindOnClick(object sender, RoutedEventArgs e)
{
  ButtonWithCodeBehind.Content = "First";
  await Task.Run(() => Thread.Sleep(1000));
  ButtonWithCodeBehind.Content = "Second";
  await Task.Run(() => Thread.Sleep(1000));
  ButtonWithCodeBehind.Content = "Third";
}

The only part that needs to be awaited is the long-running part - the IO call or, in this instance, the CPU-bound sleep.

private async void ButtonWithCodeBehindOnClick(object sender, RoutedEventArgs e)
{
    ButtonWithCodeBehind.Content = "First";
    await Task.Factory.StartNew(() => Thread.Sleep());
    ButtonWithCodeBehind.Content = "Second";
    await Task.Factory.StartNew(() => Thread.Sleep());
    ButtonWithCodeBehind.Content = "Third";
}

Await captures the synchronization context and ensures that the rest of the method is signed up to a continuation which is run on a thread with the same context. In WPF, the UI thread handles the code ButtonWithCodeBehindOnClick and thus, by default, will be responsible for the rest of the method call after await .

You can override this default behaviour by configuring the await on the task:

 await Task.Factory.StartNew(() =>
    Thread.Sleep()).ConfigureAwait(continueOnCapturedContext: false);

However, you absolutely don't want to do this in WPF as a threadpool thread will try to update your UI.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM