Prism框架中的DelagateCommand(上)

背景

  在很多时候在WPF中我们都会使用到ICommand接口来定义我们的命令,然后将这个命令绑定到前台的控件比如Button上面,这个是一个很常规的操作,在后台的ViewModel中我们通常会使用一个实现了ICommand接口的DelegateCommand类来实例化我们定义的ICommand命令,我们这篇文章来重点分析一下Prism中这个DelegateCommand会写出什么不同的东西。

常规实现

  在看常规的实现之前我们首先来看看ICommand这个接口中到底定义了一些什么东西?

namespace System.Windows.Input
{
    //
    // 摘要:
    //     Defines a command.
    public interface ICommand
    {
        //
        // 摘要:
        //     Occurs when changes occur that affect whether or not the command should execute.
        event EventHandler CanExecuteChanged;

        //
        // 摘要:
        //     Defines the method that determines whether the command can execute in its current
        //     state.
        //
        // 参数:
        //   parameter:
        //     Data used by the command. If the command does not require data to be passed,
        //     this object can be set to null.
        //
        // 返回结果:
        //     true if this command can be executed; otherwise, false.
        bool CanExecute(object parameter);
        //
        // 摘要:
        //     Defines the method to be called when the command is invoked.
        //
        // 参数:
        //   parameter:
        //     Data used by the command. If the command does not require data to be passed,
        //     this object can be set to null.
        void Execute(object parameter);
    }
}

  这个接口是定义在System.Windows.Input命令空间下面的,主要包括两个方法和一个事件,我们的继承类就是要实现这个接口中的这些方法和事件

/// <summary>
   /// 实现DelegateCommand
   /// </summary>
 internal  class DelegateCommand : ICommand
   {
       /// <summary>
       /// 命令所需执行的事件
       /// </summary>
       public Action<object> ExecuteAction { get; set; }
       /// <summary>
       /// 命令是否可用所执行的事件
       /// </summary>
       public Func<object, bool> CanExecuteFunc { get; set; }
      
 
       public DelegateCommand(Action<object> execute, Func<object, bool> canexecute)
       {
           ExecuteAction = execute;
           CanExecuteFunc = canexecute;
       }
 
       /// <summary>
       /// 命令可用性获取
       /// </summary>
       /// <param name="parameter"></param>
       /// <returns></returns>
       public bool CanExecute(object parameter)
       {
           return CanExecuteFunc(parameter);
       }
 
       public event EventHandler CanExecuteChanged
       {
           add { CommandManager.RequerySuggested += value; }
           remove { CommandManager.RequerySuggested -= value; }
       }
 
       /// <summary>
       /// 命令具体执行
       /// </summary>
       /// <param name="parameter"></param>
       public void Execute(object parameter)
       {
           ExecuteAction(parameter);
       }
   }

  这个里面这两个方法都比较好理解,关键是这个CanExecuteChanged这个事件订阅了CommandManager的RequerySuggested这个事件,对于这个实现请参阅MSDN的详细解释。

Prism中的实现

  Prism框架中的DelegateCommand首先是继承自一个DelegateCommandBase的抽象类,我们先来看看这个抽象类的具体实现

namespace Prism.Commands
{
    /// <summary>
    /// An <see cref="ICommand"/> whose delegates can be attached for <see cref="Execute"/> and <see cref="CanExecute"/>.
    /// </summary>
    public abstract class DelegateCommandBase : ICommand, IActiveAware
    {
        private bool _isActive;

        private SynchronizationContext _synchronizationContext;
        private readonly HashSet<string> _observedPropertiesExpressions = new HashSet<string>();

        /// <summary>
        /// Creates a new instance of a <see cref="DelegateCommandBase"/>, specifying both the execute action and the can execute function.
        /// </summary>
        protected DelegateCommandBase()
        {
            _synchronizationContext = SynchronizationContext.Current;
        }

        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute.
        /// </summary>
        public virtual event EventHandler CanExecuteChanged;

        /// <summary>
        /// Raises <see cref="ICommand.CanExecuteChanged"/> so every 
        /// command invoker can requery <see cref="ICommand.CanExecute"/>.
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        {
            var handler = CanExecuteChanged;
            if (handler != null)
            {
                if (_synchronizationContext != null && _synchronizationContext != SynchronizationContext.Current)
                    _synchronizationContext.Post((o) => handler.Invoke(this, EventArgs.Empty), null);
                else
                    handler.Invoke(this, EventArgs.Empty);
            }
        }

