本篇定位
上一篇分析了 ObservableObject 的属性通知机制。这次来看命令系统——RelayCommand。读完你会理解:
ICommand 的三个成员在框架内部如何落地?
- 为什么按钮有时不自动禁用/启用?
CanExecuteChanged 的两种实现策略各有什么优劣?
1. ICommand 接口回顾
1 2 3 4 5 6
| public interface ICommand { event EventHandler? CanExecuteChanged; bool CanExecute(object? parameter); void Execute(object? parameter); }
|
WPF 绑定引擎:按钮点击调用 Execute,按钮启用/禁用依赖 CanExecute。
2. 类结构
1 2 3 4 5 6
| public sealed partial class RelayCommand : IRelayCommand { private readonly Action execute; private readonly Func<bool>? canExecute; }
|
用 sealed 标记——不希望你继承。职责单一且确定,不需要扩展点。
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 CounterViewModel : ObservableObject { private int _count; public int Count { get => _count; set => SetProperty(ref _count, value); }
public RelayCommand IncrementCommand { get; }
public CounterViewModel() { IncrementCommand = new RelayCommand(Increment); }
private void Increment() { Count++; } }
|
XAML:
1 2 3 4
| <StackPanel> <TextBlock Text="{Binding Count, StringFormat=计数: {0}}" FontSize="48" /> <Button Content="+1" Command="{Binding IncrementCommand}" /> </StackPanel>
|
运行效果: 每次点击按钮,计数 +1,UI 自动刷新。
4. 示例二:带 CanExecute 的登录按钮
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
| public class LoginViewModel : ObservableObject { private string _userName = ""; private string _password = "";
public string UserName { get => _userName; set { if (SetProperty(ref _userName, value)) LoginCommand.NotifyCanExecuteChanged(); } }
public string Password { get => _password; set { if (SetProperty(ref _password, value)) LoginCommand.NotifyCanExecuteChanged(); } }
public RelayCommand LoginCommand { get; }
public LoginViewModel() { LoginCommand = new RelayCommand(Login, CanLogin); }
private void Login() { MessageBox.Show($"登录成功: {UserName}"); }
private bool CanLogin() { return !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password); } }
|
XAML:
1 2 3 4 5 6 7
| <StackPanel> <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}" Placeholder="用户名" /> <PasswordBox Password="{Binding Password, UpdateSourceTrigger=PropertyChanged}" Placeholder="密码" /> <Button Content="登录" Command="{Binding LoginCommand}" /> </StackPanel>
|
行为演示:
| 状态 |
UserName |
Password |
登录按钮 |
| 初始 |
空 |
空 |
禁用 |
| 输入用户名 |
“admin” |
空 |
禁用 |
| 输入密码 |
“admin” |
“123” |
启用 |
| 清空密码 |
“admin” |
空 |
禁用 |
源码联动: 关键在 CanExecute 的实现:
1 2 3 4 5 6 7
| [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanExecute(object? parameter) { return this.canExecute?.Invoke() != false; }
|
5. 示例三:参数命令(打开文件)
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
| public class DocumentViewModel : ObservableObject { private string _selectedFile = ""; public string SelectedFile { get => _selectedFile; set => SetProperty(ref _selectedFile, value); }
public RelayCommand<string?> OpenFileCommand { get; }
public DocumentViewModel() { OpenFileCommand = new RelayCommand<string?>(OpenFile, CanOpenFile); }
private void OpenFile(string? fileName) { SelectedFile = fileName!; MessageBox.Show($"打开文件: {fileName}"); }
private bool CanOpenFile(string? fileName) { return !string.IsNullOrEmpty(fileName) && File.Exists(fileName); } }
|
XAML:
1 2 3 4 5 6 7 8 9 10 11
| <StackPanel> <ListBox ItemsSource="{Binding Files}" SelectedItem="{Binding SelectedFile}"> <ListBox.ItemTemplate> <DataTemplate> <Button Content="{Binding}" Command="{Binding DataContext.OpenFileCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" /> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </StackPanel>
|
6. 示例四:手动刷新命令状态
RelayCommand 不会自动刷新 CanExecute。你必须手动调用 NotifyCanExecuteChanged():
1 2 3 4 5 6 7 8 9 10 11
| public string UserName { set { if (SetProperty(ref _userName, value)) { LoginCommand.NotifyCanExecuteChanged(); } } }
|
对比两种策略:
| 策略 |
优点 |
缺点 |
| 自动(CommandManager) |
省事,始终最新 |
频繁触发,性能开销 |
| 手动(NotifyCanExecuteChanged) |
精确控制 |
开发者容易忘记 |
更好的做法 — 用 Source Generators 自动生成:
1 2 3 4 5 6 7 8 9 10
| [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(LoginCommand))] private string _userName;
|
7. 源码精读
7.1 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| public RelayCommand(Action execute) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; }
public RelayCommand(Action execute, Func<bool> canExecute) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; }
|
注意:有 canExecute 参数的构造中,canExecute 也做了 null 检查(不同于早期的 MvvmLight)。
7.2 CanExecute
1 2 3 4 5
| [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanExecute(object? parameter) { return this.canExecute?.Invoke() != false; }
|
canExecute?.Invoke() != false 等价于:null 时返回 true,不为 null 时返回委托结果。
7.3 CanExecuteChanged 事件
1 2 3 4 5 6
| public event EventHandler? CanExecuteChanged;
public void NotifyCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
|
8. 调试技巧:捕捉命令绑定
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
| public class DebugRelayCommand : ICommand { private readonly ICommand _inner;
public DebugRelayCommand(ICommand inner) { _inner = inner; _inner.CanExecuteChanged += (s, e) => Debug.WriteLine($"[Command] CanExecuteChanged"); }
public event EventHandler? CanExecuteChanged { add => _inner.CanExecuteChanged += value; remove => _inner.CanExecuteChanged -= value; }
public bool CanExecute(object? p) { var result = _inner.CanExecute(p); Debug.WriteLine($"[Command] CanExecute({p}) = {result}"); return result; }
public void Execute(object? p) { Debug.WriteLine($"[Command] Execute({p})"); _inner.Execute(p); } }
|
9. 手写最小实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| public class MiniRelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool>? _canExecute;
public MiniRelayCommand(Action execute, Func<bool>? canExecute = null) { _execute = execute; _canExecute = canExecute; }
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object? parameter) => _execute();
public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
|
下一篇预告:AsyncRelayCommand 源码分析 — 异步命令的状态机实现