WPF 命令 ICommand 从原理到实战

0 阅读8分钟

在 WPF MVVM 模式下,按钮点击、菜单命令、快捷键等交互,不建议直接使用 Click 事件。更优雅、解耦、可测试的方案是 ICommand 命令模式。本文带你从零实现通用命令,并用图文示意直观理解整个流程。


一、为什么要用 ICommand?

传统事件写法:

 <Grid>
     <Button Click="Button_Click" />
 </Grid>
private void Button_Click(object sender, RoutedEventArgs e)
{
    // 逻辑写在界面后台,耦合严重
    MessageBox.Show("按钮被点击了");
}

image-20260322121925613

缺点:

  • 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);
}

image

核心成员:

  1. Execute:命令执行逻辑
  2. CanExecute:返回 true/false,控制 UI 可用状态
  3. 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>

效果:

  1. 提交按钮:输入框为空则禁用,输入内容后启用,点击弹窗显示输入内容和固定参数“hello”;
  2. 删除按钮:始终可用,点击弹窗显示固定ID(1001),参数错误则提示报错;
  3. 保存功能:按下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 执行保存逻辑

八、常见问题

  1. 按钮一直灰色 原因:CanExecute 返回 false 或未触发 CanExecuteChanged

  2. 点击没反应 原因:DataContext 未设置 / 命令为 null / CanExecute 为 false

  3. 属性变了但按钮状态不变 解决:手动调用 RaiseCanExecuteChanged()


九、总结

  • ICommand 是 WPF MVVM 标准交互方案
  • RelayCommand 是最常用通用实现
  • Execute 执行业务逻辑,CanExecute 控制可用性
  • 配合 INotifyPropertyChanged 可实现完整 MVVM
  • 解耦、可复用、易测试、支持快捷键

学会 ICommand,你的 WPF 架构将更加清晰、可维护、专业,同时对初学者也更直观明了。

十、最后

👋 关注我!持续分享 C# 实战技巧、代码示例 & 技术干货

  • 获取示例代码,轻松上手!
  • 私信输入数字: 18vm21
  • 获取代码下载链接 image