        /// <summary>
        /// Raises <see cref="CanExecuteChanged"/> so every command invoker
        /// can requery to check if the command can execute.
        /// </summary>
        /// <remarks>Note that this will trigger the execution of <see cref="CanExecuteChanged"/> once for each invoker.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")]
        public void RaiseCanExecuteChanged()
        {
            OnCanExecuteChanged();
        }

        void ICommand.Execute(object parameter)
        {
            Execute(parameter);
        }

        bool ICommand.CanExecute(object parameter)
        {
            return CanExecute(parameter);
        }

        /// <summary>
        /// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
        /// </summary>
        /// <param name="parameter">Command Parameter</param>
        protected abstract void Execute(object parameter);

        /// <summary>
        /// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
        protected abstract bool CanExecute(object parameter);

        /// <summary>
        /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
        /// </summary>
        /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
        /// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
        protected internal void ObservesPropertyInternal<T>(Expression<Func<T>> propertyExpression)
        {
            if (_observedPropertiesExpressions.Contains(propertyExpression.ToString()))
            {
                throw new ArgumentException($"{propertyExpression.ToString()} is already being observed.",
                    nameof(propertyExpression));
            }
            else
            {
                _observedPropertiesExpressions.Add(propertyExpression.ToString());
                PropertyObserver.Observes(propertyExpression, RaiseCanExecuteChanged);
            }
        }

        #region IsActive

        /// <summary>
        /// Gets or sets a value indicating whether the object is active.
        /// </summary>
        /// <value><see langword="true" /> if the object is active; otherwise <see langword="false" />.</value>
        public bool IsActive
        {
            get { return _isActive; }
            set
            {
                if (_isActive != value)
                {
                    _isActive = value;
                    OnIsActiveChanged();
                }
            }
        }

        /// <summary>
        /// Fired if the <see cref="IsActive"/> property changes.
        /// </summary>
        public virtual event EventHandler IsActiveChanged;

        /// <summary>
        /// This raises the <see cref="DelegateCommandBase.IsActiveChanged"/> event.
        /// </summary>
        protected virtual void OnIsActiveChanged()
        {
            IsActiveChanged?.Invoke(this, EventArgs.Empty);
        }

        #endregion
    }
}

  这个DelegateCommandBase除了继承我们前面说的ICommand接口之外还实现了一个IActiveAware的接口,这个接口主要定义了一个IsActive的属性和IsActiveChanged的事件,这里就不再贴出具体的定义。后面我们来重点看一下Prism中的DelegateCommand的实现,然后重点分析一下这个里面的实现。

namespace Prism.Commands
{
    /// <summary>
    /// An <see cref="ICommand"/> whose delegates do not take any parameters for <see cref="Execute()"/> and <see cref="CanExecute()"/>.
    /// </summary>
    /// <see cref="DelegateCommandBase"/>
    /// <see cref="DelegateCommand{T}"/>
    public class DelegateCommand : DelegateCommandBase
    {
        Action _executeMethod;
        Func<bool> _canExecuteMethod;

        /// <summary>
        /// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Action"/> to invoke on execution.
        /// </summary>
        /// <param name="executeMethod">The <see cref="Action"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
        public DelegateCommand(Action executeMethod)
            : this(executeMethod, () => true)
        {

        }

        /// <summary>
        /// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Action"/> to invoke on execution
        /// and a <see langword="Func" /> to query for determining if the command can execute.
        /// </summary>
        /// <param name="executeMethod">The <see cref="Action"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
        /// <param name="canExecuteMethod">The <see cref="Func{TResult}"/> to invoke when <see cref="ICommand.CanExecute"/> is called</param>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
            : base()
        {
            if (executeMethod == null || canExecuteMethod == null)
                throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull);

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
        }

        ///<summary>
        /// Executes the command.
        ///</summary>
        public void Execute()
        {
            _executeMethod();
        }

        /// <summary>
        /// Determines if the command can be executed.
        /// </summary>
        /// <returns>Returns <see langword="true"/> if the command can execute,otherwise returns <see langword="false"/>.</returns>
        public bool CanExecute()
        {
            return _canExecuteMethod();
        }

        /// <summary>
        /// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
        /// </summary>
        /// <param name="parameter">Command Parameter</param>
        protected override void Execute(object parameter)
        {
            Execute();
        }

        /// <summary>
        /// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
        protected override bool CanExecute(object parameter)
        {
            return CanExecute();
        }

