命令是一种设计模式(命令模式,Command Pattern),用于将"请求"封装为一个对象,从而:
解耦调用者(如按钮)与执行者(如 ViewModel 中的方法)
支持统一的启用/禁用控制(CanExecute)
实现撤销(Undo)、日志、队列等高级功能
在 WPF 中,命令通过 ICommand 接口实现。
csharp
public interface ICommand
{
event EventHandler CanExecuteChanged;
bool CanExecute(object parameter);
void Execute(object parameter);
}
Execute:执行命令时调用的方法(相当于"点击后做什么")
CanExecute:决定命令是否可用(返回 true/false,影响按钮是否可点击)
CanExecuteChanged:当命令的可用状态可能改变时触发,通知 UI 刷新(如按钮自动变灰)
🌟 一、先打个比方:命令就像"遥控器"
想象你有一个电视(代表界面上的按钮),还有一个遥控器(代表命令)。
- 你按遥控器上的"开机"键(相当于点击按钮),
- 遥控器会把"开机"这个指令发给电视,
- 电视收到后就执行"打开屏幕"。
在这个过程中:
- 你不需要知道电视内部怎么工作的(解耦),
- 如果电视没插电,遥控器会自动变灰,按不了(自动禁用)。
👉 WPF 的命令就是这个"遥控器":它把"用户操作"和"实际要做的事"分开,让代码更清晰、更好维护。
🧱 二、命令的核心:ICommand 接口
在 WPF 中,所有命令都要实现一个叫 ICommand 的接口。它只有两个关键方法:
| 方法 | 作用 | 类比 |
|---|---|---|
Execute() |
真正要做的事情(比如保存文件) | 按下遥控器,电视开机 |
CanExecute() |
判断现在能不能执行(比如没填内容就不能保存) | 电视没电了,遥控器自动锁住 |
💡 还有一个事件
CanExecuteChanged,用来告诉界面:"我的可用状态变了,请刷新按钮颜色!"
🛠️ 三、自己做一个简单的命令(RelayCommand)
.NET 没有直接提供现成的命令类,所以我们通常自己写一个通用的命令类,叫 RelayCommand。
第一步:创建 RelayCommand
csharp
using System;
using System.Windows.Input;
public class RelayCommand : ICommand
{
private readonly Action _execute; // 要执行的方法
private readonly Func<bool> _canExecute; // 判断能不能执行
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
// 谁想知道我能不能用?我状态变了会通知你!
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
// 能不能执行?
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
// 执行!
public void Execute(object parameter) => _execute();
}
✅ 这个类你可以复制到任何 WPF 项目里复用!
🏗️ 四、在 ViewModel 中使用命令(MVVM 模式)
假设我们做一个"留言本":输入文字,点"保存"按钮。
1. 创建 ViewModel
csharp
public class MainViewModel
{
// 用户输入的内容
public string Message { get; set; }
// 保存命令
public ICommand SaveCommand { get; }
// 构造函数
public MainViewModel()
{
SaveCommand = new RelayCommand(
execute: OnSave, // 点击时做什么
canExecute: CanSave // 什么时候能点
);
}
// 真正的保存逻辑
private void OnSave()
{
MessageBox.Show($"保存成功:{Message}");
}
// 判断:只有输入了内容才能保存
private bool CanSave()
{
return !string.IsNullOrWhiteSpace(Message);
}
}
🖼️ 五、在 XAML 中绑定命令
xml
<Window x:Class="MyApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="留言本" Width="300" Height="150">
<StackPanel Margin="10">
<!-- 输入框,绑定到 Message -->
<TextBox Text="{Binding Message, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,10" />
<!-- 按钮,绑定到 SaveCommand -->
<Button Content="保存"
Command="{Binding SaveCommand}"
Height="30" />
</StackPanel>
</Window>
关键点解释:
Text="{Binding Message, UpdateSourceTrigger=PropertyChanged}"
→ 用户一打字,Message属性立刻更新。Command="{Binding SaveCommand}"
→ 按钮自动监听命令的CanExecute:- 如果
CanSave()返回false(比如空内容),按钮自动变灰、不可点! - 如果返回
true,按钮可点击,点击就执行OnSave()。
- 如果
✅ 完全不用写后台代码(.xaml.cs)!
🔁 六、命令 vs 传统事件(对比)
| 方式 | 传统事件 | 命令(Command) |
|---|---|---|
| 代码位置 | 写在 .xaml.cs(Code-Behind) |
写在 ViewModel(逻辑层) |
| 按钮禁用 | 手动写 button.IsEnabled = false; |
自动根据 CanExecute 控制 |
| 可测试性 | 难(依赖 UI) | 容易(纯 C# 类,可单元测试) |
| 复用性 | 低 | 高(同一个命令可用于按钮、菜单、快捷键) |
🎯 命令让界面和逻辑彻底分离,是 MVVM 的核心!
🎁 七、额外小技巧
1. 给命令传参数
比如删除某条记录,需要传 ID:
csharp
// 命令定义
public ICommand DeleteCommand { get; }
DeleteCommand = new RelayCommand<string>(id => DeleteItem(id));
// XAML
<Button Content="删除"
Command="{Binding DeleteCommand}"
CommandParameter="123" />
2. 快捷键也能用命令!
xml
<Window.InputBindings>
<KeyBinding Key="S" Modifiers="Ctrl" Command="{Binding SaveCommand}" />
</Window.InputBindings>
→ 按 Ctrl+S 就能保存,和按钮共用同一个命令!
✅ 总结:一句话记住命令
命令 = 把"按钮点击"变成"可绑定、可控制、可复用"的逻辑对象。
它让你:
- 不用写
.xaml.cs代码 - 按钮自动变灰/变亮
- 逻辑集中、易于测试
- 支持快捷键、菜单、按钮统一处理