跳到主要内容

4.2 ICommand 与命令状态

学习目标​

你将把按钮点击连接到 ViewModel,并控制何时允许提交。重点是理解 ICommand 的三个成员,以及何时应主动发出 CanExecuteChanged。本节沿用 MVVM 分层 中的 WpfBasics 项目和主窗口。

命令与后端对照​

后端的应用服务方法可以表达“新增任务”;桌面按钮还需要知道“现在是否能新增”。ICommand.Execute 执行动作,CanExecute 判断当前是否允许执行,CanExecuteChanged 通知命令源重新查询许可状态。WPF 的 Button 等命令源会据此更新启用状态;不要在多个按钮上各自写一套 IsEnabled 判断。

CanExecute 应快速返回、没有副作用,不能在里面发送 HTTP 请求。它只是客户端的交互约束,不能替代服务端的权限检查和业务校验。来自键盘、代码或其他入口的调用也可能绕过按钮状态,因此关键条件应在业务入口再次检查。

用普通 C# 实现命令​

新增下列完整文件。这是无路由、无快捷键注册的轻量命令;WPF 的路由命令是另一套机制。本实现主动发事件,没有接入 CommandManager.RequerySuggested,因此不会自动感知任意属性变化。

RelayCommand.cs
using System;
using System.Windows.Input;

namespace WpfBasics;

public sealed class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;

public RelayCommand(Action<object?> execute,
Predicate<object?>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}

public bool CanExecute(object? parameter)
=> _canExecute?.Invoke(parameter) ?? true;

public void Execute(object? parameter)
{
if (CanExecute(parameter)) _execute(parameter);
}

public event EventHandler? CanExecuteChanged;

public void RaiseCanExecuteChanged()
=> CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

将上一节的 ViewModel 替换为这个完整文件。输入改变后要更新属性通知和命令通知,它们服务于不同的观察者。现有窗口构造函数仍负责设置 DataContext。

MainViewModel.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfBasics;

public sealed class MainViewModel : INotifyPropertyChanged
{
private string _title = "";
private string _status = "请输入标题";

public MainViewModel()
{
SaveCommand = new RelayCommand(
_ => Status = $"已接受:{Title.Trim()}",
_ => !string.IsNullOrWhiteSpace(Title));
}

public RelayCommand SaveCommand { get; }

public string Title
{
get => _title;
set
{
if (_title == value) return;
_title = value;
Notify();
SaveCommand.RaiseCanExecuteChanged();
}
}

public string Status
{
get => _status;
private set { _status = value; Notify(); }
}

public event PropertyChangedEventHandler? PropertyChanged;
private void Notify([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

这是窗口内容片段。由于 TextBox.Text 默认通常在失焦时更新源,显式选择每次输入就更新,按钮才能立即反映标题是否为空。

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

命令参数来自 CommandParameter,适合传递当前行或选中项。参数类型、空值和页面关闭后的对象状态仍需处理;不能仅凭“按钮曾经能点击”断定执行时数据一定有效。

常见错误​

在 setter 中仅触发 PropertyChanged,按钮可能保持旧状态,因为命令没有收到重查通知。反过来,仅触发命令通知也不会刷新普通文本绑定。把 async lambda 传给本节的 Action 会形成 async void,难以等待、取消和管理异常;实际网络命令请使用 Toolkit 的异步命令。

练习与验收​

增加标题最长 30 个字符的限制,把同一规则加入 CanExecute。验收:空白和超长标题均使按钮不可点击,恢复有效标题后立即可点击;程序主动调用 SaveCommand.Execute(null) 时,非法标题仍不会更新状态。若使用单元测试,可以直接创建 ViewModel 和调用命令,无需打开窗口。

官方资料​

下一节:CommunityToolkit.Mvvm。