文章目录
-
- [1. 命令系统概述](#1. 命令系统概述)
- [2. ICommand 接口详解](#2. ICommand 接口详解)
- [3. WPF 内置命令库](#3. WPF 内置命令库)
- [4. RoutedCommand 与 RoutedUICommand](#4. RoutedCommand 与 RoutedUICommand)
-
- [4.1 RoutedCommand](#4.1 RoutedCommand)
- [4.2 RoutedUICommand](#4.2 RoutedUICommand)
- [5. 命令绑定(CommandBinding)](#5. 命令绑定(CommandBinding))
-
- [示例:为自定义窗口绑定 `Save` 命令](#示例:为自定义窗口绑定
Save命令)
- [示例:为自定义窗口绑定 `Save` 命令](#示例:为自定义窗口绑定
- [6. 命令源(CommandSource)与命令目标](#6. 命令源(CommandSource)与命令目标)
-
- [6.1 常见命令源](#6.1 常见命令源)
- [6.2 命令目标示例](#6.2 命令目标示例)
- [7. 自定义命令的多种实现方式](#7. 自定义命令的多种实现方式)
-
- [7.1 简单委托命令(RelayCommand / DelegateCommand)](#7.1 简单委托命令(RelayCommand / DelegateCommand))
- [7.2 支持参数的命令](#7.2 支持参数的命令)
- [7.3 异步命令(AsyncCommand)](#7.3 异步命令(AsyncCommand))
- [7.4 基于路由的自定义 RoutedCommand](#7.4 基于路由的自定义 RoutedCommand)
- [8. 完整例程:文本编辑器命令演示](#8. 完整例程:文本编辑器命令演示)
-
- [8.1 项目结构](#8.1 项目结构)
- [8.2 完整代码](#8.2 完整代码)
- [8.3 运行效果](#8.3 运行效果)
- [9. 性能与最佳实践](#9. 性能与最佳实践)
-
- [9.1 性能注意事项](#9.1 性能注意事项)
- [9.2 最佳实践清单](#9.2 最佳实践清单)
- [9.3 调试技巧](#9.3 调试技巧)
- [10. 总结与速查表](#10. 总结与速查表)
-
- [10.1 命令类型对比](#10.1 命令类型对比)
- [10.2 核心 API 速查](#10.2 核心 API 速查)
- [10.3 常用命令源属性](#10.3 常用命令源属性)
- [10.4 核心原则](#10.4 核心原则)
1. 命令系统概述
WPF 命令系统的目标:
- 解耦:UI 控件(按钮、菜单)不直接包含业务逻辑,而是调用命令。
- 统一启用/禁用 :命令的
CanExecute方法控制关联控件的IsEnabled状态。 - 可复用 :相同的命令(如
ApplicationCommands.Copy)可在不同控件间共享。 - 支持路由 :
RoutedCommand可以在元素树中寻找合适的命令绑定。
典型工作流程:
- 命令源 (如
Button)触发命令(单击时调用Execute)。 - 命令 (实现
ICommand)决定是否可以执行(CanExecute)以及执行逻辑(Execute)。 - 命令绑定 (
CommandBinding)将命令与具体处理逻辑关联,通常放在窗口或控件上。 - 命令目标 (可选)是命令操作的对象(如
TextBox接收粘贴命令)。
2. ICommand 接口详解
ICommand 位于 System.Windows.Input 命名空间,定义如下:
csharp
public interface ICommand
{
// 当 CanExecute 结果发生变化时触发,命令源会刷新其 IsEnabled 状态
event EventHandler CanExecuteChanged;
// 确定命令在当前状态下是否可执行
bool CanExecute(object parameter);
// 执行命令的逻辑
void Execute(object parameter);
}
CanExecuteChanged:通常通过CommandManager.RequerySuggested事件自动触发(如焦点变化、用户交互时),也可手动调用CommandManager.InvalidateRequerySuggested()强制刷新。
3. WPF 内置命令库
WPF 提供了多个内置静态命令类,涵盖常见应用程序操作:
| 类 | 命令示例 | 用途 |
|---|---|---|
ApplicationCommands |
New, Open, Save, SaveAs, Print, Undo, Redo, Cut, Copy, Paste, Delete, Stop, CancelPrint |
通用应用程序命令 |
EditingCommands |
AlignLeft, AlignRight, Bold, Italic, Underline, IncreaseFontSize, DecreaseFontSize, ToggleBullets |
文本编辑(RichTextBox)命令 |
NavigationCommands |
BrowseHome, BrowseBack, BrowseForward, Refresh, ZoomIn, ZoomOut |
导航命令 |
MediaCommands |
Play, Pause, Stop, Rewind, FastForward, IncreaseVolume, DecreaseVolume |
媒体播放命令 |
ComponentCommands |
MoveLeft, MoveRight, SelectAll, ExtendSelectionLeft |
组件导航命令 |
示例:使用内置命令
xml
<StackPanel>
<Button Command="ApplicationCommands.Copy" Content="复制"/>
<Button Command="ApplicationCommands.Paste" Content="粘贴"/>
<TextBox x:Name="txtEditor" Width="200" Height="100"/>
</StackPanel>
当 TextBox 获得焦点且选中文本时,"复制"按钮自动启用;当剪贴板有文本时,"粘贴"按钮自动启用。无需任何代码。
4. RoutedCommand 与 RoutedUICommand
4.1 RoutedCommand
RoutedCommand 是 WPF 内置命令的基类,它实现了 ICommand,并增加了路由 能力。命令的执行和可用性检查会在元素树中向上(冒泡)寻找 CommandBinding。
特点:
- 命令本身不包含执行逻辑,逻辑由
CommandBinding提供。 - 适合需要在多个不同控件或窗口中复用的命令。
csharp
public class RoutedCommand : ICommand
{
public string Name { get; }
public Type OwnerType { get; }
public bool CanExecute(object parameter, IInputElement target);
public void Execute(object parameter, IInputElement target);
}
4.2 RoutedUICommand
RoutedUICommand 继承自 RoutedCommand,添加了 Text 属性,用于在 UI 中显示命令的文本(如菜单项标题)。
csharp
public class RoutedUICommand : RoutedCommand
{
public string Text { get; set; }
}
内置命令如 ApplicationCommands.Copy 实际上是 RoutedUICommand 实例。
5. 命令绑定(CommandBinding)
CommandBinding 将 RoutedCommand 与具体的执行和校验逻辑关联起来。通常添加到窗口或自定义控件的 CommandBindings 集合中。
主要属性:
Command:要绑定的命令对象。Executed:命令执行时触发的事件处理程序。CanExecute:查询命令是否可执行时触发的事件处理程序。
示例:为自定义窗口绑定 Save 命令
XAML:
xml
<Window x:Class="CommandDemo.MainWindow"
Title="命令绑定演示" Height="200" Width="300">
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
Executed="Save_Executed"
CanExecute="Save_CanExecute"/>
</Window.CommandBindings>
<StackPanel>
<Button Command="ApplicationCommands.Save" Content="保存"/>
<TextBox x:Name="txtContent" Text="输入一些内容..." Margin="5"/>
</StackPanel>
</Window>
C#:
csharp
private void Save_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
// 只有文本框有内容时保存按钮才可用
e.CanExecute = !string.IsNullOrWhiteSpace(txtContent.Text);
}
private void Save_Executed(object sender, ExecutedRoutedEventArgs e)
{
MessageBox.Show($"保存内容: {txtContent.Text}");
}
6. 命令源(CommandSource)与命令目标
6.1 常见命令源
任何继承自 ICommandSource 的控件都可以作为命令源,包括:
Button,RadioButton,CheckBoxMenuItem,HyperlinkButtonBase派生类
ICommandSource 接口:
csharp
public interface ICommandSource
{
ICommand Command { get; set; }
object CommandParameter { get; set; }
IInputElement CommandTarget { get; set; }
}
Command:要调用的命令对象。CommandParameter:传递给命令的Execute和CanExecute的参数。CommandTarget:指定命令操作的目标元素(若未设置,则以获得焦点的元素为目标)。
6.2 命令目标示例
xml
<StackPanel>
<Button Command="ApplicationCommands.Copy" CommandTarget="{Binding ElementName=myTextBox}" Content="复制"/>
<TextBox x:Name="myTextBox" Text="Hello World"/>
</StackPanel>
7. 自定义命令的多种实现方式
7.1 简单委托命令(RelayCommand / DelegateCommand)
这是 MVVM 框架中最常用的实现,无需依赖路由。适合视图模型(ViewModel)中的命令。
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)
{
return _canExecute == null || _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
// 手动刷新命令状态
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}
使用示例(ViewModel)
csharp
public class MainViewModel : INotifyPropertyChanged
{
private string _inputText;
public string InputText
{
get => _inputText;
set { _inputText = value; OnPropertyChanged(); SubmitCommand.RaiseCanExecuteChanged(); }
}
public ICommand SubmitCommand { get; }
public MainViewModel()
{
SubmitCommand = new RelayCommand(
execute: _ => Submit(),
canExecute: _ => !string.IsNullOrWhiteSpace(InputText)
);
}
private void Submit()
{
MessageBox.Show($"提交: {InputText}");
}
// INotifyPropertyChanged 实现略
}
XAML 绑定:
xml
<TextBox Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"/>
<Button Command="{Binding SubmitCommand}" Content="提交"/>
7.2 支持参数的命令
RelayCommand 已经支持参数(Execute 和 CanExecute 均接收 object parameter)。在 XAML 中通过 CommandParameter 传递。
xml
<Button Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedItem}" Content="删除"/>
7.3 异步命令(AsyncCommand)
适用于长时间操作,自动管理"执行中"状态,防止重复执行。
csharp
public class AsyncCommand : ICommand
{
private readonly Func<object, Task> _execute;
private readonly Predicate<object> _canExecute;
private bool _isExecuting;
public AsyncCommand(Func<object, Task> execute, Predicate<object> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute == null || _canExecute(parameter));
}
public async void Execute(object parameter)
{
if (!CanExecute(parameter)) return;
_isExecuting = true;
RaiseCanExecuteChanged();
try
{
await _execute(parameter);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
使用:
csharp
public ICommand LoadDataCommand { get; }
LoadDataCommand = new AsyncCommand(async _ =>
{
await Task.Delay(2000);
// 加载数据
});
7.4 基于路由的自定义 RoutedCommand
当需要命令在元素树中路由(例如从子控件冒泡到父窗口)时,使用 RoutedCommand。
csharp
public static class CustomCommands
{
public static readonly RoutedUICommand RefreshData = new RoutedUICommand(
"刷新数据", // Text
"RefreshData", // Name
typeof(CustomCommands)
);
}
XAML 绑定:
xml
<Window x:Class="MyApp.MainWindow"
xmlns:local="clr-namespace:MyApp">
<Window.CommandBindings>
<CommandBinding Command="local:CustomCommands.RefreshData"
Executed="RefreshData_Executed"
CanExecute="RefreshData_CanExecute"/>
</Window.CommandBindings>
<Button Command="local:CustomCommands.RefreshData" Content="刷新"/>
</Window>
8. 完整例程:文本编辑器命令演示
本示例创建一个简单的文本编辑器,演示:
- 内置命令(
Cut,Copy,Paste,Undo,Redo)与TextBox自动交互。 - 自定义
SaveCommand(使用RelayCommand和AsyncCommand两种方式)。 - 自定义
ClearCommand(使用RoutedUICommand+ 命令绑定)。 - 命令参数传递。
8.1 项目结构
MainWindow.xaml-- 界面布局MainWindow.xaml.cs-- 后台代码(或使用 MVVM 模式,为简洁本示例使用 code-behind 配合 ViewModel)RelayCommand.cs-- 委托命令实现AsyncCommand.cs-- 异步命令实现
8.2 完整代码
RelayCommand.cs
csharp
using System;
using System.Windows.Input;
namespace CommandEditorDemo
{
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 == null || _canExecute(parameter);
public void Execute(object parameter) => _execute(parameter);
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
}
}
AsyncCommand.cs
csharp
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace CommandEditorDemo
{
public class AsyncCommand : ICommand
{
private readonly Func<object, Task> _execute;
private readonly Predicate<object> _canExecute;
private bool _isExecuting;
public AsyncCommand(Func<object, Task> execute, Predicate<object> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute == null || _canExecute(parameter));
}
public async void Execute(object parameter)
{
if (!CanExecute(parameter)) return;
_isExecuting = true;
RaiseCanExecuteChanged();
try
{
await _execute(parameter);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
MainWindow.xaml
xml
<Window x:Class="CommandEditorDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CommandEditorDemo"
Title="命令系统演示 - 文本编辑器" Height="450" Width="600">
<Window.CommandBindings>
<!-- 绑定自定义路由命令 ClearCommand -->
<CommandBinding Command="local:CustomCommands.ClearCommand"
Executed="ClearCommand_Executed"
CanExecute="ClearCommand_CanExecute"/>
</Window.CommandBindings>
<DockPanel>
<!-- 工具栏 -->
<ToolBar DockPanel.Dock="Top">
<Button Command="ApplicationCommands.Cut" Content="剪切" ToolTip="Ctrl+X"/>
<Button Command="ApplicationCommands.Copy" Content="复制" ToolTip="Ctrl+C"/>
<Button Command="ApplicationCommands.Paste" Content="粘贴" ToolTip="Ctrl+V"/>
<Separator/>
<Button Command="ApplicationCommands.Undo" Content="撤销" ToolTip="Ctrl+Z"/>
<Button Command="ApplicationCommands.Redo" Content="重做" ToolTip="Ctrl+Y"/>
<Separator/>
<Button Command="local:CustomCommands.ClearCommand" Content="清除" ToolTip="清空文本"/>
<Button Command="{Binding SaveCommand}" Content="保存(同步)" ToolTip="模拟保存"/>
<Button Command="{Binding SaveAsyncCommand}" Content="保存(异步)" ToolTip="异步保存,禁用按钮直到完成"/>
</ToolBar>
<!-- 文本编辑区 -->
<TextBox x:Name="txtEditor" AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
FontSize="14" Margin="5"/>
</DockPanel>
</Window>
CustomCommands.cs(自定义路由命令)
csharp
using System.Windows.Input;
namespace CommandEditorDemo
{
public static class CustomCommands
{
public static readonly RoutedUICommand ClearCommand = new RoutedUICommand(
"清除文本", "ClearCommand", typeof(CustomCommands)
);
}
}
MainWindow.xaml.cs(后台逻辑)
csharp
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace CommandEditorDemo
{
public partial class MainWindow : Window
{
// ViewModel 属性(实际可分离,这里为简洁放在 Window 中)
public ICommand SaveCommand { get; }
public ICommand SaveAsyncCommand { get; }
public MainWindow()
{
InitializeComponent();
// 初始化命令
SaveCommand = new RelayCommand(
execute: _ => SaveDocument(),
canExecute: _ => !string.IsNullOrWhiteSpace(txtEditor?.Text)
);
SaveAsyncCommand = new AsyncCommand(
execute: async _ => await SaveDocumentAsync(),
canExecute: _ => !string.IsNullOrWhiteSpace(txtEditor?.Text)
);
// 设置 DataContext 以便绑定
this.DataContext = this;
// 监听文本变化以刷新命令状态
txtEditor.TextChanged += (s, e) =>
{
((RelayCommand)SaveCommand).RaiseCanExecuteChanged();
((AsyncCommand)SaveAsyncCommand).RaiseCanExecuteChanged();
CommandManager.InvalidateRequerySuggested(); // 刷新 ClearCommand 等路由命令
};
}
// 同步保存模拟
private void SaveDocument()
{
MessageBox.Show($"保存文档:\n{txtEditor.Text}", "同步保存", MessageBoxButton.OK, MessageBoxImage.Information);
}
// 异步保存模拟
private async Task SaveDocumentAsync()
{
await Task.Delay(2000); // 模拟保存耗时操作
MessageBox.Show($"异步保存完成:\n{txtEditor.Text}", "异步保存", MessageBoxButton.OK, MessageBoxImage.Information);
}
// ClearCommand 执行逻辑
private void ClearCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
txtEditor.Clear();
}
// ClearCommand 可用性逻辑
private void ClearCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = !string.IsNullOrWhiteSpace(txtEditor?.Text);
}
}
}
8.3 运行效果
- "剪切/复制/粘贴/撤销/重做" 按钮自动与
TextBox交互,无需任何代码。 - "清除"按钮使用自定义路由命令,仅当文本框有内容时启用。
- "保存(同步)" 和 "保存(异步)" 使用委托命令,实时监听文本框内容变化刷新启用状态;异步命令在执行期间自动禁用按钮。
9. 性能与最佳实践
9.1 性能注意事项
- 避免在
CanExecute中执行复杂逻辑 :CanExecute会被频繁调用(如焦点切换、输入时)。应保持轻量,或缓存结果。 - 使用
CommandManager.InvalidateRequerySuggested()谨慎 :频繁调用会导致全局命令重新查询,可能影响性能。推荐依赖CommandManager.RequerySuggested自动触发。 - 异步命令中正确处理
CanExecute状态:防止执行中重复触发。
9.2 最佳实践清单
- 优先使用内置命令 (如
ApplicationCommands)以节省开发时间并获得标准行为。 - 在 MVVM 中使用
RelayCommand,将逻辑放在 ViewModel 中,保持 View 的 XAML 简洁。 - 为长时间操作实现
AsyncCommand,并显示进度指示器。 - 使用
CommandParameter传递数据,避免为每个数据项创建单独命令。 - 当命令需要在多个不相干的控件间复用时,考虑
RoutedCommand+CommandBinding。 - 实现
CanExecuteChanged时,优先使用CommandManager.RequerySuggested而非手动触发,除非需要精确控制。 - 避免在命令中直接操作 UI 元素,应通过数据绑定和 ViewModel 属性。
9.3 调试技巧
- 在
CanExecute和Execute中添加断点或日志。 - 使用
PresentationTraceSources跟踪命令路由(适用于RoutedCommand)。 - 检查
CommandBinding是否正确添加到父元素的CommandBindings集合中。
10. 总结与速查表
10.1 命令类型对比
| 命令类型 | 特点 | 适用场景 | 示例 |
|---|---|---|---|
内置命令 (ApplicationCommands 等) |
系统预定义,自动支持路由和标准 UI 行为 | 通用操作(复制/粘贴/保存) | ApplicationCommands.Copy |
| RoutedCommand / RoutedUICommand | 通过元素树路由,需要 CommandBinding | 跨控件、跨窗口的应用程序命令 | 自定义 RefreshCommand |
| RelayCommand (委托命令) | 轻量级,不依赖路由,直接在 ViewModel 中实现逻辑 | MVVM 模式中的视图模型命令 | 提交表单、加载数据 |
| AsyncCommand | 支持异步操作,自动管理执行状态 | 异步加载、上传、保存等耗时操作 | SaveAsyncCommand |
10.2 核心 API 速查
| API | 用途 |
|---|---|
ICommand |
命令的根接口 |
RoutedCommand |
支持路由的抽象命令 |
RoutedUICommand |
增加 Text 属性的路由命令 |
CommandBinding |
将路由命令与处理逻辑关联 |
CommandManager |
管理命令状态和重新查询 |
ICommandSource |
命令源控件实现的接口 |
10.3 常用命令源属性
| 属性 | 说明 |
|---|---|
Command |
绑定命令对象 |
CommandParameter |
传递给命令的参数 |
CommandTarget |
显式指定命令操作的目标元素 |
10.4 核心原则
- 命令将"意图"与"实现"分离,提高代码可维护性和可测试性。
- 内置命令能处理大多数标准场景,几乎不需要写代码。
- 在 MVVM 中,
RelayCommand是最常用的命令实现。 - 异步命令需要正确处理
CanExecute状态,防止并发执行。 - 路由命令适合全局操作 ,通过
CommandBinding在窗口级别集中处理。
通过掌握 WPF 命令系统,你可以构建出响应式、可扩展且 UI 与逻辑分离的应用程序。结合依赖属性、路由事件和 MVVM 模式,WPF 开发将变得高效而优雅。