WPF(六) Command 命令模型源码分析

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>的委托类型 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);
    }
}

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.刷新状态函数:判断命令的可执行状态,刷新一次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.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 查看,关键部分如下:

扫描二维码关注公众号,回复: 15765188 查看本文章
// 省略前面。
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.命令可执行状态改变事件 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);
    	}
 
    }
}

猜你喜欢

转载自blog.csdn.net/qq_40772692/article/details/127287839
今日推荐