        /// <summary>
        /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
        /// </summary>
        /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
        /// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
        /// <returns>The current instance of DelegateCommand</returns>
        public DelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)
        {
            ObservesPropertyInternal(propertyExpression);
            return this;
        }

        /// <summary>
        /// Observes a property that is used to determine if this command can execute, and if it implements INotifyPropertyChanged it will automatically call DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
        /// </summary>
        /// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
        /// <returns>The current instance of DelegateCommand</returns>
        public DelegateCommand ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
        {
            _canExecuteMethod = canExecuteExpression.Compile();
            ObservesPropertyInternal(canExecuteExpression);
            return this;
        }
    }
}

  这个类中的其它的定义和我们常规的实现没有什么区别,重点是这个里面这个里面增加了ObservesPropertyObservesCanExecute这两个带Expression参数的方法,这两个方法其内部都调用了一个叫做ObservesPropertyInternal的方法,我们来看看这个在基类DelegateCommandBase中定义的方法。

/// <summary>
        /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
        /// </summary>
        /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
        /// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
        protected internal void ObservesPropertyInternal<T>(Expression<Func<T>> propertyExpression)
        {
            if (_observedPropertiesExpressions.Contains(propertyExpression.ToString()))
            {
                throw new ArgumentException($"{propertyExpression.ToString()} is already being observed.",
                    nameof(propertyExpression));
            }
            else
            {
                _observedPropertiesExpressions.Add(propertyExpression.ToString());
                PropertyObserver.Observes(propertyExpression, RaiseCanExecuteChanged);
            }
        }

  这个方法的内部会将当前的Expression参数传入一个类型为HashSet<string>的_observedPropertiesExpressions的局部变量里面,如何当前集合中存在该变量就抛出异常避免重复添加,如果没有添加过就添加到当前集合中,添加到集合中以后有调用了一个新的的方法,这个PropertyObserver.Observes方法会将当前的DelegateCommandBase 中的RaiseCanExecuteChanged方法作为参数传入到PropertyObserver类中的Observes方法中去,那我们再来看看这个方法到底是什么,我们接着来看代码。

/// <summary>
    /// Provide a way to observe property changes of INotifyPropertyChanged objects and invokes a 
    /// custom action when the PropertyChanged event is fired.
    /// </summary>
    internal class PropertyObserver
    {
        private readonly Action _action;

        private PropertyObserver(Expression propertyExpression, Action action)
        {
            _action = action;
            SubscribeListeners(propertyExpression);
        }

        private void SubscribeListeners(Expression propertyExpression)
        {
            var propNameStack = new Stack<PropertyInfo>();
            while (propertyExpression is MemberExpression temp) // Gets the root of the property chain.
            {
                propertyExpression = temp.Expression;
                propNameStack.Push(temp.Member as PropertyInfo); // Records the member info as property info
            }

            if (!(propertyExpression is ConstantExpression constantExpression))
                throw new NotSupportedException("Operation not supported for the given expression type. " +
                                                "Only MemberExpression and ConstantExpression are currently supported.");

            var propObserverNodeRoot = new PropertyObserverNode(propNameStack.Pop(), _action);
            PropertyObserverNode previousNode = propObserverNodeRoot;
            foreach (var propName in propNameStack) // Create a node chain that corresponds to the property chain.
            {
                var currentNode = new PropertyObserverNode(propName, _action);
                previousNode.Next = currentNode;
                previousNode = currentNode;
            }

            object propOwnerObject = constantExpression.Value;

            if (!(propOwnerObject is INotifyPropertyChanged inpcObject))
                throw new InvalidOperationException("Trying to subscribe PropertyChanged listener in object that " +
                                                    $"owns '{propObserverNodeRoot.PropertyInfo.Name}' property, but the object does not implements INotifyPropertyChanged.");

            propObserverNodeRoot.SubscribeListenerFor(inpcObject);
        }

        /// <summary>
        /// Observes a property that implements INotifyPropertyChanged, and automatically calls a custom action on 
        /// property changed notifications. The given expression must be in this form: "() => Prop.NestedProp.PropToObserve".
        /// </summary>
        /// <param name="propertyExpression">Expression representing property to be observed. Ex.: "() => Prop.NestedProp.PropToObserve".</param>
        /// <param name="action">Action to be invoked when PropertyChanged event occurs.</param>
        internal static PropertyObserver Observes<T>(Expression<Func<T>> propertyExpression, Action action)
        {
            return new PropertyObserver(propertyExpression.Body, action);
        }
    }

    顾名思义PropertyObserver就是一个用来监控Propery属性变化的类,在这个PropertyObserver类中定义了一个静态的方法Observes方法,这个方法会创建一个PropertyObserver的对象,在这个构造方法中调用SubscribeListeners方法,我们再来看看这个方法的内部又创建了PropertyObserverNode的对象,这个对象又是什么?我们再来继续看。

