跳到主要内容

6.1 异步、取消与 UI 线程

学习目标​

你将让耗时操作保持界面响应,并为成功、失败、取消三条路径恢复状态。前提:已读 Toolkit 的异步命令,理解 C# 的 Task 与 await。本节片段使用实战命名空间 TaskDesk.Client,API 契约为 TodoDto(int Id, string Title, bool IsCompleted)。

与 ASP.NET Core 的不同​

ASP.NET Core 通常没有请求线程的 UI 同步上下文;WPF 的界面对象则由创建它们的 Dispatcher 线程管理。按钮命令从 UI 线程开始,普通 await 默认捕获当前上下文,等待网络完成后通常回到 UI 线程。await 不表示“开一个后台线程”,它表示等待尚未完成的操作,并让调用线程有机会继续做其他工作。

HTTP、文件等已有异步 API 的 I/O 通常直接 await,没有必要再用 Task.Run 包起来。大量图片处理、计算等 CPU 工作可以用 Task.Run,但后台计算不能读写真实控件或随意修改绑定集合。.Result、.Wait() 和在 UI 线程长时间计算会让窗口卡住,阻塞异步结果还可能死锁。

取消贯穿整个调用链​

下面是已有 partial ViewModel 内的独立演示成员。前提:类继承 ObservableObject;构造函数已注入并保存 HttpClient 为 _httpClient,其 BaseAddress 为 http://localhost:5080/;已定义上述 DTO。需要引入 System、System.Collections.ObjectModel、System.Net.Http.Json、System.Threading、System.Threading.Tasks、TaskDesk.Client.Models 以及 Toolkit 两个命名空间。实战通过 ITodoService 封装 HTTP。

MainViewModel.cs:异步加载成员片段
public ObservableCollection<TodoDto> Items { get; } = new();

[ObservableProperty]
private bool isBusy;

[ObservableProperty]
private string errorMessage = "";

[RelayCommand(IncludeCancelCommand = true)]
private async Task RefreshAsync(CancellationToken cancellationToken)
{
IsBusy = true;
ErrorMessage = "";
try
{
var result = await _httpClient.GetFromJsonAsync<TodoDto[]>(
"api/todos", cancellationToken);
cancellationToken.ThrowIfCancellationRequested();

// 命令从 UI 线程进入,以上 await 保留上下文。
Items.Clear();
foreach (var item in result ?? Array.Empty<TodoDto>())
Items.Add(item);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
ErrorMessage = "已取消加载";
}
catch (OperationCanceledException)
{
ErrorMessage = "请求超时,请重试";
}
catch (Exception)
{
ErrorMessage = "加载失败,请检查服务是否已启动";
// 实际应用同时记录异常详情,HTTP 层区分状态码。
}
finally
{
IsBusy = false;
}
}

XAML 绑定 RefreshCommand 和 RefreshCancelCommand。令牌不仅传给命令,还要一路传给应用服务、HTTP 发送与响应读取;只在外层声明令牌却未传下去,取消按钮不会中断等待。取消是合作式请求,服务器也可能已提交写操作;取消后需重新查询确认服务端状态。

明确切回界面线程​

服务内部可以按自身约定使用 ConfigureAwait(false);它不负责更新界面。ViewModel 等待服务时仍可捕获自己的 UI 上下文。若入口来自后台订阅、工作线程或显式取消了上下文捕获,需要 Dispatcher。下面是后台入口中的片段;result 是已经取得的 DTO 数组,_dispatcher 是构造时从 UI 线程注入保存的 System.Windows.Threading.Dispatcher。

后台回调:集合更新片段
await _dispatcher.InvokeAsync(() =>
{
Items.Clear();
foreach (var item in result)
Items.Add(item);
});

不要在工作线程调用 Dispatcher.CurrentDispatcher 来寻找窗口线程,那可能创建或取得工作线程自己的 Dispatcher。页面离开和窗口关闭时,也要取消仍属于该页面的工作,避免完成回调继续更新过期状态。允许多个查询并发时,还需解决旧请求晚完成覆盖新结果的问题,可取消前次请求或检查请求版本。

常见错误​

把所有异常都显示成“已取消”会隐藏超时和网络故障;本例先检查是否是用户令牌导致取消。遗漏 finally 会留下永久忙碌状态。后台批量添加行不仅可能异常,也会制造大量通知,应先在后台准备结果,再一次性安排 UI 更新;绑定集合与其集合视图都应遵守线程规则。

练习与验收​

在服务层临时增加可取消的 5 秒延迟,期间移动窗口并点击取消。验收:窗口持续响应;取消后不替换已有列表;停掉 API 后显示失败提示;再次启动 API 后还能刷新。所有路径都恢复忙碌状态,且没有通过 .Result 或关闭线程检查来掩盖问题。

官方资料​

下一节:依赖注入与配置。