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方法到底有什么作用以及到底該怎么用?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM