异步 WPF UI 线程上的多个状态更新

本文关键字:状态 更新 WPF UI 线程 异步 | 更新日期: 2023-09-27 18:33:10

我希望从长时间运行的方法更新状态。通常我会使用调度程序回发到 UI 线程,但我对使用 async await 感到好奇。

为简单起见:

创建窗口,添加按钮

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

在 onClick 处理程序后面添加一些代码

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";
    });
}

这显然会中断,因为ButtonWithCodeBehind.Content将在错误的线程上访问。

有没有办法在不做这样的事情的情况下完成这项工作:

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

这里的关键是长时间运行的任务将随着进度生成更新,我可以将代码重构为如下所示的内容:

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);
    });
}

但这是愚蠢的。此外,如果您删除了 async 和 await 关键字并将其替换为 Task.WaitAll,它仍将按预期执行。

注意:如果你想知道为什么我使用Thread.Sleep而不是Task.Delay,我实际上也在Silverlight中测试它,并且异步await支持不包括。延迟(或者至少不是我期望的地方(。

异步 WPF UI 线程上的多个状态更新

如果可以将长时间运行的任务拆分为两个不同的长时间运行操作(如上面示例中的两个 Thread.Sleeps(,则可以单独等待每个长时间运行的任务。因此,UI 更新将在 UI 线程上执行。

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";
}

唯一需要等待的部分是长时间运行的部分 - IO 调用,或者在这种情况下,CPU 绑定的睡眠。

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 捕获同步上下文,并确保方法的其余部分注册到在具有相同上下文的线程上运行的延续。在 WPF 中,UI 线程处理代码ButtonWithCodeBehindOnClick,因此,默认情况下,将负责 await 之后方法调用的其余部分。

您可以通过在任务上配置 await 来覆盖此默认行为:

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

但是,您绝对不希望在 WPF 中执行此操作,因为线程池线程将尝试更新您的 UI。