前言

最近在学习Stylet中Command="{s:Action 方法名}"的设计与实现,但要弄明白这个之前,必须对原生实现命令比较熟悉,一想我也很久没有自己实现原生的命令了,之前都是用Community.Mvvm库来实现,所以今天先来回顾一下,在WPF中如何实现原生的命令。

借助AI使用原生的WPF写法实现了一个跟Stylet例子Hello一样的效果:

WPF中如何使用命令

WPF命令是实现用户界面交互的核心机制,通过实现ICommand接口来封装可执行的操作。命令支持松耦合的UI设计,可以绑定到按钮、菜单等控件,实现统一的执行逻辑。WPF提供了丰富的内置命令如ApplicationCommandsNavigationCommands等,同时也支持自定义命令,便于实现撤销/重做、数据绑定等复杂功能。

现在先来看看这个例子中是如何使用命令的吧!!

 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 event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
} public bool CanExecute(object? parameter)
{
return _canExecute == null || _canExecute(parameter);
} public void Execute(object? parameter)
{
_execute(parameter);
} public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
}

这个例子中自己实现了一个实现ICommand接口的RelayCommand类。

先来看看ICommand接口:

    public interface ICommand
{
event EventHandler? CanExecuteChanged;
bool CanExecute(object? parameter);
void Execute(object? parameter);
}

这个ICommand接口起到了什么作用呢?

  • 统一命令规范:定义了命令的标准结构,包含执行方法Execute和状态判断方法CanExecute
  • 实现命令绑定:允许UI控件(如Button、MenuItem)通过Command属性绑定到具体命令实现
  • 控制可用性:CanExecute方法动态控制控件的启用/禁用状态,CanExecuteChanged事件通知UI更新状态
  • 参数传递:通过parameter参数在UI和命令逻辑间传递数据
  • 解耦UI与业务逻辑:将界面操作与具体实现分离,提高代码的可维护性和可测试性

RelayCommand中:

 private readonly Action<object?> _execute;
private readonly Predicate<object?>? _canExecute;

_execute (Action<object?>): 存储要执行的操作委托

_canExecute (Predicate<object?>?): 存储判断命令是否可执行的谓词委托,可为 null

 public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}

这里出现了一个CommandManager

WPF 中的 CommandManager 是一个帮助类,位于System.Windows.Input命名空间。它并不负责“执行命令”,而是为整个命令系统(RoutedCommand / RoutedUICommand)提供基础支撑,核心职责可以概括为四类:

1、处理路由命令的 4 个附加事件

CommandManager 预定义了 4 个 static 的 RoutedEvent,都是附加事件,所有 UIElement 都可以通过它们监听或引发命令相关路由事件:

附加事件 触发时机 典型用途
PreviewCanExecuteEvent 准备询问某命令能否执行时触发(隧道) 用于全局或父级拦截“能否执行”判断
CanExecuteEvent 同上,但为冒泡阶段 本地逻辑判断命令当前是否可用
PreviewExecutedEvent 准备执行命令时触发(隧道) 做执行前的统一拦截,例如日志、撤销栈
ExecutedEvent 同上,但为冒泡阶段 实际执行业务逻辑(如 Save、Cut、Paste)

这里出现了隧道冒泡两个概念,该如何理解呢?

在 WPF 路由事件体系中,隧道(Tunneling)冒泡(Bubbling)是指事件在可视化树上传递的两个方向,想象成“从上到下”还是“从下到上”即可。与命令系统结合时,理解这两个方向就等于知道“谁先被通知”、“谁可以打断谁”。

树结构:

Window → Grid → StackPanel → Button

这是典型的一棵可视化树。

隧道(Preview……)→ 从根向叶

PreviewCanExecute / PreviewExecuted 这类以 Preview 开头的事件,先由 Window 收到,再依次 Grid、StackPanel,最后才到达实际声明 CommandBinding / 声明 InputBindings 的那个 Button。

作用:你可以在高层(例如 Window 一级)拦截事件,做“统一处理”或“统一否决”,比如给所有按钮加日志、在全局禁止某些快捷键等。只要沿途某级标记 e.Handled = true,它就终止继续向下传递。

冒泡(……无 Preview)→ 从叶向根

隧道阶段结束后如果仍然 Handled == false,则进入冒泡阶段。方向反过来:Button 先收到,再依次 StackPanel、Grid、Window。

作用:一般在最具体元素(Button)里决定命令是否可用或执行,而父容器只做辅助行为,如更新状态栏、刷新菜单对勾等。同样可以用 e.Handled = true 阻止再向上传。

