跳到主要内容

4.3 CommunityToolkit.Mvvm

学习目标​

在理解手写通知和 ICommand 后,你将用工具包生成同样的行为,并能解释生成出的属性、命令和取消入口。前提:WpfBasics 使用 .NET 10 WPF;以下固定使用 CommunityToolkit.Mvvm 8.4.0。

工具包生成了什么​

后端常用代码生成器减少 DTO 或序列化代码,MVVM Toolkit 则生成属性 setter、通知和命令包装。它仍依赖相同的 INotifyPropertyChanged、ICommand 机制;使用特性不意味着状态会自行同步。包含生成成员的类必须为 partial;如果嵌套在其他类中,外层类型也需要满足生成器的分部声明要求。

本教程使用字段形式的 [ObservableProperty],避免部分属性语法对语言版本和项目配置的额外要求。字段 title 生成公开属性 Title;界面绑定属性,不绑定字段。[RelayCommand] 修饰 SaveAsync 时会去掉 Async 后缀,生成 SaveCommand;不要写成 SaveAsyncCommand。

安装与完整示例​

在项目目录执行:

安装依赖
dotnet add package CommunityToolkit.Mvvm --version 8.4.0

下面完整文件替换现有 ViewModel。命令采用模拟延迟演示异步与取消;延迟不代表数据库或 API 已保存,实战会替换为服务调用。若项目保留了上一节自写的 RelayCommand.cs,本节使用的是特性生成的命令,不必实例化那个类。

MainViewModel.cs
using System;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace WpfBasics;

public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string title = "";

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private bool isBusy;

[ObservableProperty]
private string status = "请输入标题";

private bool CanSave() => !IsBusy && !string.IsNullOrWhiteSpace(Title);

[RelayCommand(CanExecute = nameof(CanSave), IncludeCancelCommand = true)]
private async Task SaveAsync(CancellationToken cancellationToken)
{
if (!CanSave()) return;
var submittedTitle = Title.Trim();
IsBusy = true;
Status = "正在处理";
try
{
await Task.Delay(1200, cancellationToken);
Status = $"演示完成:{submittedTitle}";
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
Status = "已取消";
}
catch (Exception)
{
Status = "处理失败,请重试";
// 实际应用同时记录异常,避免只保留提示文本。
}
finally
{
IsBusy = false;
}
}
}

沿用主窗口设置 DataContext = new MainViewModel() 的构造函数,并替换窗口内容:

MainWindow.xaml:Window 内的内容
<StackPanel Margin="24">
<TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" />
<Button Margin="0,12,0,0" Content="处理" Command="{Binding SaveCommand}" />
<Button Margin="0,8,0,0" Content="取消" Command="{Binding SaveCancelCommand}" />
<TextBlock Margin="0,12,0,0" Text="{Binding Status}" />
</StackPanel>

SaveCancelCommand 来自 IncludeCancelCommand,需要可取消的异步方法签名;取消是发出请求,实际操作还必须观察传入的令牌。异步命令默认不允许自身并发执行,但其他命令并不会一起禁用;共享的 IsBusy 和各命令的条件负责跨操作约束。

状态与异常不能省略​

NotifyCanExecuteChangedFor 会在生成的属性 setter 中调用指定命令的 NotifyCanExecuteChanged()。若直接改字段 title,就绕过了通知。条件依赖服务状态或集合数量时,要在相应状态变化处主动调用命令通知,不能期待工具包扫描所有依赖。

异步命令不是异常保险箱。默认异常传播策略可能把未处理异常送到调用上下文;界面操作应在明确的边界捕获预期故障、显示结果并记录日志。FlowExceptionsToTaskScheduler 改变传播方式,也不等于完成错误处理。本节用 finally 还原忙碌状态,避免失败后按钮永久禁用。

常见错误​

漏掉 partial 会产生生成器错误;把绑定写成 title 或 SaveAsyncCommand 会找不到成员。将 Task.Run 当成所有异步方法的包装也没有必要,HTTP I/O 直接 await 即可。业务服务若包含独立的线程策略,ViewModel 更新状态时仍需遵守 UI 线程边界。

练习与验收​

把模拟延迟改为 5 秒,再增加一个受 IsBusy 控制的“清空”命令。验收:处理期间两个业务命令均不可点击,取消按钮可点击;取消后状态恢复;成功、取消和人为抛出的异常三条路径都能再次操作。检查 IDE 中生成的成员,确认属性和命令名与绑定一致。

官方资料​

下一节:页面导航与对话框。