赞
踩
在之前 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>的委托类型 EventHandler
public 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);
}
}
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.刷新状态函数:判断命令的可执行状态,刷新一次CanExecute
private 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 alive
AddHandlerToCWT(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.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);
}
}
}
}
}
经过上述的分析,我们发现其实要想通过命令影响命令关联的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节中实现):
(1)手动刷新方案
public class CommandBase : ICommand
{
//1.命令可执行状态改变事件 CanExecuteChanged
public event EventHandler CanExecuteChanged;
//2.命令具体执行逻辑委托 Action
public 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
{
//fileds
private Action<object> _executeAction;
private Func<object, bool> _canExecuteFunc;
/Constructors
public 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; }
}
//Methods
public bool CanExecute(object parameter)
{
return _canExecuteFunc == null?true:_canExecuteFunc(parameter);
}
public void Execute(object parameter)
{
_executeAction(parameter);
}
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。