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 的混合使用图谱:
-
await Task.Delay/HttpClient(IO):- 不需要
Task.Run。 - 不需要
Dispatcher(await 帮你自动切回)。
- 不需要
-
await Task.Run(CPU):- 大括号外面(await 之后):不需要
Dispatcher(await 帮你自动切回)。 - 大括号里面(Task.Run 内部):必须用
Dispatcher才能更 UI。
- 大括号外面(await 之后):不需要
-
最佳实践:
- 如果不喜欢到处写
Dispatcher.Invoke,可以使用IProgress<T>来优雅地解耦。
- 如果不喜欢到处写