WPF-CommandBinding 与 RoutedEvent 的关系.
目录
一、先建立全局认知
┌─────────────────────────────────────────────────────────┐
│ WPF 命令系统 │
│ │
│ ICommand RoutedCommand CommandBinding │
│ (接口) (路由命令) (命令绑定) │
│ │
│ 底层全部依赖 RoutedEvent 实现传播 │
└─────────────────────────────────────────────────────────┘核心结论: RoutedCommand 的执行和判断能否执行,本质上是通过触发 RoutedEvent 在可视化树上传播,由 CommandBinidng 拦截并处理。
二、关键角色
2.1 ICommand 接口
public interface ICommand
{
bool CanExecute(object parameter);
void Execute(object parameter);
event EventHandler CanExecuteChanged;
}2.2 RoutedCommand(实现了 ICommand)
public class RoutedCommand : ICommand
{
// RoutedCommand 自身没有业务逻辑,它的 Execute/CanExecute 内部做的是 -> 触发 RoutedEvent
// 这两个就是 RoutedCommand 内部定义的路由事件
public static readonly RoutedEvent ExecutedEvent;
public static readonly RoutedEvent CanExecuteEvent;
// (实际定义在 CommandManager 中)
}2.3 CommandBinding
public class CommandBinding
{
public ICommand Command { get; set; }
// 这两个就是对 RoutedEvent 的处理器
public event ExecutedRoutedEventHandler Executed;
public event CanExecuteRoutedEventHandler CanExecute;
}2.4 CommandManager (静态管理器)
public static class CommandManger
{
// 这两个是真正的 RoutedEvent
public static readonly RoutedEvent ExecutedEvent =
EventManager.RegisterRoutedEvent(
"Executed",
RoutingStrategy.Bubble, // 冒泡
typeof(ExecutedRoutedEventHandler),
typeof(CommandManager));
public static readonly RoutedEvent CanExecuteEvent =
EventManager.RegisterRoutedEvent(
"CanExecute",
RoutingStratrgy.Bubble, // 冒泡
typeof(CanExecuteRoutedEventHandler),
typeof(CommandManager));
// 还有对应的 Tunnel(Preview) 版本
public static readonly RoutedEvent PreviewExecutedEvent;
public static readonly RoutedEvent PreviewCanExecuteEvent;
}三、完整执行流程
场景
<Window>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Save"
Executed="Save_Executed"
CanExecute="Save_CanExecute"/>
</Window.CommandBindings>
<Grid>
<StackPanel>
<Button Command="ApplicationCommands.Save" Content="保存"/>
</StackPanel>
</Grid>
</Window>点击按钮后发生了什么?
用户点击 Button
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 1: Button.OnClick() │
│ Button 发现自己绑定了 Command │
│ 调用 RoutedCommand.Execute(parameter, Button)│
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 2: RoutedCommand.Execute() 内部 │
│ │
│ // 关键!不是直接执行业务逻辑 │
│ // 而是触发 RoutedEvent! │
│ │
│ ExecutedRoutedEventArgs args = new ...; │
│ args.RoutedEvent = CommandManager.ExecutedEvent; │
│ args.Command = this; // Save 命令 │
│ │
│ // 先触发 Tunnel │
│ target.RaiseEvent(previewArgs); │
│ // 再触发 Bubble │
│ target.RaiseEvent(args); │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 3: RoutedEvent 开始在可视树中冒泡传播 │
│ │
│ Button ──→ StackPanel ──→ Grid ──→ Window │
│ │
│ 每经过一个节点,检查该节点的 CommandBindings │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Step 4: 到达 Window 时 │
│ │
│ Window 有 CommandBinding: │
│ Command = ApplicationCommands.Save │
│ │
│ 匹配成功! │
│ → 调用 Save_Executed 处理器 │
│ → e.Handled = true │
│ → 冒泡停止 │
└─────────────────────────────────────────────────────┘四、源码级解析
4.1 RoutedCommand.Execute 源码简化
public class RoutedCommand : ICommand
{
void ICommand.Execute(object parameter)
{
Execute(parameter, FilterInputElement(Keyboard.FocusedElement));
}
public void Execute(object parameter, IInputElement target)
{
// 不是执行业务逻辑,而是触发路由事件!
ExecuteImpl(parameter, target);
}
private void ExecuteImpl(object parameter, IInputElement target)
{
// 构造事件参数
var args = new ExecutedRoutedEventArgs(this, parameter);
// ========== 关键:触发 RoutedEvent ==========
// 1. Tunnel 阶段
args.RoutedEvent = CommandManager.PreviewExecutedEvent;
target.RaiseEvent(args);
// 2. Bubble 阶段
if (!args.Handled)
{
args.RoutedEvent = CommandManager.ExecutedEvent;
target.RaiseEvent(args);
}
// 如果没人处理,什么都不会发生
}
}4.2 RoutedCommand.CanExecute 源码简化
public bool CanExecute(object parameter, IInputElement target)
{
// 同样是触发路由事件!
var args = new CanExecuteRoutedEventArgs(this, parameter);
// Tunnel
args.RoutedEvent = CommandManager.PreviewCanExecuteEvent;
target.RaiseEvent(args);
// Bubble
if (!args.Handled)
{
args.RoutedEvent = CommandManager.CanExecuteEvent;
target.RaiseEvent(args);
}
return args.CanExecute; // CommandBinding 会设置这个值
}4.3 CommandBinding 如何拦截?
UIElement 的静态构造函数注册了 类级别处理器:
static UIElement()
{
// 注册类处理器,拦截命令相关的路由事件
EventManager.RegisterClassHandler(
typeof(UIElement),
CommandManager.ExecutedEvent,
new ExecutedRoutedEventHandler(OnExecutedThunk));
EventManager.RegisterClassHandler(
typeof(UIElement),
CommandManager.CanExecuteEvent,
new CanExecuteRoutedEventHandler(OnCanExecuteThunk));
// Tunnel 版本也注册
EventManager.RegisterClassHandler(
typeof(UIElement),
CommandManager.PreviewExecutedEvent,
new ExecutedRoutedEventHandler(OnPreviewExecutedThunk));
EventManager.RegisterClassHandler(
typeof(UIElement),
CommandManager.PreviewCanExecuteEvent,
new CanExecuteRoutedEventHandler(OnPreviewCanExecuteThunk));
}类处理器内部逻辑:
private static void OnExecutedThunk(object sender, ExecutedRoutedEventArgs e)
{
UIElement uie = sender as UIElement;
// 遍历该元素的 CommandBindings
if (uie.CommandBindings != null)
{
foreach (CommandBinding binding in uie.CommandBindings)
{
// 命令匹配?
if (binding.Command == e.Command)
{
// 调用 CanExecute 确认能否执行
if (binding.CheckCanExecute(sender, e))
{
// 调用 CommandBinding 的 Executed 处理器
binding.OnExecuted(sender, e);
e.Handled = true; // 阻止继续冒泡
}
}
}
}
}五、完整流程图
RoutedCommand.Execute()
│
▼
┌─── RaiseEvent ───┐
│ PreviewExecuted │ (Tunnel: Window → Button)
│ ExecutedEvent │ (Bubble: Button → Window)
└────────┬─────────┘
│
路由到每个 UIElement 节点
│
▼
┌─────────────────────────────┐
│ UIElement.OnExecutedThunk │ (类级别处理器)
│ ↓ │
│ 遍历 CommandBindings │
│ ↓ │
│ 匹配 Command? │
│ ├── No → 继续冒泡 │
│ └── Yes → 检查 CanExecute│
│ ├── false → 跳过│
│ └── true │
│ ↓ │
│ 调用 Executed 处理器 │
│ e.Handled = true │
│ 冒泡停止 │
└─────────────────────────────┘六、CanExecute 的定时刷线机制
6.1 CommandManager.RequerySuggested
public static class CommandManager
{
// 当 WPF 认为命令状态可能变化时触发
public static event EventHandler RequerySuggested;
// 手动触发刷线
public static void InvalidateRequerySuggested()
{
// 触发所有命令重新查询 CanExecute
}
}6.2 自动触发时机
以下情况 WPF 自动调用 InvalidateRequerySuggested:
┌─────────────────────────────┐
│ • 焦点变化 (Focus Changed) │
│ • 键盘输入 │
│ • 鼠标点击 │
│ • 属性变化 │
│ • Dispatcher 空闲时 │
└─────────────────────────────┘
│
▼
对所有绑定了 RoutedCommand 的控件
重新触发 CanExecuteEvent 路由事件
│
▼
CommandBinding.CanExecute 处理器被调用
│
▼
Button.IsEnabled 自动更新6.3 CanExecute 路由事件流程
CommandManager.InvalidateRequerySuggested()
│
▼
Button 绑定的 RoutedCommand.CanExecute()
│
▼
RaiseEvent(CanExecuteEvent) ← 路由事件!
│
▼
冒泡传播: Button → StackPanel → Grid → Window
│
▼
Window 的 CommandBinding 匹配
│
▼
调用 Save_CanExecute 处理器
e.CanExecute = true/false;
│
▼
Button.IsEnabled = e.CanExecute七、对比 RoutedCommand vs 普通 ICommand (如 RelayCommand)
┌───────────────────────────────────────────────────────────┐
│ RoutedCommand │
│ │
│ Execute() ──→ RaiseEvent(ExecutedEvent) │
│ │ │
│ RoutedEvent 在可视树冒泡 │
│ │ │
│ CommandBinding 拦截并执行 │
│ │
│ 特点: 命令逻辑在 View 层 (CodeBehind) │
│ 适用: 应用级命令 (Copy/Paste/Save...) │
└───────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────┐
│ RelayCommand / DelegateCommand │
│ │
│ Execute() ──→ 直接调用 Action<T> 委托 │
│ │
│ 没有路由事件!没有可视树传播! │
│ │
│ 特点: 命令逻辑在 ViewModel 层 │
│ 适用: MVVM 模式 │
└───────────────────────────────────────────────────────────┘详细对比:
| 特性 | RoutedCommand | RelayCommand |
|---|---|---|
| 实现接口 | ICommand | ICommand |
| 内部机制 | RoutedEvent 路由传播 | 直接委托调用 |
| 业务逻辑位置 | CommandBinding (View 层) | ViewModel 中的方法 |
| 可视树传播 | 支持 | 不支持 |
| CanExecute 刷新 | CommandManager 自动触发 | 手动调用 RaiseCanExecuteChanged |
| MVVM 友好 | 不友好 | 友好 |
| 适用场景 | 系统命令、控件库 | 业务命令 |
八、自定义 RoutedCommand 完整示例
// 1. 定义命令
public static class MyCommands
{
public static readonly RoutedCommand ExportCommand =
new RoutedCommand("Export", typeof(MyCommands),
new InputGestureCollection
{
new KeyGesture(Key.E, ModifierKeys.Control) // Ctrl+E
});
}<!-- 2. XAML 中使用 -->
<Window>
<Window.CommandBindings>
<CommandBinding Command="local:MyCommands.ExportCommand"
Executed="Export_Executed"
CanExecute="Export_CanExecute"/>
</Window.CommandBindings>
<Window.InputBindings>
<KeyBinding Command="local:MyCommands.ExportCommand"
Gesture="Ctrl+E"/>
</Window.InputBindings>
<StackPanel>
<Button Command="local:MyCommands.ExportCommand" Content="导出"/>
<!-- Button 的 Click 最终触发:
RoutedCommand.Execute()
→ RaiseEvent(ExecutedEvent)
→ 冒泡到 Window
→ CommandBinding 匹配
→ Export_Executed 被调用 -->
</StackPanel>
</Window>// 3. CodeBehind 处理
private void Export_Executed(object sender, ExecutedRoutedEventArgs e)
{
// sender = Window (CommandBinding 所在的元素)
// e.Source = Button (触发命令的元素)
// e.Command = ExportCommand
// e.Parameter = CommandParameter
MessageBox.Show("导出成功!");
}
private void Export_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true; // 这个值会影响 Button.IsEnabled
}九、总结关系图
┌─────────────────────────────────────────────────────────────┐
│ │
│ RoutedCommand ─── "我不知道怎么执行, │
│ │ 我只负责触发路由事件" │
│ │ │
│ ▼ │
│ CommandManager.ExecutedEvent ←── 这是一个 RoutedEvent │
│ │ │
│ ▼ │
│ 可视树冒泡传播 │
│ Button → StackPanel → Grid → Window │
│ │ │
│ ▼ │
│ UIElement 类处理器拦截 │
│ 遍历每个节点的 CommandBindings │
│ │ │
│ ▼ │
│ CommandBinding ─── "这个命令我认识! │
│ │ 我来提供执行逻辑" │
│ │ │
│ ▼ │
│ Executed / CanExecute 处理器被调用 │
│ │
└─────────────────────────────────────────────────────────────┘
一句话总结:
RoutedCommand 是"发号施令者" → 通过 RoutedEvent 传播命令
CommandBinding 是"执行者" → 拦截路由事件并执行业务逻辑
RoutedEvent 是"传令兵" → 在可视树中传递命令消息