WPF 命令系统
大约 9 分钟约 2601 字
WPF 命令系统
简介
命令(Command)是 WPF 中替代传统事件处理的机制。它将用户操作(点击按钮、菜单选择、快捷键)与业务逻辑解耦,是实现 MVVM 模式的核心组件。命令通过 ICommand 接口实现,支持判断是否可执行(CanExecute),并能自动更新 UI 状态。
特点
ICommand 接口
/// <summary>
/// ICommand 接口定义
/// </summary>
public interface ICommand
{
// 判断命令是否可以执行 — 自动控制控件的 IsEnabled 状态
bool CanExecute(object parameter);
// 执行命令逻辑
void Execute(object parameter);
// 当 CanExecute 的结果可能发生变化时触发
event EventHandler CanExecuteChanged;
}自定义命令实现
通用 RelayCommand
/// <summary>
/// 通用命令实现 — 无参数版本
/// </summary>
public class RelayCommand : ICommand
{
private readonly Action _execute;
private readonly Func<bool> _canExecute;
public RelayCommand(Action execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;
public void Execute(object parameter) => _execute();
// 绑定到 WPF 的 CommandManager,自动评估 CanExecute
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
/// <summary>
/// 通用命令实现 — 带参数版本
/// </summary>
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool> _canExecute;
public RelayCommand(Action<T> execute, Func<T, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return _canExecute?.Invoke((T)parameter) ?? true;
}
public void Execute(object parameter)
{
_execute((T)parameter);
}
public event EventHandler CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}命令的基本使用
XAML 绑定命令
<!-- 无参数命令 -->
<Button Content="刷新" Command="{Binding RefreshCommand}" />
<!-- 带参数命令 -->
<Button Content="删除"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedUser}" />
<!-- 带多个参数(使用 MultiBinding + Converter) -->
<Button Content="移动">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource MoveParameterConverter}">
<Binding Path="SelectedUser" />
<Binding Source="Up" />
</MultiBinding>
</Button.CommandParameter>
</Button>ViewModel 中定义命令
/// <summary>
/// ViewModel 中定义和使用命令
/// </summary>
public class MainViewModel : ViewModelBase
{
// 无参数命令
public RelayCommand RefreshCommand { get; }
public RelayCommand NewCommand { get; }
// 带参数命令
public RelayCommand<User> EditCommand { get; }
public RelayCommand<int> DeleteByIdCommand { get; }
private ObservableCollection<User> _users;
public ObservableCollection<User> Users
{
get => _users;
set => SetProperty(ref _users, value);
}
private User _selectedUser;
public User SelectedUser
{
get => _selectedUser;
set => SetProperty(ref _selectedUser, value);
}
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
set => SetProperty(ref _isBusy, value);
}
public MainViewModel()
{
// 简单命令
RefreshCommand = new RelayCommand(RefreshData, () => !IsBusy);
NewCommand = new RelayCommand(CreateNewUser);
EditCommand = new RelayCommand<User>(EditUser, user => user != null);
DeleteByIdCommand = new RelayCommand<int>(DeleteUser, id => id > 0);
}
private void RefreshData()
{
IsBusy = true;
// 加载数据...
IsBusy = false;
}
private void CreateNewUser()
{
// 创建新用户...
}
private void EditUser(User user)
{
// 编辑用户...
}
private void DeleteUser(int id)
{
// 删除用户...
}
}异步命令
AsyncRelayCommand
/// <summary>
/// 异步命令 — 支持 async/await
/// 解决耗时操作阻塞 UI 的问题
/// </summary>
public class AsyncRelayCommand : ICommand
{
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async void Execute(object parameter)
{
if (!CanExecute(parameter)) return;
_isExecuting = true;
RaiseCanExecuteChanged();
try
{
await _execute();
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// 带参数的异步命令
/// </summary>
public class AsyncRelayCommand<T> : ICommand
{
private readonly Func<T, Task> _execute;
private readonly Func<T, bool> _canExecute;
private bool _isExecuting;
public AsyncRelayCommand(Func<T, Task> execute, Func<T, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return !_isExecuting && (_canExecute?.Invoke((T)parameter) ?? true);
}
public async void Execute(object parameter)
{
if (!CanExecute(parameter)) return;
_isExecuting = true;
RaiseCanExecuteChanged();
try
{
await _execute((T)parameter);
}
finally
{
_isExecuting = false;
RaiseCanExecuteChanged();
}
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}使用异步命令
public class ProductViewModel : ViewModelBase
{
private bool _isLoading;
public bool IsLoading
{
get => _isLoading;
set => SetProperty(ref _isLoading, value);
}
public AsyncRelayCommand LoadCommand { get; }
public AsyncRelayCommand<int> LoadDetailCommand { get; }
public ProductViewModel()
{
LoadCommand = new AsyncRelayCommand(LoadProductsAsync, () => !IsLoading);
LoadDetailCommand = new AsyncRelayCommand<int>(LoadDetailAsync, id => id > 0 && !IsLoading);
}
private async Task LoadProductsAsync()
{
IsLoading = true;
try
{
var products = await _productService.GetAllAsync();
Products = new ObservableCollection<Product>(products);
}
catch (Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
IsLoading = false;
}
}
private async Task LoadDetailAsync(int productId)
{
IsLoading = true;
try
{
var detail = await _productService.GetByIdAsync(productId);
SelectedProduct = detail;
}
finally
{
IsLoading = false;
}
}
}命令参数传递
方式1:CommandParameter 直接传递
<!-- 传递固定值 -->
<Button Content="跳转第1页" Command="{Binding GoToPageCommand}" CommandParameter="1" />
<Button Content="跳转第2页" Command="{Binding GoToPageCommand}" CommandParameter="2" />
<!-- 传递绑定值 -->
<Button Content="删除" Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedItem, ElementName=MyDataGrid}" />
<!-- 传递当前项(在 DataTemplate 中) -->
<Button Content="编辑" Command="{Binding DataContext.EditCommand,
RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" />方式2:使用 Converter 封装多参数
/// <summary>
/// 多参数转换器
/// </summary>
public class CommandParameterConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Clone(); // 返回参数数组
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}WPF 内置命令(RoutedCommand)
预定义命令
// WPF 内置了常用命令
ApplicationCommands.New
ApplicationCommands.Open
ApplicationCommands.Save
ApplicationCommands.Undo
ApplicationCommands.Redo
ApplicationCommands.Copy
ApplicationCommands.Paste
ApplicationCommands.Cut
ApplicationCommands.Delete
ApplicationCommands.Find
NavigationCommands.BrowseBack
NavigationCommands.BrowseForward
NavigationCommands.Refresh
MediaCommands.Play
MediaCommands.Pause
MediaCommands.Stop使用内置命令
<!-- 绑定内置命令 — 自动支持快捷键 -->
<CommandBinding Command="ApplicationCommands.Save"
Executed="Save_Executed"
CanExecute="Save_CanExecute" />
<!-- 按钮自动显示对应文本和快捷键 -->
<Button Command="ApplicationCommands.Save" />
<!-- 显示 "保存(S)" 并响应 Ctrl+S -->
<TextBox />
<Button Command="ApplicationCommands.Copy" Content="复制" />
<!-- TextBox 获得焦点且有选中文字时,Copy 命令自动可用 -->自定义 RoutedCommand
/// <summary>
/// 自定义路由命令 — 支持快捷键绑定
/// </summary>
public static class AppCommands
{
public static RoutedCommand ExportCommand { get; } = new RoutedCommand();
public static RoutedCommand SearchCommand { get; } = new RoutedCommand();
public static RoutedCommand ToggleFullScreenCommand { get; } = new RoutedCommand();
}<!-- 注册命令绑定 -->
<Window.CommandBindings>
<CommandBinding Command="{x:Static local:AppCommands.ExportCommand}"
Executed="Export_Executed" CanExecute="Export_CanExecute" />
<CommandBinding Command="{x:Static local:AppCommands.SearchCommand}"
Executed="Search_Executed" />
</Window.CommandBindings>
<!-- 绑定快捷键 -->
<Window.InputBindings>
<KeyBinding Command="{x:Static local:AppCommands.ExportCommand}"
Key="E" Modifiers="Ctrl+Shift" />
<KeyBinding Command="{x:Static local:AppCommands.SearchCommand}"
Key="F" Modifiers="Ctrl" />
<KeyBinding Command="{x:Static local:AppCommands.ToggleFullScreenCommand}"
Key="F11" />
</Window.InputBindings>
<!-- 菜单项自动显示快捷键提示 -->
<MenuItem Header="导出" Command="{x:Static local:AppCommands.ExportCommand}"
InputGestureText="Ctrl+Shift+E" />命令与输入手势
<!-- MouseBinding — 鼠标手势 -->
<Window.InputBindings>
<MouseBinding Command="{Binding RefreshCommand}"
MouseAction="MiddleClick" />
<KeyBinding Command="{Binding SearchCommand}"
Key="F" Modifiers="Ctrl" />
</Window.InputBindings>
<!-- 全局快捷键管理 -->
<Window.Resources>
<!-- 为命令设置快捷键提示 -->
</Window.Resources>
<MenuItem Header="文件(_F)">
<MenuItem Header="新建(_N)" Command="{Binding NewCommand}" InputGestureText="Ctrl+N" />
<MenuItem Header="打开(_O)" Command="{Binding OpenCommand}" InputGestureText="Ctrl+O" />
<MenuItem Header="保存(_S)" Command="{Binding SaveCommand}" InputGestureText="Ctrl+S" />
<Separator />
<MenuItem Header="退出(_X)" Command="{Binding ExitCommand}" InputGestureText="Alt+F4" />
</MenuItem>CommunityToolkit.Mvvm 中的命令
/// <summary>
/// CommunityToolkit.Mvvm — 最简洁的命令定义方式
/// </summary>
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
// 同步命令
[RelayCommand]
private void Refresh() => LoadData();
// 带条件的命令
[RelayCommand(CanExecute = nameof(CanDelete))]
private void Delete(User user) => Users.Remove(user);
private bool CanDelete(User user) => user != null;
// 异步命令
[RelayCommand]
private async Task LoadAsync()
{
var data = await _service.GetAllAsync();
Users = new ObservableCollection<User>(data);
}
// 带取消支持的异步命令
[RelayCommand(IncludeCancelCommand = true)]
private async Task ExportAsync(CancellationToken token)
{
await _service.ExportAsync(token);
}
// 自动生成:RefreshCommand, DeleteCommand, LoadCommand, ExportCommand
// ExportCommand 还有配套的 ExportCancelCommand
}<!-- 自动生成的命令名 -->
<Button Content="刷新" Command="{Binding RefreshCommand}" />
<Button Content="删除" Command="{Binding DeleteCommand}" CommandParameter="{Binding SelectedUser}" />
<Button Content="加载" Command="{Binding LoadCommand}" />
<Button Content="导出" Command="{Binding ExportCommand}" />
<Button Content="取消导出" Command="{Binding ExportCancelCommand}" />命令优缺点
优点
缺点
总结
命令系统是 WPF MVVM 模式的核心。掌握 RelayCommand/AsyncRelayCommand 的实现,理解 CommandParameter 参数传递,以及 CommunityToolkit.Mvvm 的 [RelayCommand] 特性,能显著提升 WPF 开发效率。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《WPF 命令系统》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《WPF 命令系统》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《WPF 命令系统》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《WPF 命令系统》最大的收益和代价分别是什么?
