WPF (6) Command command model source code analysis

1. ICommand source code analysis

​ In the previous WPF (3) WPF command, we have analyzed the WPF command system, including WPF's default RoutedCommand and our custom ICommand command implementation. But the last article mainly focused on the use of commands, and there are still some questions about the working principle and process details of some commands, such as how to subscribe to the CanExecuteChanged event of ICommand? What affects the enabled/disabled state of the associated ICommand object control? How is it affected and so on. Before explaining and analyzing, let's analyze the relevant source code of the ICommand command.

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. Command model analysis (taking Button control as an example)

2.1 Binding subscription process

​ The Command in the ICommandSource implemented in the ButtonBase class, which is the base class of Button, is a dependency property. It registers a callback function OnCommandChanged when the property changes. When the Button binds/sets the Command, the callback function will be called automatically. The logic source code is as follows:

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);
            }
        }
    
    }
    
}

​ As can be seen from the above source code, the callback function OnCommandChanged method of the instance will further call the UnhookCommand and HookCommand methods to first unassociate the original Command from the control, and then further associate the new Command with the control. We mainly focus on HookCommand here, and the specific association processing logic is as follows:

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;
            }
        }
    }   
}

​ The HookCommand method has two main functions, one is to call the CanExecuteChangedEventManager.AddHandler method, and entrust its own OnCanExecuteChanged method as an EventHandler to subscribe to the changed event CanExecuteChanged of the Command, so that when the CanExecuteChanged event of the Command is triggered, it will be automatically released to call the control's OnCanExecuteChanged method to update the CanExecute state. Its source code is as follows:

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;
            }
        }
    }
}

​ The second function of the HookCommand method is to call the UpdateCanExecute method to initialize the CanExecute state. And the UpdateCanExecute method is also the main logic in the OnCanExecuteChanged delegate. It is used to judge the executable state of the command and refresh CanExecute once. The essence is to call the bool CanExecute method inside the Command once. The source code analysis is as follows:

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 State Association

​2.1 analyzed and explained how the Command is bound to the Button control and establishes an event subscription relationship, so how is the available state of the Button control associated with the CanExecute method of the Command? In fact, in the above analysis, the UpdateCanExecute() method sets the value of its own CanExecute property from the value returned by CommandHelpers.CanExecuteCommandSource(this), and when setting the CanExecute property, it is automatically associated with the status variable IsEnabledProperty that the button is disabled/enabled, and its source code analysis as follows:

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 Event trigger mechanism

​ After the above analysis, we found that in order to affect the enable/disable status of the Button button associated with the command through the command, someone needs to actively trigger the CanExecuteChanged event in the Command when the data changes, so as to wake up a series of subsequent subscriptions to the event state refresh delegate, who will call it?

(1)RoutedCommand

​ For WPF's built-in RoutedCommand, any client that subscribes to the ICommand.CanExecuteChanged event is actually a subscribed CommandManager.RequerySuggested event. RoutedCommand delegates the logic of updating the command's available/disabled status to the CommandManager.RequerySuggested event, and this event The trigger is automatically detected by CommandManager itself, and its source code is as follows:

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; }
        }
    }
}

​ For example, when the spatial focus on the UI interface changes, RequerySuggested will be triggered. This implementation is a lazy trigger method, which does not need to be invoked by the developer himself, but is automatically detected by the WPF system. The problem with this lazy triggering method is that the CanExecute method may be executed multiple times, which may have a certain performance impact. Of course we can also manually call CommandManager.InvalidateRequerySuggested() to update the command status, which will perform the same operation as triggering ICommand.CanExecuteChanged, but this will perform this operation on all RoutedCommands on a background thread at the same time. By default, the triggering condition of WPF RequerySuggested event is WPF built-in, which will only refresh availability at the following times:

KeyUp
MouseUp
GotKeyboardFocus
LostKeyboardFocus

​ Its source code can be viewed at CommandDevice.PostProcessInput, the key parts are as follows:

// 省略前面。
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) Custom Command

​ For a custom Command, the CanExecute method will only be refreshed once when the binding initialization starts , and then no matter how the data changes, the event refresh status will not be triggered, because no one actively triggers the ICommand.CanExecuteChanged event to further activate the subscription delegate . However, we can manually implement the event refresh trigger mechanism in the custom Command, mainly including the following two methods (implemented in Section 3):

  • Manually refresh the state: when the property value that affects the executable state of the Command changes, manually call the method to trigger the CanExecuteChanged event
  • Use CommandManager proxy: imitate RoutedCommand, delegate CanExecuteChanged event to CommandManager.RequerySuggested event

3. Advanced Custom Command

(1) Manual refresh scheme

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) Use the CommandManager proxy solution

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);
    	}
 
    }
}

Guess you like

Origin blog.csdn.net/qq_40772692/article/details/127287839