WPF-09 命令系统

文章目录

    • [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 命令)
    • [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. 完整例程:文本编辑器命令演示)
    • [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 可以在元素树中寻找合适的命令绑定。

典型工作流程:

  1. 命令源 (如 Button)触发命令(单击时调用 Execute)。
  2. 命令 (实现 ICommand)决定是否可以执行(CanExecute)以及执行逻辑(Execute)。
  3. 命令绑定CommandBinding)将命令与具体处理逻辑关联,通常放在窗口或控件上。
  4. 命令目标 (可选)是命令操作的对象(如 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)

CommandBindingRoutedCommand 与具体的执行和校验逻辑关联起来。通常添加到窗口或自定义控件的 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, CheckBox
  • MenuItem, Hyperlink
  • ButtonBase 派生类

ICommandSource 接口

csharp 复制代码
public interface ICommandSource
{
    ICommand Command { get; set; }
    object CommandParameter { get; set; }
    IInputElement CommandTarget { get; set; }
}
  • Command:要调用的命令对象。
  • CommandParameter:传递给命令的 ExecuteCanExecute 的参数。
  • 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 已经支持参数(ExecuteCanExecute 均接收 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(使用 RelayCommandAsyncCommand 两种方式)。
  • 自定义 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 调试技巧

  • CanExecuteExecute 中添加断点或日志。
  • 使用 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 开发将变得高效而优雅。

相关推荐
晓纪同学5 小时前
WPF-10资源系统
wpf
七夜zippoe11 小时前
DolphinDB集群部署:从单机到分布式
分布式·wpf·单机·dolphindb·分集群
波波0071 天前
写出稳定C#系统的关键:不可变性思想解析
开发语言·c#·wpf
bugcome_com1 天前
从 MVVMLight 到 CommunityToolkit.Mvvm:MVVM 框架的现代化演进与全面对比
wpf
笺上知微2 天前
基于HelixToolkit.SharpDX 渲染3D模型
wpf
晓纪同学3 天前
WPF-03 第一个WPF程序
大数据·hadoop·wpf
光电大美美-见合八方中国芯3 天前
用于无色波分复用光网络的 10.7 Gb/s 反射式电吸收调制器与半导体光放大器单片集成
网络·后端·ai·云计算·wpf·信息与通信·模块测试
晓纪同学3 天前
WPF-02体系结构
wpf
晓纪同学3 天前
WPF-01概述
wpf