关于 mvvm-toolkit
mvvm-toolkit 是为使用 CommunityToolkit.Mvvm 8.x 的应用开发者准备的技能,涵盖 ViewModel、属性、命令和验证。它说明源生成器的用法、基类选择及常见问题。
主要功能
- 01用特性生成可观察属性
- 02生成同步与异步命令
- 03支持属性与命令通知
- 04涵盖表单数据验证
- 05提供诊断与常见问题指导
适用场景
- 01为 ViewModel 添加可观察属性
- 02为操作创建同步或异步命令
- 03构建带输入验证的表单
- 04检查 CommunityToolkit.Mvvm 实现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ github/ awesome-copilot --skill mvvm-toolkit安装后在 Claude Code 中输入/mvvm-toolkit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/end-to-end-walkthrough.mdrelaycommand-cookbook.mdsource-generators.mdtroubleshooting.mdvalidation.md详细介绍
CommunityToolkit.Mvvm(核心)
在使用 CommunityToolkit.Mvvm 8.x 的应用中编写或审查 ViewModel、属性、命令或验证时,请使用此技能。
配套技能。 加载
mvvm-toolkit-messenger以了解IMessenger发布/订阅模式。加载mvvm-toolkit-di以了解Microsoft.Extensions.DependencyInjection集成。
快速回顾。 在
partial类的私有字段上使用[ObservableProperty];在实例方法上使用[RelayCommand];继承ObservableObject(或对输入表单继承ObservableValidator,在使用IMessenger时继承ObservableRecipient)。
包与设置
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
</ItemGroup>
目标框架:netstandard2.0、netstandard2.1、net6.0+。可在 .NET、.NET Framework、Mono 上运行。源生成器随同一个 NuGet 包提供——无需额外的分析器引用。
命名空间:
using CommunityToolkit.Mvvm.ComponentModel; // ObservableObject, [ObservableProperty]
using CommunityToolkit.Mvvm.Input; // [RelayCommand], RelayCommand, AsyncRelayCommand
通用规则。 每个使用
[ObservableProperty]或[RelayCommand]的类型——以及每个外层类型(如果是嵌套的)——都必须声明为partial。否则,生成器会报出MVVMTK0008/MVVMTK0042。
源生成器速查表
| 特性 | 应用于 | 生成内容 |
|---|---|---|
[ObservableProperty] | 私有字段 | 公共 INotifyPropertyChanged 属性 + OnXxxChanging/OnXxxChanged 分部方法钩子 |
[NotifyPropertyChangedFor(nameof(Other))] | 可观察字段 | 同时为所列属性触发 PropertyChanged |
[NotifyCanExecuteChangedFor(nameof(MyCommand))] | 可观察字段 | 在变更时调用 MyCommand.NotifyCanExecuteChanged() |
[NotifyDataErrorInfo] | ObservableValidator 上的可观察字段 | 在 setter 中调用 ValidateProperty(value) |
[NotifyPropertyChangedRecipients] | ObservableRecipient 上的可观察字段 | 变更后调用 Broadcast(old, new) |
[RelayCommand] | 实例方法 | 以 IRelayCommand / IAsyncRelayCommand 形式公开的惰性 RelayCommand / AsyncRelayCommand |
[RelayCommand(CanExecute = nameof(CanX))] | 实例方法 | 将 CanExecute 连接到方法或属性 |
[RelayCommand(IncludeCancelCommand = true)] | 带 CancellationToken 的异步方法 | 同时生成 XxxCancelCommand |
[RelayCommand(AllowConcurrentExecutions = true)] | 异步方法 | 允许排队/并行调用(默认在运行期间禁用) |
[RelayCommand(FlowExceptionsToTaskScheduler = true)] | 异步方法 | 通过 ExecutionTask 呈现异常,而不是 await 并重新抛出 |
[property: SomeAttr] | 可观察字段或 [RelayCommand] 方法 | 将 SomeAttr 转发到生成的属性上(例如 [JsonIgnore]) |
命名。 字段 name / _name / m_name → Name。方法 LoadAsync → LoadCommand(Async 后缀会被去除;前导 On 也会被去除)。
有关完整特性参考及生成代码示例,请参阅 references/source-generators.md。
ViewModel 模式
简单的可观察属性
public partial class ContactViewModel : ObservableObject
{
[ObservableProperty]
private string? name;
}
钩子:OnXxxChanging / OnXxxChanged
[ObservableProperty]
private string? name;
partial void OnNameChanged(string? value) =>
Logger.LogInformation("Name changed to {Name}", value);
单参数 (value) 和双参数 (oldValue, newValue) 重载均可用。只需实现你需要的那些;未实现的钩子会被编译器省略(零运行时开销)。
依赖属性 + 依赖命令
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? firstName;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FullName))]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? lastName;
public string FullName => $"{FirstName} {LastName}".Trim();
包装非可观察模型
public sealed class ObservableUser(User user) : ObservableObject
{
public string Name
{
get => user.Name;
set => SetProperty(user.Name, value, user, (u, n) => u.Name = n);
}
}
传递静态 lambda(不捕获状态)以保持调用无分配。
命令
[RelayCommand]
private void Refresh() => Items.Reset();
[RelayCommand]
private async Task LoadAsync()
{
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync() => repo.SaveAsync(Name!);
private bool CanSave() => !string.IsNullOrWhiteSpace(Name);
仅当你必须显式拥有命令的生命周期或从非平凡来源组合它时,才使用手动 RelayCommand / AsyncRelayCommand 构造函数。特性风格覆盖了约 95% 的情况。
有关同步 / 异步 / 可取消 / 并发 / 错误呈现的配方,请参阅 references/relaycommand-cookbook.md。
基类选择
| 基类 | 适用场景 |
|---|---|
ObservableObject | 默认选择。INotifyPropertyChanged + INotifyPropertyChanging + SetProperty 重载 + 用于 Task 属性的 SetPropertyAndNotifyOnCompletion |
ObservableValidator | VM 需要 INotifyDataErrorInfo(表单、设置输入) |
ObservableRecipient | VM 发送或接收 IMessenger 消息 — 参见 mvvm-toolkit-messenger 技能 |
C# 是单继承:ObservableValidator 和 ObservableRecipient
都继承自 ObservableObject,因此组合它们需要采用组合方式
(例如,将 IMessenger 注入到 ObservableValidator 中)。
验证
using System.ComponentModel.DataAnnotations;
public sealed partial class RegistrationViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, MinLength(2), MaxLength(100)]
private string? name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required, EmailAddress]
private string? email;
[RelayCommand]
private void Submit()
{
ValidateAllProperties();
if (HasErrors) return;
// submit...
}
}
其他入口点:TrySetProperty、ValidateProperty(value, name)、
ClearAllErrors()、GetErrors(propertyName)。自定义规则支持
[CustomValidation] 方法和自定义 ValidationAttribute 子类。
有关验证器的完整接口范围,请参见
references/validation.md。
常见陷阱
- 忘记
partial。 类(以及每一个外层类型)都必须是partial。否则会出现编译错误MVVMTK0008/MVVMTK0042。 - 字段名使用 PascalCase。
[ObservableProperty] private string Name;会与生成的属性冲突。请使用name、_name或m_name。 - 在
[RelayCommand]上使用async void。 生成器只会将 返回Task的方法包装为IAsyncRelayCommand。async void会变成 同步的RelayCommand,异常也不会被观察到。请始终返回Task。 - 忘记
[NotifyCanExecuteChangedFor]。 即使CanSave()现在会返回true,Save 按钮仍保持禁用。 - 修改
[ObservableProperty]字段持有的同一引用。EqualityComparer<T>.Default返回true,不会触发通知。 请替换该实例,而不是修改它。
有关完整的诊断表(MVVMTK0xxx)和更多陷阱,请参见
references/troubleshooting.md。
端到端迷你演练
一个双窗格 Notes 应用,演示生成器 + 命令 +
[NotifyCanExecuteChangedFor]:
public sealed partial class NoteViewModel(INotesService notes,
IMessenger messenger) : ObservableRecipient(messenger)
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
[NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
private string? filename;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSave))]
private Task SaveAsync()
{
Messenger.Send(new NoteSavedMessage(Filename!));
return notes.SaveAsync(Filename!, Text!);
}
[RelayCommand(CanExecute = nameof(CanDelete))]
private Task DeleteAsync() => notes.DeleteAsync(Filename!);
private bool CanSave() =>
!string.IsNullOrWhiteSpace(Filename) && !string.IsNullOrEmpty(Text);
private bool CanDelete() => !string.IsNullOrWhiteSpace(Filename);
}
有关完整示例(DI 接线、View 代码隐藏、XAML、单元测试),请参见
references/end-to-end-walkthrough.md。
参考资料与配套技能
| 主题 | 位置 |
|---|---|
| 源生成器特性参考 | references/source-generators.md |
| RelayCommand 配方 | references/relaycommand-cookbook.md |
| 验证深入解析 | references/validation.md |
| 完整 Notes 应用演练 | references/end-to-end-walkthrough.md |
MVVMTK0xxx 诊断与陷阱 | references/troubleshooting.md |
| Messenger 发布/订阅 | 配套技能:mvvm-toolkit-messenger |
Microsoft.Extensions.DependencyInjection 接线 | 配套技能:mvvm-toolkit-di |
外部来源: