1.ICommand源码分析

​ 在之前 WPF(三) WPF命令 中我们已经分析过了 WPF 的命令系统,包括WPF默认的 RoutedCommand 以及我们自定义的 ICommand 命令实现。但是上篇文章主要侧重于命令的使用,而一些命令工作原理和流程细节还存在一些疑问,比如 ICommand 的 CanExecuteChanged 事件是如何实现订阅的?关联 ICommand 对象控件的启用/禁用状态是由什么来影响的?是怎么影响的等等。在说明和分析之前,我们还是先来分析一下 ICommand 命令的相关源码。

namespace System {//1.WPF 默认声明的委托类型 EventHandler//   - Object sender: 委托调用对象/源// - EventArgs e: 事件参数对象public delegate void EventHandler(Object sender, EventArgs e);//2.带泛型<TEventArgs>的委托类型 EventHandlerpublic delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e); // Removed TEventArgs constraint post-.NET 4
}namespace System.Windows.Input
{///<summary>///     An interface that allows an application author to define a method to be invoked.///</summary>public interface ICommand{//3.Raised when the ability of the command to execute has changed.//(1)说明:包装委托EventHandler的事件对象CanExecuteChanged//(2)作用:既然ICommand包含了event事件属性,则说明ICommand就成为了事件发布者。由绑定Command的控件订阅CanExecuteChanged事件,在特定属性改变时,来触发该CanExecuteChanged事件,从而进一步调用 CanExecute 方法刷新绑定控件的可用状态。event EventHandler CanExecuteChanged;//4.Returns whether the command can be executed.//  - <param name="parameter">A parameter that may be used in executing the command. This parameter may be ignored by some implementations.</param>//    - <returns>true if the command can be executed with the given parameter and current state. false otherwise.</returns>//(1)说明:该方法用于判断命令的可执行状态//(2)作用:常与绑定控件的可用状态 UIElement.IsEnabledProperty 相关联,配合CanExecuteChanged事件来刷新控件状态。若不需要判断控件的可用状态,则可以直接返回true bool CanExecute(object parameter);//5.Defines the method that should be executed when the command is executed.// - <param name="parameter">A parameter that may be used in executing the command. This parameter may be ignored by some implementations.</param>//(1)说明:该方法用于编写命令的执行逻辑,是命令的关键//(2)作用: 该方法用于封装命令的执行逻辑,是命令执行的主体void Execute(object parameter);}
}

2.命令模型分析(以Button控件为例)

2.1 绑定订阅过程

​ Button 的基类 ButtonBase 类中实现的 ICommandSource 中的 Command,该 Command 是一个依赖属性。其注册了一个属性改变时的回调函数 OnCommandChanged,当 Button 绑定/设置Command时,就会自动调用该回调函数,其逻辑源码如下:

