WPF Command 设计思想与实现剖析

文章目录

  • [为什么要用 Command?](#为什么要用 Command?)
    • Command
    • CommandParameter
      • 常用场景分类
        • [A. 传递简单常量](#A. 传递简单常量)
        • [B. 传递控件引用 (Control Reference)](#B. 传递控件引用 (Control Reference))
        • [C. 绑定数据对象 (Data Object)](#C. 绑定数据对象 (Data Object))
  • [ICommand 接口](#ICommand 接口)
  • [RelayCommand / DelegateCommand](#RelayCommand / DelegateCommand)
    • 命令四要素
      • [I. 命令 (Command)](#I. 命令 (Command))
      • [II. 命令源 (Command Source)](#II. 命令源 (Command Source))
      • [III. 命令目标 (Command Target)](#III. 命令目标 (Command Target))
      • [IV. 命令关联 (Command Binding)](#IV. 命令关联 (Command Binding))
  • 系统命令
  • 自定义命令
    • [方法一:实现 `ICommand` 接口(MVVM 模式推荐)](#方法一:实现 ICommand 接口(MVVM 模式推荐))
      • [1. 定义一个通用的命令类 (RelayCommand)](#1. 定义一个通用的命令类 (RelayCommand))
      • [2. 在 ViewModel 中声明与实例化](#2. 在 ViewModel 中声明与实例化)
    • [方法二:自定义 `RoutedCommand` (传统 WPF 路由模式)](#方法二:自定义 RoutedCommand (传统 WPF 路由模式))
      • [1. 定义静态命令类](#1. 定义静态命令类)
      • [2. 在 XAML 中配置命令关联 (CommandBinding)](#2. 在 XAML 中配置命令关联 (CommandBinding))
      • 两种方式如何选择
  • 命令绑定
    • [1. 命令绑定的三要素](#1. 命令绑定的三要素)
    • [2. 逻辑架构与路由过程](#2. 逻辑架构与路由过程)

在 WPF/Avalonia 的 MVVM 开发模式中,命令 (Command) 是解耦界面(View)与逻辑(ViewModel)的核心机制。它替代了传统的事件处理(Event Handler),实现了"业务逻辑不依赖 UI 控件"的目标。

为什么要用 Command?

  • 传统方式 (Event) :在 XAML 后台写 Button_Click。这导致业务逻辑与特定的 UI 控件强耦合,难以进行单元测试。
  • 命令方式 (Command):按钮只负责发送一个"执行指令",至于"怎么执行"由 ViewModel 里的 Command 对象决定。

Command

WPF本身就为我们提供了一个基础的MVVM框架,本节要讲的命令就是其中一环,通过在ViewModel中声明命令,从View中使用Binding绑定命令,就能实现从View到ViewModel之间操作的流通。

(1)命令command:要执行的动作。

(2)命令源command source:发出命令的对象(继承自ICommandSource)。

(3)命令目标command target:执行命令的主体

(4)命令绑定command binding:映射命令逻辑的对象

CommandParameter

命令参数 (CommandParameter) 充当了指令的"载体"。如果说命令(Command)是"做什么",那么命令参数就是"针对谁做"或"带着什么数据做"。

  • 本质 :它是一个 object 类型的属性,允许你将数据从 View 传递到 ViewModel(或命令执行函数)中。
  • 核心职责:解决"一个命令处理多个对象"的问题。例如:一个删除命令,需要知道具体删除哪一行数据。

常用场景分类

A. 传递简单常量

用于区分同一个命令的不同行为。例如,一个计算器有多个数字按钮,共用一个命令,通过参数区分数字。

xml 复制代码
<Button Content="1" Command="{Binding NumClickCommand}" CommandParameter="1" />
<Button Content="2" Command="{Binding NumClickCommand}" CommandParameter="2" />
B. 传递控件引用 (Control Reference)

将一个控件对象作为参数传给另一个控件的命令。

xml 复制代码
<TextBox x:Name="InputBox" />
<Button Content="清除文本"
        Command="{Binding ClearCommand}"
        CommandParameter="{Binding ElementName=InputBox}" />
C. 绑定数据对象 (Data Object)

ListBoxDataGrid 中,将当前行代表的数据模型(Model)传给 ViewModel。

xml 复制代码
<Button Content="删除"
        Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"
        CommandParameter="{Binding}" />

ICommand 接口

所有命令的本质都实现了 .NETSystem.Windows.Input.ICommand 接口。它包含三个关键成员:

  1. Execute(object parameter):定义命令执行的具体逻辑。
  2. CanExecute(object parameter) :返回一个 bool 值,决定命令当前是否可用。如果返回 false,绑定的按钮会自动变灰(禁用)。
  3. CanExecuteChanged :当执行条件发生变化时触发的事件,通知 UI 重新调用 CanExecute

Command 就是对函数的一种封装。只是在调用这个函数前,必须进行一个可执行判定。比如,在使用滴滴打车。

执行参数:打车距离

执行函数:将乘客送到目的地。

可执行条件:打车距离必须大于5公里,才会有司机接单。

csharp 复制代码
public interface ICommand
{
    /// <summary>
    /// 用于通知使用者,当前命令的可执行条件发生变化。
    ///需要使用者主动调用 CanExecute 判定是否可执行命令。
    /// </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);
}

常规模式下,比如button事件在后台代码里只能实现一次函数调用,MVVM做法是视图和功能函数分开,在 WPF 中 ICommand 可以在视图元素的事件触发后,执行调用。

所有的命令都是需要继承ICommand接口,该接口有如下三个成员:

csharp 复制代码
 class CustomCommand : ICommand
{
        //当能不能做发生变化时会触发的事件(必须要实现)
        public event EventHandler CanExecuteChanged;
        public void Execute(object param)  //做什么(必须要实现)
        {
            ExecuteAction?.Invoke(param);
        }
        public bool CanExecute(object param)  //能做吗(必须要实现)
        {
            if (CanExecuteAction != null)
                return CanExecuteAction(param);
            return false;
        }
        public Action<object> ExecuteAction { get; set; }
        public Func<object, bool> CanExecuteAction { get; set; }
    }

CanExecute

第二个成员,它是个返回值为bool的方法,通过这个方法,可以设置命令能不能继续执行,即返回值为TRUE,命令继续执行,返回值为FALSE命令不会执行;也就是说,在相关的命令从CanExecute中返回False的时候,按钮将变得不可用。

CanExecuteChanged

第一个成员是个事件处理器,从名字可以看出来该事件处理器关注于第二个成员,也就是当命令能否执行的状态出现改变时可以使用此事件通知到关注此命令执行状态的成员;

Execute

第三个成员也是个方法,命令的执行逻辑放在这个方法里边,当CanExecute返回值为TRUE时,该方法才会被执行。是命令的关键,当被调用时,它将触发命令的执行,当命令状态改变时,会触发CanExecuteChanged事件

然后把这个命令类放到viewmodel里使用

csharp 复制代码
//先实例化这个命令(这是属于ViewModel的命令,等下要被送到View中去)
public CustomCommand MyCommand { get; set; }
public void DoSomething(object param){
    //这个命令真正要做的事情
}
public bool CanDoSomething(object param){
    return true;  //判断能否做这个事情,大部分时候返回true就行了
}
public MyViewModel(){
    //在ViewModel的构造函数中,完成对命令的设置
    MyCommand = new CustomCommand();
    MyCommand.ExecuteAction = new Action<object>(this.DoSomething);
    MyCommand.CanExecuteAction = new Func<object, bool>(this.CanDoSomething);
}

RelayCommand / DelegateCommand

WPF 原生只提供了 RoutedCommand(主要用于系统内置命令,如复制粘贴)。在 MVVM 中,我们通常自定义一个通用的 RelayCommand 来包装逻辑。

命令类型 来源 特点
RoutedCommand WPF 内置 基于路由事件,适合"全系统"快捷键(如 Ctrl+C)。
RelayCommand 社区/框架 (CommunityToolkit.Mvvm) MVVM 最常用。将逻辑委托给 ViewModel 的方法。
AsyncRelayCommand 现代框架 专门用于 await/async 异步操作,防止界面假死。

用于通知使用者,当前命令的可执行条件发生变化。需要使用者主动调用 CanExecute

判定是否可执行命令 event EventHandler CanExecuteChanged;

判定是否可执行命令 bool CanExecute(object parameter);

执行命令 void Execute(object parameter);

设计的初衷就是为了解耦

命令四要素

I. 命令 (Command)

本质: 一个"待办事项"的声明。

  • 它不包含具体代码,只是一张"入场券"。
  • RoutedCommand (路由命令) :WPF 特色,它具有"冒泡"特性。即命令源发出指令后,指令会沿着视觉树向上寻找能够处理它的 CommandBinding

II. 命令源 (Command Source)

本质: 指令的"发射器"。

  • 只要实现了 ICommandSource 接口(如 Button, MenuItem, KeyBinding),就具备了三个关键属性:Command(发什么)、CommandParameter(带什么参数)、CommandTarget(发给谁)。

III. 命令目标 (Command Target)

本质: 指令的"接收站"。

  • 必须实现 IInputElement(几乎所有 UI 控件都实现了)。
  • 重要细节 :如果 CommandTarget 为空,命令会自动发给当前的 焦点元素 (Focused Element) 。这就是为什么点击菜单栏的"剪切"能作用于当前选中的 TextBox

IV. 命令关联 (Command Binding)

本质: 指令的"执行逻辑"。

  • 它是命令系统的"大脑"。它负责把"抽象的命令"和"具体的 C# 后台方法"挂钩。
  • 它包含两个核心事件:CanExecute(能不能做)和 Executed(怎么做)。

系统命令

WPF 预定义了大量的标准命令,目的是为了统一交互体验 。例如,无论你在哪个软件按 Ctrl+C,对应的都是 ApplicationCommands.Copy

组名 核心职责
ApplicationCommands 基础程序操作 (Open, Save, Print)
ComponentCommands 组件级移动 (MoveUp, Scroll)
NavigationCommands 导航跳转 (Back, Forward)
MediaCommands 多媒体控制 (Play, Stop)
EditingCommands 文本/内容编辑 (Bold, Undo)
  • ApplicationCommands提供一组标准的与应用程序相关的命令,包含Open、Close、Delete、Cut等。
  • ComponentCommands提供一组标准的与组件相关的命令,这些命令具有预定义的按键输入笔势和 RoutedUICommand.Text 属性。包含MoveLeft、MoveRight、MoveUp等。
  • NavigationCommands提供一组标准的与导航相关的命令,包括BrowseHome、BrowseStop、BrowseStop等。
  • MediaCommands提供一组标准的与媒体相关的命令,包括Play、Pause、Stop等。
  • EditingCommands提供一组标准的与编辑相关的命令,包括AlignCenter、Backspace、Delete等。

自定义命令

在 WPF 开发中,自定义命令主要有两种主流方式:一种是通过 RoutedCommand 定义系统级路由命令 ,另一种是通过实现 ICommand 接口定义业务级关联命令

方法一:实现 ICommand 接口(MVVM 模式推荐)

这种方式的本质是创建一个"逻辑包装器",将业务逻辑直接注入到命令对象中。

1. 定义一个通用的命令类 (RelayCommand)

为了避免给每个动作都写一个类,我们通常写一个通用的委托类:

csharp 复制代码
public 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) => _execute(parameter);

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }
}

2. 在 ViewModel 中声明与实例化

csharp 复制代码
public class MyViewModel
{
    public ICommand SaveCommand { get; }

    public MyViewModel()
    {
        // 绑定具体的执行逻辑和判断逻辑
        SaveCommand = new RelayCommand(ExecuteSave, CanSave);
    }

    private void ExecuteSave(object param) { /* 存盘逻辑 */ }
    private bool CanSave(object param) => true;
}

方法二:自定义 RoutedCommand (传统 WPF 路由模式)

这种方式利用了 WPF 的事件冒泡机制,适合在控件树中传播命令。

1. 定义静态命令类

csharp 复制代码
public static class MyProjectCommands
{
    public static readonly RoutedUICommand ClearAll = new RoutedUICommand(
        "清除所有", "ClearAll", typeof(MyProjectCommands),
        new InputGestureCollection { new KeyGesture(Key.L, ModifierKeys.Control) }
    );
}

2. 在 XAML 中配置命令关联 (CommandBinding)

由于路由命令本身不包含逻辑,你需要在窗体或控件中建立关联:

xml 复制代码
<Window.CommandBindings>
    <CommandBinding Command="local:MyProjectCommands.ClearAll"
                    CanExecute="ClearAll_CanExecute"
                    Executed="ClearAll_Executed" />
</Window.CommandBindings>

<Button Command="local:MyProjectCommands.ClearAll" Content="清空" />

两种方式如何选择

特性 ICommand 实现 (RelayCommand) RoutedCommand (路由命令)
逻辑位置 放在 ViewModel 中 放在 View (Code-behind) 中
耦合度 低(完全解耦,易于单元测试) 高(依赖视觉树查找 Binding)
快捷键支持 需要手动通过 KeyBinding 关联 自动支持 InputGestures
主要用途 MVVM 业务开发 (推荐) 开发通用控件库、处理系统级快捷键

命令绑定

命令绑定 (CommandBinding) 是将"抽象指令"转化为"具体动作"的转换站。它负责在 XAML 视图层和 C# 逻辑层之间建立一条通道。

1. 命令绑定的三要素

一个完整的 CommandBinding 对象包含以下关键属性:

  • Command (指令) :要关联的命令对象(如 ApplicationCommands.Open)。
  • CanExecute (能否执行):挂接一个事件处理程序,用于判断当前命令是否可用(决定按钮是否变灰)。
  • Executed (执行动作):挂接具体的业务逻辑代码。

2. 逻辑架构与路由过程

CommandBinding 通常被放置在容器(如 WindowGrid)的 CommandBindings 集合中。由于 WPF 使用的是路由命令 (RoutedCommand),它会沿着视觉树向上寻找匹配的绑定。

相关推荐
Aevget2 小时前
DevExpress WPF中文教程:Data Grid - 服务器模式和即时反馈模式
.net·wpf·界面控件·devexpress·ui开发
EnCi Zheng2 小时前
P1B-Python环境配置基础完全指南-Windows系统安装与验证
开发语言·windows·python
小陈phd2 小时前
多模态大模型学习笔记(十九)——基于 LangChain+Faiss的本地知识库问答系统实战
开发语言·c#
yue0082 小时前
C#读取App.Config配置文件
开发语言·c#
武藤一雄2 小时前
WPF 资源解析:StaticResource & DynamicResource 实战指南
微软·c#·.net·wpf·.netcore
c#上位机2 小时前
wpf路径
wpf
武藤一雄2 小时前
WPF UI 开发深度指南:资源 (Resources)、样式 (Style) 与触发器 (Trigger) 全解析
ui·c#·.net·wpf·.netcore·avalonia
蓝天星空2 小时前
C# .net闭源与Java开源框架的对比
java·c#·.net
John_ToDebug2 小时前
WaitableEvent 跨线程等待的死锁陷阱
windows·笔记·死锁