目录

WPF-Dispatcher的简单使用

搞清楚 async/await和Task的关系后,在WPF中肯定离不开Dispatcher的使用。核心原则:只有UI线程(主线程)才能碰UI控件

1.根本不需要Dispatcher的情况(绝大多数情况下)

误区: 用了 async/await 必须配上 Dispatcher,这是不对的

WPF 的 await 机制非常智能。它会自动记录"我是从UI线程出来的"。当 await 结束时,它会自动通过 SynchronizationContext(同步上下文) 把你送回 UI 线程。

场景 :IO操作 / 简单的异步等待

private async void Button_Click(object sender, RoutedEventArgs e)
{
     // --- UI 线程 ---
     txtLog.Text = "开始下载...";
     
    // 自动切出:去后台等待,不卡 UI
    string html = await HttpClient.GetStringAsync("http://baidu.com");

    // 自动切回: await 结束后,WPF 自动把你送回了 UI 线程
    // --- UI 线程 ---
    // 由于处于 UI 线程,这里可以直接对控件进行操作,不需要 Dispatcher
    txtLog.Text = "下载完毕,长度:" + html.Length;
    

}

2.必须使用Dispatcher的情况(混合 Task.Run)

回顾一下 Task 的用法,在“CPU密集型”标准写法 Task.Run() 时 ,Task.Run 内的代码是彻底在后台线程运行的。

如果想在后台计算的过程中(比如计算到了50%)更新一下页面,这时候后台线程必须用 Dispatcher 通知主线程。

场景:后台计算,中途实时更新UI

private async void Button_Start_Click(object sender, RoutedEventArgs e)
{
  progressBar.Value = 0;
  txtLog.Text = "开始计算...";
  
  // 启动后台线程
  await Task.Run(()=>
  {
     // 这里的每一行代码都在 后台线程 运行
     for (int i = 0; i <= 100; i++)
     {
         Thread.Sleep(50); // 模拟耗时操作
       
         // 错误:直接操作 UI 会崩!!
         // progressBar.Value = i;

         // 正确操作:混合使用 Dispatcher,切换到 UI 线程去执行
         Application.Current.Dispatcher.Invoke(() =>
         {
              // 此处的代码运行在 UI 线程 
              ProgressBar.Value = i;
              txtLog.Text = $"进度:{i}%";
         });
         // Invoke 执行完,又回到了后台线程继续 for 循环
     }
  });
  // await 结束,自动回到 UI 线程
  txtLog.Text = "算完了!";
}

3.Invoke还是InvokeAsync?

Dispatcher 提供了两个常用方法:

1.Invoke(…) 同步

  • 行为:后台线程说:“主线程大哥,帮我干个事。” 然后后台线程暂停等待,直到主线程干完这件事,后台线程才继续往下走。
  • 适用:绝大多数场景,逻辑简单直接。

2.InvokeAsync(…) 异步:

  • 行为:后台线程说:“主线程大哥,这个事你抽空办一下。” 然后后台线程不等待,直接继续跑下面的代码。
  • 适用:当你更新频率极高(比如 1 毫秒一次),或者不希望后台计算被 UI 渲染拖慢时。

写法对比:

await Task.Run(()=>
{
   // 方式 A:Invoke
   Application.Current.Dispatcher.Invoke(()=> {progressBar.Value = 50});

   // 方式 B:InvokeAsync(更高效, 但是注意它不等待 UI 更新完)
   Application.Current.Dispatcher.InvokeAsync(()=> {progressBar.Value = 50;});
});

4. 进阶:完全解耦的写法 (IProgress)

虽然用 Dispatcher 没问题,但它让你的业务代码依赖了 WPF 的对象(Application.Current...)。如果你想把计算逻辑写在单独的类库里,类库里是引用不到 Dispatcher 的。

推荐方案:使用 IProgress<T>
这是微软官方推荐的替代 Dispatcher 的做法。

后端代码 (不知道 UI 存在):

// 这个方法在 ClassLibrary 里,不引用 WPF
public void HeavyWork(IProgress<int> progress)
{
    for (int i = 0; i <= 100; i++)
    {
        Thread.Sleep(50);
        // 只管报告进度,不关心谁在听,也不关心是不是 UI 线程
        if (progress != null)
        {
            progress.Report(i); 
        }
    }
}

前端代码 (WPF):

private async void Button_Click(object sender, RoutedEventArgs e)
{
    // 定义一个 Progress 对象,告诉它:收到进度后怎么更新 UI
    // Progress<T> 内部自动封装了 Dispatcher/SynchronizationContext
    var progressIndicator = new Progress<int>(value => 
    {
        // 这里的代码自动在 UI 线程运行!
        progressBar.Value = value;
    });

    // 传入 progress 对象
    await Task.Run(() => 
    {
         var worker = new MyClassLibrary();
         worker.HeavyWork(progressIndicator);
    });
}

总结

关于 Async / Await / Task / Dispatcher 的混合使用图谱:

  1. await Task.Delay/HttpClient (IO):

    • 不需要 Task.Run
    • 不需要 Dispatcher(await 帮你自动切回)。
  2. await Task.Run (CPU):

    • 大括号外面(await 之后):不需要 Dispatcher(await 帮你自动切回)。
    • 大括号里面(Task.Run 内部):必须用 Dispatcher 才能更 UI。
  3. 最佳实践

    • 如果不喜欢到处写 Dispatcher.Invoke,可以使用 IProgress<T> 来优雅地解耦。