/// <summary>
    /// Represents each node of nested properties expression and takes care of 
    /// subscribing/unsubscribing INotifyPropertyChanged.PropertyChanged listeners on it.
    /// </summary>
    internal class PropertyObserverNode
    {
        private readonly Action _action;
        private INotifyPropertyChanged _inpcObject;

        public PropertyInfo PropertyInfo { get; }
        public PropertyObserverNode Next { get; set; }

        public PropertyObserverNode(PropertyInfo propertyInfo, Action action)
        {
            PropertyInfo = propertyInfo ?? throw new ArgumentNullException(nameof(propertyInfo));
            _action = () =>
            {
                action?.Invoke();
                if (Next == null) return;
                Next.UnsubscribeListener();
                GenerateNextNode();
            };
        }

        public void SubscribeListenerFor(INotifyPropertyChanged inpcObject)
        {
            _inpcObject = inpcObject;
            _inpcObject.PropertyChanged += OnPropertyChanged;

            if (Next != null) GenerateNextNode();
        }

        private void GenerateNextNode()
        {
            var nextProperty = PropertyInfo.GetValue(_inpcObject);
            if (nextProperty == null) return;
            if (!(nextProperty is INotifyPropertyChanged nextInpcObject))
                throw new InvalidOperationException("Trying to subscribe PropertyChanged listener in object that " +
                                                    $"owns '{Next.PropertyInfo.Name}' property, but the object does not implements INotifyPropertyChanged.");

            Next.SubscribeListenerFor(nextInpcObject);
        }

        private void UnsubscribeListener()
        {
            if (_inpcObject != null)
                _inpcObject.PropertyChanged -= OnPropertyChanged;

            Next?.UnsubscribeListener();
        }

        private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e?.PropertyName == PropertyInfo.Name || string.IsNullOrEmpty(e?.PropertyName))
            {
                _action?.Invoke();
            }
        }
    }

  这个类到底有什么作用,我们只需要监测一个属性的变化,这个PropertyObserverNode肯定是表示对当前属性的一个描述,这个节点里面还定义了一个Next表示当前属性节点的下一个节点,这个该如何解释呢?这里你应该想到了属性这个对象的复杂性,比如下面的一个例子就能够很好的说明,比如我们定义了下面的一个类。

 public class ComplexType : TestPurposeBindableBase
        {
            private int _intProperty;
            public int IntProperty
            {
                get { return _intProperty; }
                set { SetProperty(ref _intProperty, value); }
            }

            private ComplexType _innerComplexProperty;
            public ComplexType InnerComplexProperty
            {
                get { return _innerComplexProperty; }
                set { SetProperty(ref _innerComplexProperty, value); }
            }
        }

  现在我们需要监控这样一个属性,如下面的代码所示,我们现在需要监控的属性是 ComplexProperty.InnerComplexProperty.IntProperty,你怎么定义这个属性的节点,那你肯定需要将ComplexProperty和InnerComplexProperty以及IntProperty三个对象都定义为一个ObserverPropertyNode,并且这三个节点之间通过Next属性再在内部互相关联起来,这样通过这样的一个数据结构就能描述所有的属性结构,并且在PropertyObserver中就能监控到每一个属性的变化了,这样是不是就是一个通用框架做的事情。

var  ComplexProperty = new ComplexType()
            {
                InnerComplexProperty = new ComplexType()
            };

  到了这里是不是感觉很晕,当然这篇只是上篇,在下篇我们会对其中的每一个技术细节进行认真的分析,这篇主要是对整个过程有一个整体上面的把握。

总结

  这篇文章中主要分析了下面两个问题以及一个疑问,下一篇文章我们将带着这些疑问来做更加细致的分析,从而完整理解这个框架中Prism的实现思路

  1 常规ICommand接口中各个方法以及事件的实现。

  2 整个Prism框架中如何实现这个ICommand接口,以及这个实现类DelegateCommand和PropertyObserver、PropertyObserverNode之间的关系和联系。

  3 一个疑问:这个DelegateCommand方法中定义的这两个ObservesProperty和ObservesCanExecute方法到底有什么作用以及到底该怎么用?

posted @ 2021-05-26 22:21  Hello——寻梦者!  阅读(1345)  评论(1编辑  收藏  举报