namespace System.Windows.Controls.Primitives
{/// <summary>///     The base class for all buttons/// </summary>public abstract class ButtonBase : ContentControl, ICommandSource{/// <summary>///     The DependencyProperty for RoutedCommand/// </summary>[CommonDependencyProperty]public static readonly DependencyProperty CommandProperty =DependencyProperty.Register("Command",typeof(ICommand),typeof(ButtonBase),new FrameworkPropertyMetadata((ICommand)null,new PropertyChangedCallback(OnCommandChanged)));//Command依赖属性注册了回调函数 OnCommandChanged/// <summary>/// Get or set the Command property/// </summary>[Bindable(true), Category("Action")][Localizability(LocalizationCategory.NeverLocalize)]public ICommand Command{get{return (ICommand) GetValue(CommandProperty);}set{SetValue(CommandProperty, value);}}//1.静态回调函数 OnCommandChanged:最终调用OnCommandChanged(ICommand oldCommand, ICommand newCommand)方法private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){ButtonBase b = (ButtonBase)d;b.OnCommandChanged((ICommand)e.OldValue, (ICommand)e.NewValue);}//2.实例回调函数 OnCommandChanged:在绑定新命令时,调用HookCommand方法进行关联处理private void OnCommandChanged(ICommand oldCommand, ICommand newCommand){if (oldCommand != null){UnhookCommand(oldCommand);}if (newCommand != null){HookCommand(newCommand);}}}}

​ 由上面的源码可以看出, 实例的回调函数 OnCommandChanged 方法会进一步调用UnhookCommand 和 HookCommand方法,用于先将原来的Command与控件取消关联,再进一步将新的Command与控件进行关联处理。我们这里主要以HookCommand为主,具体的关联处理逻辑如下:

namespace System.Windows.Controls.Primitives
{public abstract class ButtonBase : ContentControl, ICommandSource{private void UnhookCommand(ICommand command){CanExecuteChangedEventManager.RemoveHandler(command, OnCanExecuteChanged);UpdateCanExecute();}//1.命令关联函数:用于将命令与控件绑定,实质上是让控件订阅Command的事件发布//   - CanExecuteChangedEventManager.AddHandler: 使用控件的OnCanExecuteChanged方法订阅command的发布事件//   - UpdateCanExecute: 执行调用一次CanExecuteCommandSource方法,更新CanExecute状态(这里首次调用是初始化状态)private void HookCommand(ICommand command){CanExecuteChangedEventManager.AddHandler(command, OnCanExecuteChanged);UpdateCanExecute();}//2.订阅函数:ICommand EventHandler的委托类型,用于控件订阅Command Changed事件,刷新CanExecute状态private void OnCanExecuteChanged(object sender, EventArgs e){UpdateCanExecute();}//3.刷新状态函数:判断命令的可执行状态,刷新一次CanExecuteprivate void UpdateCanExecute(){if (Command != null){CanExecute = MS.Internal.Commands.CommandHelpers.CanExecuteCommandSource(this);}else{CanExecute = true;}}}
}

​ HookCommand 方法主要有两个作用,一个是调用 CanExecuteChangedEventManager.AddHandler方法,将自身的 OnCanExecuteChanged 方法作为EventHandler 委托去订阅 Command 的 changed 事件 CanExecuteChanged,这样当 Command 的 CanExecuteChanged 事件触发时就会自动去发布从而调用控件的 OnCanExecuteChanged 方法来更新 CanExecute 状态。其源码如下:

namespace System.Windows.Input
{/// <summary>/// Manager for the ICommand.CanExecuteChanged event./// </summary>public class CanExecuteChangedEventManager : WeakEventManager{/// <summary>/// Add a handler for the given source's event./// </summary>public static void AddHandler(ICommand source, EventHandler<EventArgs> handler){if (source == null)throw new ArgumentNullException("source");if (handler == null)throw new ArgumentNullException("handler");//1.单例模式:调用CurrentManager.PrivateAddHandler方法来处理(Command,Handler)CurrentManager.PrivateAddHandler(source, handler);}private void PrivateAddHandler(ICommand source, EventHandler<EventArgs> handler){// get the list of sinks for this source, creating if necessary// 2.获取Sink链表,用于维护全局的(Command,Handler)关系List<HandlerSink> list = (List<HandlerSink>)this[source];if (list == null){list = new List<HandlerSink>();this[source] = list;}// add a new sink to the list// 3.将当前的(Command,Handler)关系加入维护链表,并注册订阅事件HandlerSink sink = new HandlerSink(this, source, handler);list.Add(sink);// keep the handler aliveAddHandlerToCWT(handler, _cwt);}//4.关键:Sink对象,维护(Command,Handler)关系对,并在初始化时注册订阅事件private class HandlerSink{public HandlerSink(CanExecuteChangedEventManager manager, ICommand source, EventHandler<EventArgs> originalHandler){_manager = manager;_source = new WeakReference(source);_originalHandler = new WeakReference(originalHandler);_onCanExecuteChangedHandler = new EventHandler(OnCanExecuteChanged);// BTW, the reason commands used weak-references was to avoid leaking// the Button - see Dev11 267916.   This is fixed in 4.5, precisely// by using the weak-event pattern.   Commands can now implement// the CanExecuteChanged event the default way - no need for any// fancy weak-reference tricks (which people usually get wrong in// general, as in the case of DelegateCommand<T>).// register the local listener//5.将当前Button的 Handler 委托订阅Command的 CanExecuteChanged 事件source.CanExecuteChanged += _onCanExecuteChangedHandler;}}}
}

​ HookCommand 方法的第二个作用是调用 UpdateCanExecute 方法来初始化 CanExecute 状态。并且 UpdateCanExecute 方法也是 OnCanExecuteChanged 委托中的主要逻辑,其用来判断命令的可执行状态,并刷新一次CanExecute,本质就是调用一次Command内部的 bool CanExecute 方法,其源码分析如下:

namespace MS.Internal.Commands
{internal static class CommandHelpers{internal static bool CanExecuteCommandSource(ICommandSource commandSource){//1.获取绑定命令对象ICommand command = commandSource.Command;if (command == null){return false;}object commandParameter = commandSource.CommandParameter;IInputElement inputElement = commandSource.CommandTarget;RoutedCommand routedCommand = command as RoutedCommand;if (routedCommand != null){if (inputElement == null){inputElement = (commandSource as IInputElement);}return routedCommand.CanExecute(commandParameter, inputElement);}//2.调用 command.CanExecute 方法判断/刷新一次状态return command.CanExecute方法(commandParameter);}}
}

2.2 状态关联

2.1 中分析并说明了Command是如何与Button控件绑定并建立事件订阅关系的,那么Button控件的可用状态是如何与Command的 CanExecute 方法相关联的呢?其实在上述分析中,UpdateCanExecute()方法从CommandHelpers.CanExecuteCommandSource(this) 返回的值设置了自身 CanExecute 属性的值,而设置 CanExecute 属性时 就自动关联到了按钮是禁用/启用的状态变量 IsEnabledProperty,其源码分析如下:

namespace System.Windows.Controls.Primitives
{public abstract class ButtonBase : ContentControl, ICommandSource{//ButtonBase 的 CanExecute属性private bool CanExecute{get { return !ReadControlFlag(ControlBoolFlags.CommandDisabled); }set{if (value != CanExecute){WriteControlFlag(ControlBoolFlags.CommandDisabled, !value);//关联到UIElement.IsEnabledProperty,是否可用状态CoerceValue(IsEnabledProperty);}}}}
}

2.3 事件触发机制

​ 经过上述的分析,我们发现其实要想通过命令影响命令关联的Button按钮的启用/禁用状态,就需要有人在数据改变时去主动触发Command中的CanExecuteChanged事件,这样才能唤醒后续一系列订阅该事件的状态刷新委托,那么由谁来调用它呢?

(1)RoutedCommand

​ 对于 WPF 内置的RoutedCommand来说,订阅 ICommand.CanExecuteChanged 事件的任何客户端实际上都是订阅的 CommandManager.RequerySuggested 事件,RoutedCommand把更新命令可用/禁用状态的逻辑代理给了CommandManager.RequerySuggested事件,而这个事件的触发是由CommandManager自己自动来检测的,其源码如下:

namespace System.Windows.Input
{/// <summary>///     A command that causes handlers associated with it to be called./// </summary>public class RoutedCommand : ICommand{//1.对于CanExecuteChanged事件的任何订阅行为都代理给了CommandManager.RequerySuggested事件,由CommandManager自动检测/更新状态public event EventHandler CanExecuteChanged{add { CommandManager.RequerySuggested += value; }remove { CommandManager.RequerySuggested -= value; }}}
}

​ 例如UI界面上的空间焦点改变时,就会触发RequerySuggested。这种实现是一种懒触发的方式,不需要开发者自己调用,而交由WPF系统自动检测。这种懒触发的方式带来的问题就是会导致CanExecute方法可能被多次执行,这可能会带来一定的性能影响。当然我们也可以手动调用CommandManager.InvalidateRequerySuggested() 来更新命令状态,这将执行与触发 ICommand.CanExecuteChanged 相同的操作,但这将同时在后台线程上对所有的 RoutedCommand 执行此操作。默认情况下,WPF RequerySuggested 事件的触发条件是 WPF 内置的,其只会在以下时机刷新可用性:

KeyUp
MouseUp
GotKeyboardFocus
LostKeyboardFocus

​ 其源码部分可以在 CommandDevice.PostProcessInput 查看,关键部分如下:

// 省略前面。
if (e.StagingItem.Input.RoutedEvent == Keyboard.KeyUpEvent ||e.StagingItem.Input.RoutedEvent == Mouse.MouseUpEvent ||e.StagingItem.Input.RoutedEvent == Keyboard.GotKeyboardFocusEvent ||e.StagingItem.Input.RoutedEvent == Keyboard.LostKeyboardFocusEvent)
{CommandManager.InvalidateRequerySuggested(); //触发事件->刷新状态
}

(2)自定义Command

​ 对于自定义的Command来说,CanExecute方法仅会在绑定初始化启动时刷新一次,之后无论数据如何变化都不会触发事件刷新状态,因为没有人主动去触发ICommand.CanExecuteChanged 事件来进一步激活订阅委托。但是,我们可以在自定义Command中手动实现事件刷新的触发机制,主要包括以下两种方式(在第3节中实现):

  • 手动刷新状态: 在影响Command可执行状态的属性值改变时,手动调用方法触发CanExecuteChanged 事件
  • 使用CommandManager代理: 模仿RoutedCommand,将CanExecuteChanged 事件代理给 CommandManager.RequerySuggested 事件

3.自定义Command进阶

(1)手动刷新方案

public class CommandBase : ICommand
{//1.命令可执行状态改变事件 CanExecuteChangedpublic event EventHandler CanExecuteChanged; //2.命令具体执行逻辑委托 Actionpublic Action<object> DoExecute { get; set; }//3.命令是否可执行判断逻辑委托(这里给个默认的值,不实现就默认返回true)public Func<object, bool> DoCanExecute { get; set; } = new Func<object, bool>(obj => true);public bool CanExecute(object parameter){// 让实例去实现这个委托return DoCanExecute?.Invoke(parameter) == true;// 绑定的对象 可用}public void Execute(object parameter){// 让实例去实现这个委托DoExecute?.Invoke(parameter);}//4.手动触发事件方法:手动触发一次CanExecuteChanged事件,刷新状态   public void DoCanExecuteChanged(){// 触发事件的目的就是重新调用CanExecute方法CanExecuteChanged?.Invoke(this, EventArgs.Empty);}
}

(2)使用CommandManager代理方案

namespace Login.ViewModels
{public class CommandBase : ICommand{//filedsprivate Action<object> _executeAction;private Func<object, bool> _canExecuteFunc;/Constructorspublic CommandBase(Action<object> executeAction){_executeAction = executeAction;_canExecuteFunc = null;}public CommandBase(Action<object> executeAction, Func<object, bool> canExecuteFunc){_executeAction = executeAction;_canExecuteFunc = canExecuteFunc;}//event: 由 CommandManager.RequerySuggested 代理事件public event EventHandler CanExecuteChanged{add { CommandManager.RequerySuggested += value; }remove { CommandManager.RequerySuggested -= value; }}//Methodspublic bool CanExecute(object parameter){return _canExecuteFunc == null?true:_canExecuteFunc(parameter);}public void Execute(object parameter){_executeAction(parameter);}}
}

WPF(六) Command 命令模型源码分析相关推荐

  1. 如何将镜像烧写至iNand(fastboot命令的源码分析)

    以下内容源于网络资源的学习与整理,如有侵权请告知删除. 参考博客 u-boot sdfuse命令烧录分析----从SD卡加载内核_white_bugs的博客-CSDN博客 一.将镜像文件烧写至iNan ...

  2. Java分布式跟踪系统Zipkin(六):Brave源码分析-Brave和SpringBoot整合

    所有博文均在个人独立博客http://blog.mozhu.org首发,欢迎访问! Zipkin是用当下最流行的SpringBoot开发的,SpringBoot将Spring项目的开发过程大大简化,一 ...

  3. memcached(二)事件模型源码分析

    在memcachedd中,作者为了专注于缓存的设计,使用了libevent来开发事件模型.memcachedd的时间模型同nginx的类似,拥有一个主进行(master)以及多个工作者线程(woker ...

  4. redis的scan命令的源码分析,实现原理

    简言 1. 线上环境keys命令不可用,会导致redis卡死.scan命令因为可以分批遍历,比较实用 2. scan命令包括多个 遍历整个数据库的scan命令,处理函数 scanCommand(),最 ...

  5. koa 中间件洋葱模型源码分析

    中间件基本使用 app.use(async(ctx,next)=>{ctx.state={username:'jeff'};await next();... })app.use(async(ct ...

  6. docker stats命令源码分析结果

    2019独角兽企业重金招聘Python工程师标准>>> 本文是基于docker 1.10.3版本的源码,对docker stats命令进行源码分析,看看docker stats命令输 ...

  7. movi命令(do_movi函数的源码分析)

    以下内容源于网络资源的学习与整理,如有侵权请告知删除. 一.do_movi函数分析 当执行movi相关命令时,实际执行的是do_movi函数. x210 # help movi movi init - ...

  8. Django源码分析1:创建项目和应用分析

    django命令行源码分析 本文环境python3.5.2,django1.10.x系列 当命令行输入时django-admin时 (venv) ACA80166:dbManger wuzi$ dja ...

  9. s-sgdisk源码分析 “--set-alignment=value分区对齐参数”

    文章目录 边界对齐子命令使用 源码分析 sgdisk.cc main函数入口 gptcl.cc DoOptions解析并执行具体命令函数 gpt.cc CreatePartition创建分区函数,设置 ...

最新文章

  1. cout 数组_C语言学习笔记(十)二维数组内存预设
  2. JavaWeb第五讲 Web核心基础之HTTP协议
  3. apache cxf之 一个简单的JAX-WS服务程序
  4. 地图编辑器开发(四)
  5. 第一讲 ISO 17799/27001 标准简介
  6. 统计字符串中各类字符的个数
  7. 有用的函数-系统采集
  8. Variation Model
  9. u大师装iso系统linux,u大师教你iso文件系统的安装
  10. 原生js实现 转义还原HTML
  11. 计算机相关缩略语,计算机缩略语精选
  12. R语言plotly可视化:使用plotly可视化模型预测真阳性率(True positive)TPR和假阳性率(False positive)FPR在不同阈值(threshold)下的曲线
  13. 名帖295 张瑞图 行书《行书帖选》
  14. QList、QVector,QSet,std::vector,std::list直接的转换关系总结
  15. MySql将两个字段查询值合并拼接
  16. 友盟推送成功但是收不到
  17. SSL集训 某OJ2021.08.14 提高B组 Luogu P7527 [USACO21OPEN] United Cows of Farmer John G【树状数组】
  18. C#模拟鼠标操作以及键盘输入
  19. xp系统打印机服务器win7连接不了,xp不能访问win7共享打印机
  20. GMap.net 涉及标绘源码

热门文章

  1. Uva 11201麻球繁衍(设概率方程的技巧)
  2. 压缩视频的软件有哪些?最好用的视频压缩软件是哪款?
  3. Java是剑客;.NET是刀客(转载)
  4. Python丨Django实现微电影网站
  5. 如何看待网易强制取消所有用户魔兽世界游戏时间
  6. 【Pytorch神经网络实战案例】33 使用BERT模型实现完形填空任务
  7. 考研英语 各种阅读/翻译/新题型/完形填空技巧
  8. 《MLB棒球创造营》:棒球团建·一球成名
  9. U盘格式化后如何恢复数据?
  10. C++ 宽字符 窄字符 char wchar