wpf - CommandManager.InvalidateRequerySuggested() 不够快。我能做些什么?

标签 wpf icommand

短版

调用CommandManager.InvalidateRequerySuggested()生效的时间比我想要的要长得多(在 UI 控件被禁用之前有 1-2 秒的延迟)。

加长版

我有一个系统,我将任务提交到基于后台线程的任务处理器。此提交发生在 WPF UI 线程上。

当这个提交发生时,管理我的后台线程的对象会做两件事:

  • 它引发了多个 View 模型响应的“忙碌”事件(仍在 UI 线程上);当他们收到这个事件时,他们设置了 IsEnabled标记自己到false .我的 View 中的控件(数据绑定(bind)到此属性)立即变灰,这是我所期望的。
  • 它通知我的 WPF ICommand他们不应该被允许执行的对象(同样,仍然在 UI 线程上)。因为没有像 INotifyPropertyChanged 这样的东西对于 ICommand对象,我被迫调用CommandManager.InvalidateRequerySuggested()强制 WPF 重新考虑我所有的命令对象的 CanExecute状态(是的,我确实需要这样做:否则,这些控件都不会被禁用)。但是,与第 1 项不同的是,使用 ICommand 的按钮/菜单项/等需要更长的时间。与具有 IsEnabled 的 UI 控件相比,对象在视觉上更改为禁用状态属性手动设置。

  • 问题是,从用户体验的角度来看,这看起来是 可怕 ;我的一半控件立即变灰(因为它们的 IsEnabled 属性设置为 false),然后整整 1-2 秒后,我的另一半控件也随之变灰(因为它们的 CanExecute 方法终于重新评估)。

    所以,我的问题的第 1 部分:
    听起来很傻,有没有办法让我 CommandManager.InvalidateRequerySuggested()它的工作更快吗?我怀疑没有。

    很公平,我的问题的第 2 部分:
    我该如何解决这个问题?我希望同时禁用所有控件。否则它看起来不专业和尴尬。有任何想法吗? :-)

    最佳答案

    CommandManager.InvalidateRequerySuggested()尝试验证所有命令,这是完全无效的(在您的情况下很慢) - 在每次更改时,您都要求每个命令重新检查其 CanExecute() !

    您需要该命令才能知道哪些对象和属性是它的CanExecute。依赖,并建议仅在它们更改时重新查询。这样,如果您更改对象的属性,则只有依赖于它的命令才会更改其状态。

    这就是我解决问题的方法,但首先是一个预告片:

    // in ViewModel's constructor - add a code to public ICommand:
    this.DoStuffWithParameterCommand = new DelegateCommand<object>(
        parameter =>
            {
                //do work with parameter (remember to check against null)
            },
        parameter => 
            {
                //can this command execute? return true or false
            }
        )
        .ListenOn(whichObject, n => n.ObjectProperty /*type safe!*/, this.Dispatcher /*we need to pass UI dispatcher here*/)
        .ListenOn(anotherObject, n => n.AnotherObjectProperty, this.Dispatcher); // chain calling!
    

    该命令正在监听 NotifyPropertyChanged对象中影响它是否可以执行的事件,并且仅在需要重新查询时才调用检查。

    现在,很多代码(我们内部框架的一部分)来做到这一点:

    我用 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
    {
        #region Constructors
    
        /// <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;
    
            this.RaiseCanExecuteChanged();
        }
    
        #endregion
    
        #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);
                    }
                }
            }
        }
    }
    

    然后我写了 ListenOn扩展方法,将命令“绑定(bind)”到属性,并调用其RaiseCanExecuteChanged :
    public static class DelegateCommandExtensions
    {
        /// <summary>
        /// Makes DelegateCommnand listen on PropertyChanged events of some object,
        /// so that DelegateCommnand can update its IsEnabled property.
        /// </summary>
        public static DelegateCommand ListenOn<ObservedType, PropertyType>
            (this DelegateCommand delegateCommand, 
            ObservedType observedObject, 
            Expression<Func<ObservedType, PropertyType>> propertyExpression,
            Dispatcher dispatcher)
            where ObservedType : INotifyPropertyChanged
        {
            //string propertyName = observedObject.GetPropertyName(propertyExpression);
            string propertyName = NotifyPropertyChangedBaseExtensions.GetPropertyName(propertyExpression);
    
            observedObject.PropertyChanged += (sender, e) =>
            {
                if (e.PropertyName == propertyName)
                {
                    if (dispatcher != null)
                    {
                        ThreadTools.RunInDispatcher(dispatcher, delegateCommand.RaiseCanExecuteChanged);
                    }
                    else
                    {
                        delegateCommand.RaiseCanExecuteChanged();
                    }
                }
            };
    
            return delegateCommand; //chain calling
        }
    
        /// <summary>
        /// Makes DelegateCommnand listen on PropertyChanged events of some object,
        /// so that DelegateCommnand can update its IsEnabled property.
        /// </summary>
        public static DelegateCommand<T> ListenOn<T, ObservedType, PropertyType>
            (this DelegateCommand<T> delegateCommand, 
            ObservedType observedObject, 
            Expression<Func<ObservedType, PropertyType>> propertyExpression,
            Dispatcher dispatcher)
            where ObservedType : INotifyPropertyChanged
        {
            //string propertyName = observedObject.GetPropertyName(propertyExpression);
            string propertyName = NotifyPropertyChangedBaseExtensions.GetPropertyName(propertyExpression);
    
            observedObject.PropertyChanged += (object sender, PropertyChangedEventArgs e) =>
            {
                if (e.PropertyName == propertyName)
                {
                    if (dispatcher != null)
                    {
                        ThreadTools.RunInDispatcher(dispatcher, delegateCommand.RaiseCanExecuteChanged);
                    }
                    else
                    {
                        delegateCommand.RaiseCanExecuteChanged();
                    }
                }
            };
    
            return delegateCommand; //chain calling
        }
    }
    

    然后,您需要将以下扩展名添加到 NotifyPropertyChanged
        /// <summary>
    /// <see cref="http://dotnet.dzone.com/news/silverlightwpf-implementing"/>
    /// </summary>
    public static class NotifyPropertyChangedBaseExtensions
    {
        /// <summary>
        /// Raises PropertyChanged event.
        /// To use: call the extension method with this: this.OnPropertyChanged(n => n.Title);
        /// </summary>
        /// <typeparam name="T">Property owner</typeparam>
        /// <typeparam name="TProperty">Type of property</typeparam>
        /// <param name="observableBase"></param>
        /// <param name="expression">Property expression like 'n => n.Property'</param>
        public static void OnPropertyChanged<T, TProperty>(this T observableBase, Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChangedWithRaise
        {
            observableBase.OnPropertyChanged(GetPropertyName<T, TProperty>(expression));
        }
    
        public static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChanged
        {
            if (expression == null)
                throw new ArgumentNullException("expression");
    
            var lambda = expression as LambdaExpression;
            MemberExpression memberExpression;
            if (lambda.Body is UnaryExpression)
            {
                var unaryExpression = lambda.Body as UnaryExpression;
                memberExpression = unaryExpression.Operand as MemberExpression;
            }
            else
            {
                memberExpression = lambda.Body as MemberExpression;
            }
    
            if (memberExpression == null)
                throw new ArgumentException("Please provide a lambda expression like 'n => n.PropertyName'");
    
            MemberInfo memberInfo = memberExpression.Member;
    
            if (String.IsNullOrEmpty(memberInfo.Name))
                throw new ArgumentException("'expression' did not provide a property name.");
    
            return memberInfo.Name;
        }
    }
    

    在哪里 INotifyPropertyChangedWithRaise是这样的吗(它建立了引发 NotifyPropertyChanged 事件的标准接口(interface)):
    public interface INotifyPropertyChangedWithRaise : INotifyPropertyChanged
    {
        void OnPropertyChanged(string propertyName);
    }
    

    最后一 block 拼图是这样的:
    public class ThreadTools
    {
        public static void RunInDispatcher(Dispatcher dispatcher, Action action)
        {
            RunInDispatcher(dispatcher, DispatcherPriority.Normal, action);
        }
    
            public static void RunInDispatcher(Dispatcher dispatcher, DispatcherPriority priority, Action action)
        {
            if (action == null) { return; }
    
            if (dispatcher.CheckAccess())
            {
                // we are already on thread associated with the dispatcher -> just call action
                try
                {
                    action();
                }
                catch (Exception ex)
                {
                    //Log error here!
                }
            }
            else
            {
                // we are on different thread, invoke action on dispatcher's thread
                dispatcher.BeginInvoke(
                    priority,
                    (Action)(
                    () =>
                    {
                        try
                        {
                            action();
                        }
                        catch (Exception ex)
                        {
                            //Log error here!
                        }
                    })
                );
            }
        }
    }
    

    关于wpf - CommandManager.InvalidateRequerySuggested() 不够快。我能做些什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1751966/

    相关文章:

    c# - C#中的中继命令

    wpf - CommandManager.InvalidateRequerySuggested() 不起作用

    wpf - 如何计算WPF中的字体高度?

    鼠标悬停和鼠标移出时的 wpf 工具提示

    c# - 使用 Entity Framework 在wpf中排序

    WPF 命令与事件触发命令

    android - 在哪个上下文中 ICommand 和 Local :Mvx are prefered

    wpf - 可以在字段更改时执行WPF命令

    WPF:有没有办法只使用 PrintDialog 来选择目标打印机而不必提供 DocumentPaginator 类?

    WPF 和 ClickOnce