写在前面
版本说明:基于 .NET 8 LTS + CommunityToolkit.Mvvm 8.2+。
承接上一篇数据绑定。绑定解决了"显示同步",但用户点击按钮、敲键盘这些"操作"怎么处理?传统做法是写 Button.Click += Handler——但这把 UI 和业务逻辑焊死了。WPF 给的答案是命令(Command)。
本篇聚焦命令系统本身和 CommunityToolkit.Mvvm 的源生成器——下一章把 MVVM 落到完整工程实践。
本文要回答:
ICommand 接口在干嘛?为什么 [ObservableProperty] 一个特性就能替代 30 行 INPC 样板代码?源生成器到底生成了什么?
一、为什么不用事件而用命令
1.1 事件的问题
1
2
3
4
5
6
7
| // 传统事件
button.Click += OnButtonClick;
private void OnButtonClick(object sender, RoutedEventArgs e)
{
// 业务逻辑
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 问题:
1. UI 和逻辑耦合
→ 这段代码必须在 Window 类里(事件源是 button)
→ 不能放到独立的 ViewModel
2. 无法"自动禁用"
→ 业务规则"未输入用户名时不能提交"
→ 要手写 button.IsEnabled = !string.IsNullOrEmpty(...)
→ 规则一变要改多处
3. 难以复用
→ 同一个"保存"操作,可能在按钮、菜单、快捷键触发
→ 事件要分别挂
4. 难以测试
→ 单元测试要 mock Window,复杂
后端类比:
事件 = WinForms 思路(UI 直接挂逻辑)
命令 = MVC 的 Controller Action(UI 调用业务命令)
|
1.2 命令的解决思路
1
2
3
4
5
| // ViewModel 暴露命令
public ICommand SaveCommand { get; }
// XAML 绑定
<Button Content="保存" Command="{Binding SaveCommand}"/>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| 好处:
1. UI 和逻辑解耦
→ ViewModel 提供 ICommand
→ View 绑定即可
→ 同一个命令绑定到按钮、菜单、快捷键都行
2. 自动启用/禁用
→ ICommand.CanExecute 决定按钮是否可用
→ CanExecute 变化自动通知 UI
3. 可测试
→ 单元测试直接调 SaveCommand.Execute()
→ 不需要 UI
|
二、ICommand 接口
2.1 接口定义
1
2
3
4
5
6
| public interface ICommand
{
void Execute(object parameter); // 执行操作
bool CanExecute(object parameter); // 是否可执行
event EventHandler CanExecuteChanged; // 可执行状态变化
}
|
2.2 三个成员的语义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| Execute(parameter):
→ 真正执行业务逻辑
→ UI 触发命令时调用
→ parameter 来自 CommandParameter(XAML 可设)
CanExecute(parameter):
→ 返回 true/false
→ true:UI 控件启用;false:UI 控件灰掉
→ 频繁调用(每次 CanExecuteChanged 都会查)
CanExecuteChanged:
→ 事件,告诉 UI"快重新查 CanExecute"
→ 触发后 UI 调 CanExecute
→ 通常转订阅 CommandManager.RequerySuggested(自动触发)
|
2.3 一个简单的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
// 关键:转订阅 CommandManager
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
|
2.4 使用
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class MainViewModel
{
public string UserName { get; set; }
public ICommand SaveCommand { get; }
public MainViewModel()
{
SaveCommand = new RelayCommand(
execute: () => MessageBox.Show($"Saving {UserName}"),
canExecute: () => !string.IsNullOrEmpty(UserName));
}
}
|
1
2
3
| <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="保存" Command="{Binding SaveCommand}"/>
<!-- UserName 为空时按钮自动灰掉 -->
|
三、CommandManager.RequerySuggested
这是 ICommand 的"自动重新评估"机制。
3.1 它在干什么
1
2
3
4
5
6
7
8
9
10
| CommandManager.RequerySuggested 是一个全局事件
WPF 在某些时机自动触发它:
- 用户输入(按键、点击)
- 焦点变化
- 鼠标移动
- 鼠标空闲(每隔一段时间)
触发后:
- 所有订阅 CanExecuteChanged 的命令重新调 CanExecute
- UI 根据返回值刷新控件状态
|
3.2 优缺点
1
2
3
4
5
6
7
8
9
10
11
12
| 优点:
- 写 ViewModel 不用关心"何时通知 CanExecute"
- 系统自动重新评估
缺点:
- 性能:全局事件,大量命令时频繁调用 CanExecute
- 不精确:CanExecute 实际只依赖某几个属性,但每次都查
- 延迟:依赖"空闲时刻",不是属性变化的瞬间
CommunityToolkit.Mvvm 的 RelayCommand 不用 CommandManager
→ 用显式 NotifyCanExecuteChangedFor
→ 更精确,但要求 ViewModel 主动通知
|
四、RoutedCommand vs RelayCommand
WPF 内建一种 RoutedCommand,但和 MVVM 不太合。
4.1 RoutedCommand
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 定义
public static readonly RoutedCommand SaveCommand =
new RoutedCommand("Save", typeof(MyWindow));
// XAML 绑定
<Button Command="{x:Static local:MyWindow.SaveCommand}"/>
// 必须有 CommandBinding 处理
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:MyWindow.SaveCommand}"
Executed="Save_Executed"
CanExecute="Save_CanExecute"/>
</Window.CommandBindings>
private void Save_Executed(object sender, ExecutedRoutedEventArgs e) { ... }
private void Save_CanExecute(object sender, CanExecuteRoutedEventArgs e)
=> e.CanExecute = !string.IsNullOrEmpty(UserName);
|
4.2 RoutedCommand 的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| 特点:
- 命令是路由事件(沿视觉树传播)
- CommandBinding 必须挂在视觉树某节点上
- 处理逻辑在 code-behind(不在 ViewModel)
为什么和 MVVM 不合:
→ 命令逻辑要写在 View(code-behind)
→ ViewModel 不能直接处理命令
→ 等于又把 UI 和逻辑耦合了
适用场景:
- ApplicationCommands.Copy/Cut/Paste(系统命令)
- NavigationCommands(导航)
- 这些"控件级"命令,RoutedCommand 更合适
业务命令:
- 用 RelayCommand(或 CommunityToolkit 的 [RelayCommand])
- 命令在 ViewModel 里,可测试
|
4.3 对比
| 维度 | RoutedCommand | RelayCommand |
|---|
| 命令来源 | 静态定义 | ViewModel 实例属性 |
| 处理位置 | CommandBinding(视觉树) | ViewModel |
| 是否路由 | 是(沿树) | 否 |
| CanExecute | CommandBinding 提供 | 闭包 / 方法 |
| 适合 MVVM | ❌ | ✅ |
| 内建命令 | ApplicationCommands | 无(自己写或用 Toolkit) |
微软出的现代 MVVM 库,零样板代码。
5.1 安装
1
| <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2"/>
|
5.2 三个核心组件
1
2
3
4
5
6
7
8
9
10
11
| ObservableObject:
→ 实现 INPC 的基类
→ 提供 SetProperty<T>(ref field, value) 方法
[ObservableProperty] 特性:
→ 给 private 字段加,自动生成 public 属性 + INPC
→ 字段 _name → 属性 Name
[RelayCommand] 特性:
→ 给方法加,自动生成 ICommand 属性
→ 方法 Save() → 属性 SaveCommand
|
5.3 用法对比
传统写法(35 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class MainViewModel : INotifyPropertyChanged
{
private string _userName;
public string UserName
{
get => _userName;
set
{
if (_userName != value)
{
_userName = value;
OnPropertyChanged(nameof(UserName));
OnPropertyChanged(nameof(CanSave));
}
}
}
public bool CanSave => !string.IsNullOrEmpty(UserName);
public ICommand SaveCommand { get; }
public MainViewModel()
{
SaveCommand = new RelayCommand(() => Save(), () => CanSave);
}
private void Save() { /* ... */ }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
|
1
2
3
4
5
6
7
8
9
10
| public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _userName;
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* ... */ }
public bool CanSave => !string.IsNullOrEmpty(UserName);
}
|
1
2
3
4
5
| 关键观察:
- 字段 _userName 自动变成属性 UserName(带 INPC)
- 方法 Save() 自动变成 ICommand 属性 SaveCommand
- UserName 变化时 CanSave 也会重新评估(通过 NotifyCanExecuteChangedFor)
- 必须标 partial(源生成器要补充代码)
|
六、[ObservableProperty] 源生成器原理
这是 CommunityToolkit.Mvvm 最神奇的地方。理解了原理,使用时心里就有底。
6.1 输入
1
2
3
4
5
| public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _userName;
}
|
6.2 生成的代码(简化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 自动生成的 MainViewModel.cs(实际在 obj/ 目录)
public partial class MainViewModel
{
public string UserName
{
get => _userName;
set
{
if (!EqualityComparer<string>.Default.Equals(_userName, value))
{
OnUserNameChanging(value);
OnPropertyChanging(new PropertyChangingEventArgs("UserName")); // 之前
_userName = value;
OnPropertyChanged(new PropertyChangedEventArgs("UserName")); // 之后
OnUserNameChanged(value);
}
}
}
// 可重写的 partial 方法(钩子)
partial void OnUserNameChanging(string value);
partial void OnUserNameChanged(string value);
}
|
6.3 字段命名约定
1
2
3
4
5
6
| 源生成器根据字段名生成属性名:
_userName → UserName
_userNameField → UserNameField
m_userName → UserName
去掉前缀(_ / m_ / s_)+ 首字母大写
|
6.4 partial 方法钩子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private int _age;
// 钩子:属性变化前
partial void OnAgeChanging(int value)
{
if (value < 0 || value > 150)
throw new ArgumentOutOfRangeException(nameof(value));
}
// 钩子:属性变化后
partial void OnAgeChanged(int value)
{
// 触发别的属性
OnPropertyChanged(nameof(IsAdult));
}
public bool IsAdult => Age >= 18;
}
|
1
2
3
4
5
6
7
| 钩子名约定:
On<PropertyName>Changing(T newValue)
On<PropertyName>Changed(T newValue)
On<PropertyName>Changing(T oldValue, T newValue)(可选 old)
不用写完整的 OnPropertyChanged 调用,框架已经做了
你只在钩子里写"额外副作用"
|
七、[RelayCommand] 源生成器
7.1 输入
1
2
3
4
5
| public partial class MainViewModel : ObservableObject
{
[RelayCommand]
private void Save() { /* ... */ }
}
|
7.2 生成的代码(简化)
1
2
3
4
5
6
7
8
9
10
11
| public partial class MainViewModel
{
private RelayCommand? _saveCommand;
public IRelayCommand SaveCommand
{
get => _saveCommand ??= new RelayCommand(Save);
}
// 原方法的 partial 包装(如果需要异步等)
}
|
1
2
3
4
5
| 命名约定:
方法 Save() → 属性 SaveCommand(自动加 Command 后缀)
这就是为什么 XAML 写:
<Button Command="{Binding SaveCommand}"/>
|
7.3 带 CanExecute
1
2
3
4
5
6
7
| public partial class MainViewModel : ObservableObject
{
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* ... */ }
private bool CanSave() => !string.IsNullOrEmpty(UserName);
}
|
生成:
1
| public IRelayCommand SaveCommand { get; } // 内部用 CanSave 作判据
|
7.4 异步命令
1
2
3
4
5
6
7
8
9
| public partial class MainViewModel : ObservableObject
{
[RelayCommand]
private async Task LoadDataAsync()
{
// await 异步操作
await Task.Delay(1000);
}
}
|
生成 IAsyncRelayCommand,UI 可以等执行完。
1
2
3
4
| 异步命令的特点:
→ 自动暴露 IsRunning(命令是否在执行)
→ 可绑到 UI 显示 loading
→ 自动防重入(执行中不能再次触发)
|
7.5 NotifyCanExecuteChangedFor(属性联动)
1
2
3
4
5
6
7
8
9
10
11
| public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))] // ← UserName 变了,重新评估 SaveCommand
private string _userName;
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { /* ... */ }
public bool CanSave => !string.IsNullOrEmpty(UserName);
}
|
1
2
3
4
5
6
7
8
9
| 效果:
UserName 变化 → 触发 INPC
→ 同时通知 SaveCommand.CanExecuteChanged
→ UI 重新调 CanSave → 刷新按钮启用状态
对比 CommandManager.RequerySuggested:
Toolkit 方案精确(只在 UserName 变时刷新 SaveCommand)
CommandManager 方案全局(每次空闲刷新所有命令)
→ Toolkit 性能更好,但要求显式声明依赖
|
八、ObservableObject 基类
8.1 三个基类
1
2
3
4
5
6
7
| ObservableObject(CommunityToolkit.Mvvm):
→ 实现 INotifyPropertyChanged + INotifyPropertyChanging
→ 提供 SetProperty<T>(ref field, value)
BindableBase(Prism):类似
ViewModelBase(自己写):手撸 INPC
|
8.2 SetProperty 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging
{
public event PropertyChangedEventHandler? PropertyChanged;
public event PropertyChangingEventHandler? PropertyChanging;
protected bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
return false;
OnPropertyChanging(propertyName);
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
}
|
8.3 不用 [ObservableProperty] 的写法
1
2
3
4
5
6
7
8
9
| public partial class MainViewModel : ObservableObject
{
private string _userName;
public string UserName
{
get => _userName;
set => SetProperty(ref _userName, value);
}
}
|
1
2
3
4
5
6
7
8
| 何时不用 [ObservableProperty]:
→ 字段已有复杂逻辑(自定义 setter)
→ 命名冲突
→ 编译器版本不支持 source generator
何时用:
→ 绝大多数场景(90%+)
→ 样板代码消失,更易读
|
九、命令参数
9.1 CommandParameter
1
2
3
4
| <!-- 传参给命令 -->
<Button Content="删除"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedItem}"/>
|
1
2
3
4
5
6
| [RelayCommand]
private void Delete(ItemModel item)
{
// item 来自 CommandParameter
if (item != null) Items.Remove(item);
}
|
9.2 生成的命令带泛型
1
2
| // 生成的命令:IRelayCommand<ItemModel>
public IRelayCommand<ItemModel> DeleteCommand { get; }
|
十、实战:可命令化的简单计算器
把命令 + Toolkit + 绑定全部用上。
10.1 ViewModel
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.ComponentModel;
namespace CalculatorDemo;
public partial class CalculatorViewModel : ObservableObject
{
[ObservableProperty]
private double _operand1;
[ObservableProperty]
private double _operand2;
[ObservableProperty]
private double _result;
[ObservableProperty]
private string _operator = "+";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(CalculateCommand))]
private bool _isCalculating;
public CalculatorViewModel()
{
CalculateCommand = new RelayCommand(Calculate, CanCalculate);
}
// 用特性版本的命令
[RelayCommand(CanExecute = nameof(CanCalculate))]
private async Task CalculateAsync()
{
IsCalculating = true;
try
{
await Task.Delay(500); // 模拟异步计算
Result = Operator switch
{
"+" => Operand1 + Operand2,
"-" => Operand1 - Operand2,
"*" => Operand1 * Operand2,
"/" => Operand2 != 0 ? Operand1 / Operand2 : 0,
_ => 0
};
}
finally
{
IsCalculating = false;
}
}
// 同步版本(简化演示)
public IRelayCommand CalculateCommand { get; }
private void Calculate()
{
Result = Operator switch
{
"+" => Operand1 + Operand2,
"-" => Operand1 - Operand2,
"*" => Operand1 * Operand2,
"/" => Operand2 != 0 ? Operand1 / Operand2 : 0,
_ => 0
};
}
private bool CanCalculate()
=> !IsCalculating && Operator != "/" || Operand2 != 0;
[RelayCommand]
private void Clear()
{
Operand1 = 0;
Operand2 = 0;
Result = 0;
}
// 依赖属性联动
partial void OnOperatorChanged(string value)
=> CalculateCommand.NotifyCanExecuteChanged();
}
|
10.2 XAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| <Window x:Class="CalculatorDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Calculator" Width="400" Height="300">
<Window.DataContext>
<local:CalculatorViewModel/>
</Window.DataContext>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
<TextBox Width="100" Text="{Binding Operand1, UpdateSourceTrigger=PropertyChanged}"/>
<ComboBox Width="50" Margin="8,0" SelectedItem="{Binding Operator}">
<ComboBoxItem Content="+"/>
<ComboBoxItem Content="-"/>
<ComboBoxItem Content="*"/>
<ComboBoxItem Content="/"/>
</ComboBox>
<TextBox Width="100" Text="{Binding Operand2, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
<TextBlock Grid.Row="1" Margin="0,8">
<Run Text="= "/><Run Text="{Binding Result, Mode=OneWay}"/>
</TextBlock>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8">
<Button Content="计算" Command="{Binding CalculateCommand}" Width="80" Margin="0,0,8,0"/>
<Button Content="清空" Command="{Binding ClearCommand}" Width="80"/>
</StackPanel>
<TextBlock Grid.Row="3" Text="{Binding IsCalculating, StringFormat=正在计算:{0}}"/>
</Grid>
</Window>
|
10.3 知识点覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ✅ [ObservableProperty] 字段 → 属性
✅ [RelayCommand] 方法 → ICommand
✅ [NotifyCanExecuteChangedFor] 属性联动命令
✅ partial OnXxxChanged 钩子
✅ CanExecute 业务规则(除数不为 0)
✅ CommandParameter 传参(隐式:通过 IsCalculating)
✅ 异步命令(CalculateAsync,async Task)
✅ 绑定 Mode=OneWay(结果只读)
✅ UpdateSourceTrigger=PropertyChanged(实时同步)
观察:
- 切换运算符为 "/" 且 Operand2=0 → 计算按钮自动灰掉
- 输入实时反映到 UI(INPC)
- 计算中 IsCalculating=true → 按钮禁用
|
十一、常见陷阱
11.1 忘了 partial
1
2
3
4
5
6
7
8
9
| // ❌ 不加 partial,源生成器无法补充
public class MainViewModel : ObservableObject
{
[ObservableProperty] private string _name;
}
// 编译报错:找不到属性 Name
// ✅
public partial class MainViewModel : ObservableObject { ... }
|
11.2 字段命名不规范
1
2
3
4
5
6
7
| // ❌ 字段没下划线前缀
[ObservableProperty]
private string name; // 生成的属性可能也是 name(冲突)
// ✅
[ObservableProperty]
private string _name;
|
11.3 命令名字冲突
1
2
3
4
5
6
7
| // ❌ 方法已叫 SaveCommand
[RelayCommand]
private void SaveCommand() { } // 生成属性也叫 SaveCommand → 冲突
// ✅ 方法叫 Save
[RelayCommand]
private void Save() { } // 生成属性 SaveCommand
|
11.4 CanExecute 不通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // ❌ UserName 变了但 SaveCommand 不知道
[ObservableProperty]
private string _userName;
[RelayCommand(CanExecute = nameof(CanSave))]
private void Save() { }
public bool CanSave => !string.IsNullOrEmpty(UserName);
// → UserName 改变,按钮启用状态不刷新
// ✅ 加 NotifyCanExecuteChangedFor
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private string _userName;
|
11.5 异步命令没 await
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ❌ fire-and-forget
[RelayCommand]
private async Task LoadAsync()
{
LoadData(); // 没 await,立即返回,IsRunning=false
}
// ✅
[RelayCommand]
private async Task LoadAsync()
{
await LoadDataAsync();
}
|
十二、与 ASP.NET Core 的对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ASP.NET Core WPF Command
──────────────────────────────────────────────
Controller Action ICommand.Execute
[HttpPost] 路由 Command 绑定到 Button
Authorization Filter ICommand.CanExecute
[FromBody] 参数 CommandParameter
MediatR Command ICommand(思路一致)
MediatR Handler Command 内部逻辑
后端类比小结:
ICommand ≈ MediatR 的 IRequest
RelayCommand.Execute ≈ Handler.Handle
CanExecute ≈ Authorization Policy
CommandParameter ≈ Request DTO
|
十三、小结
本文聚焦命令系统本身:
- 事件 vs 命令的本质差别(耦合度、可测试性)
- ICommand 三成员(Execute / CanExecute / CanExecuteChanged)
- CommandManager.RequerySuggested 的角色
- RoutedCommand vs RelayCommand 的选型
- CommunityToolkit.Mvvm 三件套(ObservableObject / [ObservableProperty] / [RelayCommand])
- [ObservableProperty] 源生成器原理(生成属性 + INPC + partial 钩子)
- [RelayCommand] 源生成器原理(生成 ICommand + 自动命名)
- NotifyCanExecuteChangedFor 的精确通知
- 异步命令与 IsRunning
- 实战:可命令化的计算器
1
2
3
4
| 记住三句话:
1. ICommand 是"业务操作的抽象"——Execute 是动作,CanExecute 是规则
2. [ObservableProperty] / [RelayCommand] 是源生成器——零样板但要知道生成了什么
3. CommunityToolkit 用 NotifyCanExecuteChangedFor 替代 CommandManager——更精确但要求显式声明
|
下一篇将进入 MVVM 工程化——把命令、绑定、DP 落到完整项目结构,引入 Microsoft.Extensions.DependencyInjection,搭建 TodoApp 完整工程。