在 WPF MVVM 模式下,按钮点击、菜单命令、快捷键等交互,不建议直接使用 Click 事件。更优雅、解耦、可测试的方案是 ICommand 命令模式。本文带你从零实现通用命令,并用图文示意直观理解整个流程。
一、为什么要用 ICommand?
传统事件写法:
<Grid>
<Button Click="Button_Click" />
</Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
// 逻辑写在界面后台,耦合严重
MessageBox.Show("按钮被点击了");
}
缺点:
- UI 与逻辑强耦合
- 逻辑无法复用
- 不方便控制按钮是否可用
- 不支持快捷键
- 不利于单元测试
ICommand 优势:
- 逻辑写在 ViewModel,UI 完全解耦
- 内置
CanExecute控制按钮可用/禁用 - 支持命令参数、快捷键、输入绑定
- 符合 MVVM 规范,方便单元测试
二、ICommand 接口解析
public interface ICommand
/// <summary>
/// 命令执行接口,定义命令的执行与状态检查规范
/// </summary>
public interface ICommand
{
/// <summary>
/// 命令可执行状态变更时触发的事件
/// </summary>
event EventHandler? CanExecuteChanged;
/// <summary>
/// 检查命令是否可执行
/// </summary>
/// <param name="parameter">命令参数</param>
/// <returns>是否可执行</returns>
bool CanExecute(object? parameter);
/// <summary>
/// 执行命令逻辑
/// </summary>
/// <param name="parameter">命令参数</param>
void Execute(object? parameter);
}
核心成员:
- Execute:命令执行逻辑
- CanExecute:返回
true/false,控制 UI 可用状态 - CanExecuteChanged:当
CanExecute状态可能改变时触发,让 UI 更新状态
三、实现通用命令 RelayCommand
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace _02.ICommand应用
{
/// <summary>
/// 主窗口视图模型(继承ViewModelBase,封装业务逻辑和命令)
/// 功能:1. 提交命令(带输入验证) 2. 删除命令(带参数) 3. 保存命令(快捷键触发)
/// </summary>
public class MainViewModel : ViewModelBase
{
#region 业务字段
/// <summary>
/// 输入框文本(用于提交命令的验证和执行)
/// 无INotifyPropertyChanged,通过UI事件手动更新值
/// </summary>
public string Input { get; set; }
/// <summary>
/// 要删除的数据ID(作为删除命令的参数)
/// 固定值示例,实际项目中可从数据库/接口获取
/// </summary>
public int Id { get; } = 1001;
#endregion
#region 命令定义
/// <summary>
/// 提交命令(带可执行判断:输入非空时可用)
/// </summary>
public RelayCommand SubmitCommand { get; }
/// <summary>
/// 删除命令(带参数:绑定Id字段)
/// </summary>
public RelayCommand DeleteCommand { get; }
/// <summary>
/// 保存命令(绑定Ctrl+S快捷键)
/// </summary>
public RelayCommand SaveCommand { get; }
#endregion
#region 构造函数
/// <summary>
/// 初始化视图模型,创建所有命令实例
/// </summary>
public MainViewModel()
{
// 初始化提交命令:传入执行逻辑和可执行判断逻辑
SubmitCommand = new RelayCommand(ExecuteSubmit, CanExecuteSubmit);
// 初始化删除命令:仅传入执行逻辑(默认CanExecute返回true)
DeleteCommand = new RelayCommand(ExecuteDelete);
// 初始化保存命令:仅传入执行逻辑(默认CanExecute返回true)
SaveCommand = new RelayCommand(ExecuteSave);
}
#endregion
#region 命令核心逻辑
/// <summary>
/// 提交命令的可执行判断逻辑
/// </summary>
/// <param name="parameter">命令参数(XAML绑定的"hello")</param>
/// <returns>true=输入非空,命令可用;false=输入为空,命令禁用</returns>
private bool CanExecuteSubmit(object parameter)
{
// 校验输入:排除null、空字符串、全空格
return !string.IsNullOrWhiteSpace(Input);
}
/// <summary>
/// 提交命令的执行逻辑
/// </summary>
/// <param name="parameter">命令参数(从XAML绑定传递)</param>
private void ExecuteSubmit(object parameter)
{
// 弹出提示框,展示输入内容和命令参数
MessageBox.Show(
$"提交成功!\n输入内容:{Input}\n命令参数:{parameter}",
"提交结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
/// <summary>
/// 删除命令的执行逻辑(带参数)
/// </summary>
/// <param name="parameter">命令参数(绑定的Id字段)</param>
private void ExecuteDelete(object parameter)
{
// 校验参数类型:确保是int类型的ID
if (parameter is int id)
{
MessageBox.Show(
$"删除操作执行成功!\n删除的ID:{id}",
"删除结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
else
{
// 参数类型错误时给出提示
MessageBox.Show(
"删除失败!参数格式错误,需要int类型的ID",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
/// <summary>
/// 保存命令的执行逻辑(快捷键触发)
/// </summary>
/// <param name="parameter">命令参数(无参数时为null)</param>
private void ExecuteSave(object parameter)
{
MessageBox.Show(
"保存操作执行成功!\n(通过Ctrl+S快捷键触发)",
"保存结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
#endregion
}
}
四、在 ViewModel 中使用命令
/// <summary>
/// 视图模型基类(无INotifyPropertyChanged,仅作为统一继承标识)
/// 作用:让所有ViewModel统一继承此类,便于后续扩展通用功能
/// </summary>
public class ViewModelBase
{
// 空基类,可根据业务需求添加通用方法(如日志、通用验证等)
}
/// <summary>
/// 主窗口视图模型(继承ViewModelBase,封装业务逻辑和命令)
/// 功能:1. 提交命令(带输入验证) 2. 删除命令(带参数) 3. 保存命令(快捷键触发)
/// </summary>
public class MainViewModel : ViewModelBase
{
#region 业务字段
/// <summary>
/// 输入框文本(用于提交命令的验证和执行)
/// 无INotifyPropertyChanged,通过UI事件手动更新值
/// </summary>
public string Input { get; set; }
/// <summary>
/// 要删除的数据ID(作为删除命令的参数)
/// 固定值示例,实际项目中可从数据库/接口获取
/// </summary>
public int Id { get; } = 1001;
#endregion
#region 命令定义
/// <summary>
/// 提交命令(带可执行判断:输入非空时可用)
/// </summary>
public RelayCommand SubmitCommand { get; }
/// <summary>
/// 删除命令(带参数:绑定Id字段)
/// </summary>
public RelayCommand DeleteCommand { get; }
/// <summary>
/// 保存命令(绑定Ctrl+S快捷键)
/// </summary>
public RelayCommand SaveCommand { get; }
#endregion
#region 构造函数
/// <summary>
/// 初始化视图模型,创建所有命令实例
/// </summary>
public MainViewModel()
{
// 初始化提交命令:传入执行逻辑和可执行判断逻辑
SubmitCommand = new RelayCommand(ExecuteSubmit, CanExecuteSubmit);
// 初始化删除命令:仅传入执行逻辑(默认CanExecute返回true)
DeleteCommand = new RelayCommand(ExecuteDelete);
// 初始化保存命令:仅传入执行逻辑(默认CanExecute返回true)
SaveCommand = new RelayCommand(ExecuteSave);
}
#endregion
#region 命令核心逻辑
/// <summary>
/// 提交命令的可执行判断逻辑
/// </summary>
/// <param name="parameter">命令参数(XAML绑定的"hello")</param>
/// <returns>true=输入非空,命令可用;false=输入为空,命令禁用</returns>
private bool CanExecuteSubmit(object parameter)
{
// 校验输入:排除null、空字符串、全空格
return !string.IsNullOrWhiteSpace(Input);
}
/// <summary>
/// 提交命令的执行逻辑
/// </summary>
/// <param name="parameter">命令参数(从XAML绑定传递)</param>
private void ExecuteSubmit(object parameter)
{
// 弹出提示框,展示输入内容和命令参数
MessageBox.Show(
$"提交成功!\n输入内容:{Input}\n命令参数:{parameter}",
"提交结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
/// <summary>
/// 删除命令的执行逻辑(带参数)
/// </summary>
/// <param name="parameter">命令参数(绑定的Id字段)</param>
private void ExecuteDelete(object parameter)
{
// 校验参数类型:确保是int类型的ID
if (parameter is int id)
{
MessageBox.Show(
$"删除操作执行成功!\n删除的ID:{id}",
"删除结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
else
{
// 参数类型错误时给出提示
MessageBox.Show(
"删除失败!参数格式错误,需要int类型的ID",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
/// <summary>
/// 保存命令的执行逻辑(快捷键触发)
/// </summary>
/// <param name="parameter">命令参数(无参数时为null)</param>
private void ExecuteSave(object parameter)
{
MessageBox.Show(
"保存操作执行成功!\n(通过Ctrl+S快捷键触发)",
"保存结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
#endregion
}
五、XAML 绑定命令
/// <summary>
/// 缓存ViewModel实例,避免重复转换DataContext
/// </summary>
private MainViewModel _viewModel;
/// <summary>
/// 窗口构造函数
/// </summary>
public MainWindow()
{
// 初始化UI组件
InitializeComponent();
// 将DataContext转换为MainViewModel并缓存
_viewModel = (MainViewModel)DataContext;
}
/// <summary>
/// 输入框文本变化事件处理方法
/// 功能:1. 更新ViewModel的Input字段 2. 刷新提交命令状态
/// </summary>
/// <param name="sender">事件源(TextBox控件)</param>
/// <param name="e">事件参数</param>
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
// 校验事件源是否为TextBox
if (sender is TextBox textBox)
{
// 1. 手动更新ViewModel的Input字段为输入框当前文本
_viewModel.Input = textBox.Text;
// 2. 手动触发提交命令的状态刷新,让WPF重新判断CanExecute
// 从而更新按钮的禁用/启用状态
_viewModel.SubmitCommand.RaiseCanExecuteChanged();
}
}
<!-- 3. 主布局 Grid -->
<Grid>
<!-- StackPanel:垂直布局,用 Margin 替代 Spacing(兼容旧版 WPF) -->
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<!-- 输入框 + 提交按钮行(水平布局,用 Margin 替代 Spacing) -->
<StackPanel Orientation="Horizontal" Margin="0 0 0 20">
<!-- TextBox:移除不兼容的 PlaceholderText,改用 ToolTip 提示 -->
<TextBox Width="200"
Margin="0 0 10 0"
ToolTip="请输入内容(为空时提交按钮禁用)"
TextChanged="TextBox_TextChanged"/>
<!-- 提交按钮 -->
<Button Content="提交"
Command="{Binding SubmitCommand}"
CommandParameter="hello"/>
</StackPanel>
<!-- 删除按钮(添加 Margin 替代 Spacing) -->
<Button Content="删除数据(带参数)"
Margin="0 0 0 20"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding Id}"/>
<!-- 快捷键提示文本 -->
<TextBlock Text="快捷键:Ctrl+S 触发保存命令" HorizontalAlignment="Center" />
</StackPanel>
</Grid>
效果:
- 提交按钮:输入框为空则禁用,输入内容后启用,点击弹窗显示输入内容和固定参数“hello”;
- 删除按钮:始终可用,点击弹窗显示固定ID(1001),参数错误则提示报错;
- 保存功能:按下Ctrl+S快捷键,弹窗提示保存成功。
六、高级用法
1. 带参数命令
<Button Content="删除数据(带参数)"
Margin="0 0 0 20"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding Id}"/>
/// 删除命令(带参数:绑定Id字段)
/// </summary>
public RelayCommand DeleteCommand { get; }
/// <summary>
/// 初始化视图模型,创建所有命令实例
/// </summary>
public MainViewModel()
{
// 初始化删除命令:仅传入执行逻辑(默认CanExecute返回true)
DeleteCommand = new RelayCommand(ExecuteDelete);
}
/// <summary>
/// 删除命令的执行逻辑(带参数)
/// </summary>
/// <param name="parameter">命令参数(绑定的Id字段)</param>
private void ExecuteDelete(object parameter)
{
// 校验参数类型:确保是int类型的ID
if (parameter is int id)
{
MessageBox.Show(
$"删除操作执行成功!\n删除的ID:{id}",
"删除结果",
MessageBoxButton.OK,
MessageBoxImage.Information
);
}
else
{
// 参数类型错误时给出提示
MessageBox.Show(
"删除失败!参数格式错误,需要int类型的ID",
"错误",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
}
2. 绑定快捷键
<Window.InputBindings>
<KeyBinding Key="S" Modifiers="Ctrl" Command="{Binding SaveCommand}" />
</Window.InputBindings>
3. 手动刷新命令状态
SubmitCommand.RaiseCanExecuteChanged();
七、图文示意:命令机制一目了然
1️⃣ 架构总览
┌───────────┐ ┌─────────────┐ ┌───────────┐
│ View │ <-----> │ ViewModel │ <-----> │ Model │
│ (XAML/UI) │ Command │ (RelayCommand) │ Data │ (业务逻辑) │
└───────────┘ └─────────────┘ └───────────┘
▲ ▲
│ │
InputBinding / Command CanExecuteChanged
(快捷键 / 按钮点击) 刷新按钮可用状态
View 只绑定命令,逻辑全在 ViewModel;Model 保存业务数据。
2️⃣ 按钮状态变化流程
[用户输入 TextBox]
│
▼
ViewModel.Input 属性改变
│
▼
SubmitCommand.RaiseCanExecuteChanged()
│
▼
CanExecute() 方法执行
│
├── true → 按钮 Enabled
└── false → 按钮 Disabled
输入为空 → 按钮灰色,输入有效 → 按钮可用
3️⃣ 命令执行流程(点击/快捷键)
[按钮点击 / Ctrl+S 快捷键]
│
▼
Command 调用 Execute(parameter)
│
▼
ViewModel 执行业务逻辑
│
▼
可选刷新按钮状态 (RaiseCanExecuteChanged)
Execute 执行逻辑,CanExecute 控制可用性,CommandParameter 可传参
4️⃣ 快捷键绑定示意(Ctrl+S 保存)
用户按 Ctrl+S
│
▼
InputBinding 捕获键盘事件
│
▼
触发 SaveCommand.Execute()
│
▼
ViewModel 执行保存逻辑
八、常见问题
-
按钮一直灰色 原因:
CanExecute返回false或未触发CanExecuteChanged -
点击没反应 原因:
DataContext未设置 / 命令为 null /CanExecute为 false -
属性变了但按钮状态不变 解决:手动调用
RaiseCanExecuteChanged()
九、总结
ICommand是 WPF MVVM 标准交互方案RelayCommand是最常用通用实现Execute执行业务逻辑,CanExecute控制可用性- 配合
INotifyPropertyChanged可实现完整 MVVM - 解耦、可复用、易测试、支持快捷键
学会 ICommand,你的 WPF 架构将更加清晰、可维护、专业,同时对初学者也更直观明了。
十、最后
👋 关注我!持续分享 C# 实战技巧、代码示例 & 技术干货
- 获取示例代码,轻松上手!
- 私信输入数字: 18vm21
- 获取代码下载链接