2、提供 4 组 Add xxx Handler / Remove xxx Handler 的快捷方法

这些只是对 UIElement.AddHandler、RemoveHandler 的二次封装,方便挂接或注销上述 4 种附加事件,省去记忆事件标识符或强制转换类型的麻烦。

3、维护全局命令“有效性”通知:RequerySuggested

事件定义:public static event EventHandler RequerySuggested;

作用:当系统条件变化(键盘焦点变化、文本被修改、网络状态变更等)时,所有命令需要重新询问“是否能执行”。WPF 内部的按钮、菜单项等在订阅此事件后,就会再次调用 ICommand.CanExecute 来决定 IsEnabled。

手动触发:CommandManager.InvalidateRequerySuggested(); 会立即引发该事件,从而强制刷新所有绑定命令的可执行状态。

4、提供“类级别” CommandBinding / InputBinding 注册

RegisterClassCommandBinding(Type type, CommandBinding commandBinding)

为指定类型(而不仅是某个实例)注册 CommandBinding,在所有实例共享同一组绑定逻辑,等同于在静态构造函数里写:

CommandManager.RegisterClassCommandBinding(
typeof(MyControl),
new CommandBinding(ApplicationCommands.Save, OnSaveExecuted, OnSaveCanExecute));
RegisterClassInputBinding(Type type, InputBinding inputBinding)

同样道理,为某个控件类统一注册快捷键:

CommandManager.RegisterClassInputBinding(
typeof(MyWindow),
new KeyBinding(ApplicationCommands.Save, Key.S, ModifierKeys.Control));

现在来看看整体流程:

  <Button Content="Say Hello"
Command="{Binding SayHelloCommand}"
Height="30"
FontSize="14"/>

在View中绑定这个命令。

刚开始这个命令不可执行:

是因为在ViewModel中是这样写的,首先在构造函数中这样写:

  public ShellViewModel()
{
SayHelloCommand = new RelayCommand(
execute: _ => ShowHelloMessage(),
canExecute: _ => CanSayHello
);
}

其中控制是否能执行的,设置了一个属性来管理:

 public bool CanSayHello => !string.IsNullOrEmpty(Name);

命令执行的方法为:

  private void ShowHelloMessage()
{
MessageBox.Show($"Hello, {Name}", "Hello, Native WPF", MessageBoxButton.OK, MessageBoxImage.Information);
}

刚开始Name属性为空,所以CanSayHello为false,所以命令不能执行。

为什么输入东西就可以变成执行了呢?

 public string Name
{
get => _name;
set
{
if (SetProperty(ref _name, value))
{
((RelayCommand)SayHelloCommand).RaiseCanExecuteChanged();
}
}
}

RelayCommand中有一个RaiseCanExecuteChanged方法:

   public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}

CommandManager.InvalidateRequerySuggested(); 是 WPF 中用于强制刷新命令的可执行状态的方法。所有绑定了ICommand的控件(如 Button、MenuItem 等)马上重新评估自己的 CanExecute 状态。

然后因为Name不为空,CanSayHello为True,这个命令就可以执行了。

点击按钮就会触发RelayCommand中的Execute方法:

在ViewModel的构造函数中。实例化了一个RelayCommand对象,并且将_ => ShowHelloMessage()这个委托赋值给了execute,所以触发命令之后就会执行ShowHelloMessage方法。

以上就是使用WPF原生的方法实现的一个使用命令的例子。

