WPF MVVM Command CanExecute,仅在焦点更改时重新评估

Posted

技术标签:

【中文标题】WPF MVVM Command CanExecute,仅在焦点更改时重新评估【英文标题】:WPF MVVM Command CanExecute, reevaluates only at focus change 【发布时间】:2017-04-07 20:07:46 【问题描述】:

WPF 中重构 MVVM 项目,我试图摆脱 MVVM 模式用户之间似乎常见的问题。

我有视图,谁的 DataContextMyViewModel。这是一个按钮,绑定了一个实现 Execute 和 CanExecute 的命令。

XAML:

<Button Command="Binding ConnectCommand"/>

MyViewModel 公开 ConnectCommand

public ICommand ConnectCommand
    
        get  return new DelegateCommand(() => Connect(), () => IsConnectEnabled); 
    

(最后是我使用的 DelegateCommand 的定义)

MyViewModel 还公开了属性 IsConnectEnabled,用于命令的 CanExecute 部分:

public bool IsConnectEnabled
        
            get
            
                return (isDisconnected && null!=selectedDevice && 0<selectedDevice.Length);
            
        

MyViewModel 类实现 INotifyPropertyChanged 接口

public class MyViewModel : INotifyPropertyChanged

    #region INotifyPropertyChanged Members
    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged(string propertyName)
    
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    
    #endregion

    #region rest of the class        

命令的CanExecute 部分仅根据应用程序中焦点的变化进行评估(即,无论我做什么点击)。我知道 UpdateSourceTrigger 默认设置为 PropertyChanged,因此我目前的解决方案是在以下几个地方手动引发 PropertyChanged 事件代码。但我想做得更好,并在 IsConnectEnabled 的值发生变化时自动完成此活动。

WPF 和 MVVM 模式是否为这个问题提供了解决方案?

为了完整起见,请遵循我正在使用的完整 ICommand 实现,DelegateCommand

    /// <summary>
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </summary>
    public class DelegateCommand : ICommand
    
        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod)
            : this(executeMethod, null, false)
        
        

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        
        

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        
            if (executeMethod == null)
            
                throw new ArgumentNullException("executeMethod");
            

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        

        #region Public Methods

        /// <summary>
        ///     Method to determine if the command can be executed
        /// </summary>
        public bool CanExecute()
        
            if (_canExecuteMethod != null)
            
                return _canExecuteMethod();
            
            return true;
        

        /// <summary>
        ///     Execution of the command
        /// </summary>
        public void Execute()
        
            if (_executeMethod != null)
            
                _executeMethod();
            
        

        /// <summary>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        
            get
            
                return _isAutomaticRequeryDisabled;
            
            set
            
                if (_isAutomaticRequeryDisabled != value)
                
                    if (value)
                    
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    
                    else
                    
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    
                    _isAutomaticRequeryDisabled = value;
                
            
        

        /// <summary>
        ///     Raises the CanExecuteChaged event
        /// </summary>
        public void RaiseCanExecuteChanged()
        
            OnCanExecuteChanged();
        

        /// <summary>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        

        #endregion

        #region ICommand Members

        /// <summary>
        ///     ICommand.CanExecuteChanged implementation
        /// </summary>
        public event EventHandler CanExecuteChanged
        
            add
            
                if (!_isAutomaticRequeryDisabled)
                
                    CommandManager.RequerySuggested += value;
                
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            
            remove
            
                if (!_isAutomaticRequeryDisabled)
                
                    CommandManager.RequerySuggested -= value;
                
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            
        

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

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

        #endregion

        #region Data

        private readonly Action _executeMethod = null;
        private readonly Func<bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    

    /// <summary>
    ///     This class allows delegating the commanding logic to methods passed as parameters,
    ///     and enables a View to bind commands to objects that are not part of the element tree.
    /// </summary>
    /// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
    public class DelegateCommand<T> : ICommand
    
        #region Constructors

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod)
            : this(executeMethod, null, false)
        
        

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod)
            : this(executeMethod, canExecuteMethod, false)
        
        

        /// <summary>
        ///     Constructor
        /// </summary>
        public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod, bool isAutomaticRequeryDisabled)
        
            if (executeMethod == null)
            
                throw new ArgumentNullException("executeMethod");
            

            _executeMethod = executeMethod;
            _canExecuteMethod = canExecuteMethod;
            _isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
        

        #endregion

        #region Public Methods

        /// <summary>
        ///     Method to determine if the command can be executed
        /// </summary>
        public bool CanExecute(T parameter)
        
            if (_canExecuteMethod != null)
            
                return _canExecuteMethod(parameter);
            
            return true;
        

        /// <summary>
        ///     Execution of the command
        /// </summary>
        public void Execute(T parameter)
        
            if (_executeMethod != null)
            
                _executeMethod(parameter);
            
        

        /// <summary>
        ///     Raises the CanExecuteChaged event
        /// </summary>
        public void RaiseCanExecuteChanged()
        
            OnCanExecuteChanged();
        

        /// <summary>
        ///     Protected virtual method to raise CanExecuteChanged event
        /// </summary>
        protected virtual void OnCanExecuteChanged()
        
            CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
        

        /// <summary>
        ///     Property to enable or disable CommandManager's automatic requery on this command
        /// </summary>
        public bool IsAutomaticRequeryDisabled
        
            get
            
                return _isAutomaticRequeryDisabled;
            
            set
            
                if (_isAutomaticRequeryDisabled != value)
                
                    if (value)
                    
                        CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
                    
                    else
                    
                        CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
                    
                    _isAutomaticRequeryDisabled = value;
                
            
        

        #endregion

        #region ICommand Members

        /// <summary>
        ///     ICommand.CanExecuteChanged implementation
        /// </summary>
        public event EventHandler CanExecuteChanged
        
            add
            
                if (!_isAutomaticRequeryDisabled)
                
                    CommandManager.RequerySuggested += value;
                
                CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
            
            remove
            
                if (!_isAutomaticRequeryDisabled)
                
                    CommandManager.RequerySuggested -= value;
                
                CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
            
        

        bool ICommand.CanExecute(object parameter)
        
            // if T is of value type and the parameter is not
            // set yet, then return false if CanExecute delegate
            // exists, else return true
            if (parameter == null &&
                typeof(T).IsValueType)
            
                return (_canExecuteMethod == null);
            
            return CanExecute((T)parameter);
        

        void ICommand.Execute(object parameter)
        
            Execute((T)parameter);
        

        #endregion

        #region Data

        private readonly Action<T> _executeMethod = null;
        private readonly Func<T, bool> _canExecuteMethod = null;
        private bool _isAutomaticRequeryDisabled = false;
        private List<WeakReference> _canExecuteChangedHandlers;

        #endregion
    

    /// <summary>
    ///     This class contains methods for the CommandManager that help avoid memory leaks by
    ///     using weak references.
    /// </summary>
    internal class CommandManagerHelper
    
        internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
        
            if (handlers != null)
            
                // Take a snapshot of the handlers before we call out to them since the handlers
                // could cause the array to me modified while we are reading it.

                EventHandler[] callees = new EventHandler[handlers.Count];
                int count = 0;

                for (int i = handlers.Count - 1; i >= 0; i--)
                
                    WeakReference reference = handlers[i];
                    EventHandler handler = reference.Target as EventHandler;
                    if (handler == null)
                    
                        // Clean up old handlers that have been collected
                        handlers.RemoveAt(i);
                    
                    else
                    
                        callees[count] = handler;
                        count++;
                    
                

                // Call the handlers that we snapshotted
                for (int i = 0; i < count; i++)
                
                    EventHandler handler = callees[i];
                    handler(null, EventArgs.Empty);
                
            
        

        internal static void AddHandlersToRequerySuggested(List<WeakReference> handlers)
        
            if (handlers != null)
            
                foreach (WeakReference handlerRef in handlers)
                
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    
                        CommandManager.RequerySuggested += handler;
                    
                
            
        

        internal static void RemoveHandlersFromRequerySuggested(List<WeakReference> handlers)
        
            if (handlers != null)
            
                foreach (WeakReference handlerRef in handlers)
                
                    EventHandler handler = handlerRef.Target as EventHandler;
                    if (handler != null)
                    
                        CommandManager.RequerySuggested -= handler;
                    
                
            
        

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler)
        
            AddWeakReferenceHandler(ref handlers, handler, -1);
        

        internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
        
            if (handlers == null)
            
                handlers = (defaultListSize > 0 ? new List<WeakReference>(defaultListSize) : new List<WeakReference>());
            

            handlers.Add(new WeakReference(handler));
        

        internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
        
            if (handlers != null)
            
                for (int i = handlers.Count - 1; i >= 0; i--)
                
                    WeakReference reference = handlers[i];
                    EventHandler existingHandler = reference.Target as EventHandler;
                    if ((existingHandler == null) || (existingHandler == handler))
                    
                        // Clean up old handlers that have been collected
                        // in addition to the handler that is to be removed.
                        handlers.RemoveAt(i);
                    
                
            
        
    

【问题讨论】:

WPF 还是 Silverlight? @heltonbiker WPF。谢谢,我现在在描述中说得更清楚了。 "WPF 和 MVVM 模式是否为这个问题提供了解决方案?"没有。 【参考方案1】:

这是一个古老的错误行为,至少在我的 WPF 经验中。

我发现 RelayCommand (GalaSoft.MvvmLight.CommandWpf.RelayCommand) 的 MvvmLight 实现似乎可以解决这个问题。

请注意,他们甚至创建了一个 WPF 特定实现 (!),apparently 旨在纠正这种怪异现象。

【讨论】:

以上是关于WPF MVVM Command CanExecute,仅在焦点更改时重新评估的主要内容,如果未能解决你的问题,请参考以下文章

WPF MVVM DataGrid Button Command绑定

使用 WPF + MVVM 中的 Command + CommandParameter 删除列表视图中的行

WPF MVVM,Prism,Command Binding

wpf中mvvm的Command绑定后,如何在点击按钮的时候在viewmodel里面异步执行方法。

WPF MVVM Command CanExecute,仅在焦点更改时重新评估

WPF 事件实现MVVM中的Command绑定