WPF Command 设计思想与实现剖析
文章目录
- 为什么要用 Command?
- Command
- CommandParameter
- 常用场景分类
- A. 传递简单常量
- B. 传递控件引用 (Control Reference)
- C. 绑定数据对象 (Data Object)
- ICommand 接口
- CanExecute
- CanExecuteChanged
- Execute
- RelayCommand / DelegateCommand
- 命令四要素
- I. 命令 (Command)
- II. 命令源 (Command Source)
- III. 命令目标 (Command Target)
- IV. 命令关联 (Command Binding)
- 系统命令
- 自定义命令
- 方法一:实现 `ICommand` 接口(MVVM 模式推荐)
- 1. 定义一个通用的命令类 (RelayCommand)
- 2. 在 ViewModel 中声明与实例化
- 方法二:自定义 `RoutedCommand` (传统 WPF 路由模式)
- 1. 定义静态命令类
- 2. 在 XAML 中配置命令关联 (CommandBinding)
- 两种方式如何选择
- 命令绑定
- 1. 命令绑定的三要素
- 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. 传递简单常量
用于区分同一个命令的不同行为。例如,一个计算器有多个数字按钮,共用一个命令,通过参数区分数字。
<ButtonContent="1"Command="{Binding NumClickCommand}"CommandParameter="1"/><ButtonContent="2"Command="{Binding NumClickCommand}"CommandParameter="2"/>B. 传递控件引用 (Control Reference)
将一个控件对象作为参数传给另一个控件的命令。
<TextBoxx:Name="InputBox"/><ButtonContent="清除文本"Command="{Binding ClearCommand}"CommandParameter="{Binding ElementName=InputBox}"/>C. 绑定数据对象 (Data Object)
在ListBox或DataGrid中,将当前行代表的数据模型(Model)传给 ViewModel。
<ButtonContent="删除"Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window}}"CommandParameter="{Binding}"/>ICommand 接口
所有命令的本质都实现了.NET的System.Windows.Input.ICommand接口。它包含三个关键成员:
- Execute(object parameter):定义命令执行的具体逻辑。
- CanExecute(object parameter):返回一个
bool值,决定命令当前是否可用。如果返回false,绑定的按钮会自动变灰(禁用)。 - CanExecuteChanged:当执行条件发生变化时触发的事件,通知 UI 重新调用
CanExecute。
Command 就是对函数的一种封装。只是在调用这个函数前,必须进行一个可执行判定。比如,在使用滴滴打车。
执行参数:打车距离
执行函数:将乘客送到目的地。
可执行条件:打车距离必须大于5公里,才会有司机接单。
publicinterfaceICommand{/// <summary>/// 用于通知使用者,当前命令的可执行条件发生变化。///需要使用者主动调用 CanExecute 判定是否可执行命令。/// </summary>eventEventHandlerCanExecuteChanged;/// <summary>/// 用于判定是否可执行命令/// </summary>/// <param name="parameter">命令参数</param>/// <returns></returns>boolCanExecute(objectparameter);/// <summary>/// 执行命令/// </summary>/// <param name="parameter">命令参数</param>voidExecute(objectparameter);}常规模式下,比如button事件在后台代码里只能实现一次函数调用,MVVM做法是视图和功能函数分开,在 WPF 中 ICommand 可以在视图元素的事件触发后,执行调用。
所有的命令都是需要继承ICommand接口,该接口有如下三个成员:
classCustomCommand:ICommand{//当能不能做发生变化时会触发的事件(必须要实现)publiceventEventHandlerCanExecuteChanged;publicvoidExecute(objectparam)//做什么(必须要实现){ExecuteAction?.Invoke(param);}publicboolCanExecute(objectparam)//能做吗(必须要实现){if(CanExecuteAction!=null)returnCanExecuteAction(param);returnfalse;}publicAction<object>ExecuteAction{get;set;}publicFunc<object,bool>CanExecuteAction{get;set;}}CanExecute
第二个成员,它是个返回值为bool的方法,通过这个方法,可以设置命令能不能继续执行,即返回值为TRUE,命令继续执行,返回值为FALSE命令不会执行;也就是说,在相关的命令从CanExecute中返回False的时候,按钮将变得不可用。
CanExecuteChanged
第一个成员是个事件处理器,从名字可以看出来该事件处理器关注于第二个成员,也就是当命令能否执行的状态出现改变时可以使用此事件通知到关注此命令执行状态的成员;
Execute
第三个成员也是个方法,命令的执行逻辑放在这个方法里边,当CanExecute返回值为TRUE时,该方法才会被执行。是命令的关键,当被调用时,它将触发命令的执行,当命令状态改变时,会触发CanExecuteChanged事件
然后把这个命令类放到viewmodel里使用
//先实例化这个命令(这是属于ViewModel的命令,等下要被送到View中去)publicCustomCommandMyCommand{get;set;}publicvoidDoSomething(objectparam){//这个命令真正要做的事情}publicboolCanDoSomething(objectparam){returntrue;//判断能否做这个事情,大部分时候返回true就行了}publicMyViewModel(){//在ViewModel的构造函数中,完成对命令的设置MyCommand=newCustomCommand();MyCommand.ExecuteAction=newAction<object>(this.DoSomething);MyCommand.CanExecuteAction=newFunc<object,bool>(this.CanDoSomething);}RelayCommand / DelegateCommand
WPF 原生只提供了RoutedCommand(主要用于系统内置命令,如复制粘贴)。在 MVVM 中,我们通常自定义一个通用的RelayCommand来包装逻辑。
| 命令类型 | 来源 | 特点 |
|---|---|---|
| RoutedCommand | WPF 内置 | 基于路由事件,适合“全系统”快捷键(如 Ctrl+C)。 |
| RelayCommand | 社区/框架 (CommunityToolkit.Mvvm) | MVVM 最常用。将逻辑委托给 ViewModel 的方法。 |
| AsyncRelayCommand | 现代框架 | 专门用于 await/async 异步操作,防止界面假死。 |
用于通知使用者,当前命令的可执行条件发生变化。需要使用者主动调用 CanExecute
判定是否可执行命令event EventHandlerCanExecuteChanged;
判定是否可执行命令boolCanExecute(object parameter);
执行命令voidExecute(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)
为了避免给每个动作都写一个类,我们通常写一个通用的委托类:
publicclassRelayCommand:ICommand{privatereadonlyAction<object>_execute;privatereadonlyPredicate<object>_canExecute;publicRelayCommand(Action<object>execute,Predicate<object>canExecute=null){_execute=execute??thrownewArgumentNullException(nameof(execute));_canExecute=canExecute;}publicboolCanExecute(objectparameter)=>_canExecute?.Invoke(parameter)??true;publicvoidExecute(objectparameter)=>_execute(parameter);publiceventEventHandlerCanExecuteChanged{add{CommandManager.RequerySuggested+=value;}remove{CommandManager.RequerySuggested-=value;}}}2. 在 ViewModel 中声明与实例化
publicclassMyViewModel{publicICommandSaveCommand{get;}publicMyViewModel(){// 绑定具体的执行逻辑和判断逻辑SaveCommand=newRelayCommand(ExecuteSave,CanSave);}privatevoidExecuteSave(objectparam){/* 存盘逻辑 */}privateboolCanSave(objectparam)=>true;}方法二:自定义RoutedCommand(传统 WPF 路由模式)
这种方式利用了 WPF 的事件冒泡机制,适合在控件树中传播命令。
1. 定义静态命令类
publicstaticclassMyProjectCommands{publicstaticreadonlyRoutedUICommandClearAll=newRoutedUICommand("清除所有","ClearAll",typeof(MyProjectCommands),newInputGestureCollection{newKeyGesture(Key.L,ModifierKeys.Control)});}2. 在 XAML 中配置命令关联 (CommandBinding)
由于路由命令本身不包含逻辑,你需要在窗体或控件中建立关联:
<Window.CommandBindings><CommandBindingCommand="local:MyProjectCommands.ClearAll"CanExecute="ClearAll_CanExecute"Executed="ClearAll_Executed"/></Window.CommandBindings><ButtonCommand="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通常被放置在容器(如Window或Grid)的CommandBindings集合中。由于 WPF 使用的是路由命令 (RoutedCommand),它会沿着视觉树向上寻找匹配的绑定。