回顾一下WPF原生实现命令的更多相关文章

  1. WPF 原生绑定和命令功能使用指南

    WPF 原生绑定和命令功能使用指南 魏刘宏 2020 年 2 月 21 日 如今,当谈到 WPF 时,我们言必称 MVVM.框架(如 Prism)等,似乎已经忘了不用这些的话该怎么使用 WPF 了.当 ...

  2. 【转】【WPF】WPF 自定义快捷键命令(Command)

    命令简介 WPF 中的命令是通过实现 ICommand 接口创建的.ICommand 公开两个方法(Execute 及 CanExecute)和一个事件(CanExecuteChanged).Exec ...

  3. 16、WPF中的命令

    一.前言 事件的作用是发布.传播一些信息,消息送达接收者,事件的使命就算完成了,至于如何响应事件送来的消息事件并不做规定,每个接收者可以使用自己的行为来响应事件,也就是说事件不具有约束力.命令能够在代 ...

  4. WPF中的命令简介

    使用Prism委托命令Demo: WPF委托命令DelegateCommand的传参方式 在WPF中使用命令的步骤很简单 1.创建命令 2.绑定命令 3.设置命令源 4.设置命令目标 WPF中命令的核 ...

  5. WPF中的命令与命令绑定导航

    1.WPF中的命令与命令绑定(一) (引入命令) 2.WPF中的命令与命令绑定(二)(详细介绍命令和命令绑定)

  6. WPF中的命令与命令绑定(二)

    原文:WPF中的命令与命令绑定(二) WPF中的命令与命令绑定(二)                                              周银辉在WPF中,命令(Commandi ...

  7. WPF中的命令与命令绑定(一)

    原文:WPF中的命令与命令绑定(一)   WPF中的命令与命令绑定(一)                                           周银辉说到用户输入,可能我们更多地会联想到 ...

  8. WPF 自定义快捷键命令(COMMAND)(转)

    命令简介 WPF 中的命令是通过实现 ICommand 接口创建的.ICommand 公开两个方法(Execute 及 CanExecute)和一个事件(CanExecuteChanged).Exec ...

  9. WPF原生环形图表

    原文:WPF原生环形图表 版权声明:欢迎转载.转载请注明出处,谢谢 https://blog.csdn.net/wzcool273509239/article/details/56480963 主要利 ...

  10. Windows Presentation Foundation (WPF)中的命令(Commands)简述

    原文:Windows Presentation Foundation (WPF)中的命令(Commands)简述 ------------------------------------------- ...

随机推荐

  1. 打开host有感

    一年前的呼喊,消失在了文化课的彼端,没有回音: 直至今日打开host,才发觉那时悔恨与泪水的珍贵. [此时此刻的光辉,盼君勿忘]也得加上过去式了啊--

  2. 解决Vditor加载Markdown网页很慢的问题(Vite+JS+Vditor)

    1. 引言 在上一篇文章<使用Vditor将Markdown文档渲染成网页(Vite+JS+Vditor)>中,详细介绍了通过Vditor将Markdown格式文档渲染成Web网页的过程, ...

  3. 【实战教程】雷池 WAF + 阿里云 CDN 深度联动:性能优化与安全防护双升级指南

    雷池 WAF(Web Application Firewall)是一款强大的网络安全防护产品,通过实时流量分析和精准规则拦截,有效抵御各种网络攻击.在部署雷池 WAF 的同时,结合阿里云 CDN(内容 ...

  4. Vue3自定义指令实现权限控制

    使用Pinia(Vue.js的轻量级状态管理库,是Vuex的替代品)来管理用户权限,并结合自定义指令控制元素的显隐.步骤操作如下: 1.安装Pinia: npm install pinia 或 yar ...

  5. 使用Oracle数据库的递归查询语句生成菜单树

    SQL 格式 SELECT * FROM TABLE WHERE [...结果过滤语句] START WITH [...递归开始条件] CONNECT BY PRIOR [...递归执行条件] 查询所 ...

  6. phpstrom (xdebug)远程断点调试 homestead (纯图)

    1.本地调试可以参考:https://www.cnblogs.com/LWMLWM/p/8251905.html 2.远程调试:主要是在虚拟机上如何进行 1)打开xshell ->file-&g ...

  7. 从 Tableau文件中获取数据方法汇总

    ↓↓↓欢迎关注我的公众号,在这里有数据相关技术经验的优质原创文章↓↓↓ 在实际使用Tableau中经常会遇到需要从已有的tableau文件或仪表板中导出/提取/复制数据,本篇文章整理了相关从Table ...

  8. openWrt安装三方插件

    前言 openWrt是一款开源的路由器系统,其最大的优点就是 支持第三方扩展插件. 新增的插件基本都会在左侧的服务菜单中展现,通过此入口就可以使用插件功能. 大部分openWrt固件都帮你装好了ope ...

  9. vue被部署到子(二级)目录

    需求有的时候,你的域名很珍贵,除了二级域名外.你还可以将你的项目部署在服务器二级目录下,这样的话,就可以部署多个项目了.比如说,我有一个域名为dshvv.com的服务器,我想部署两个项目:12306项 ...

  10. 认识Android Studio中各个模块

    首先看看刚创建完的项目界面,除了菜单栏.工具栏等,没有什么可以编辑的界面   通过项目的文件浏览器可以打开所有项目文件,所以文件管理器在整个开发过程中相当重要. 其中用到最多的便是app项,其余大部分 ...