CommandManager.InvalidateRequerySuggested() 不够快。我该怎么办?

34

短版本

CommandManager.InvalidateRequerySuggested()调用需要比我想象中的时间更长(在UI控件变成禁用状态之前需要1-2秒的延迟)。

长版本

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

当此提交发生时,管理我的后台线程的对象会执行两件事:

  1. 它会触发一个“忙碌”事件(仍在UI线程上),几个视图模型对此做出响应;当它们接收到此事件时,它们将自己的IsEnabled标志设置为false。我的视图中的控件与此属性进行数据绑定,这些控件立即灰显,这是我所期望的。

  2. 它通知我的WPF ICommand对象不允许执行(同样在UI线程上)。因为没有像INotifyPropertyChanged这样的东西可以用于ICommand对象,所以我被迫调用CommandManager.InvalidateRequerySuggested()强制WPF重新考虑所有我的命令对象的CanExecute状态(是的,我真的需要这样做:否则,这些控件都不会变成禁用状态)。不过与第1项不同的是,使用ICommand对象的按钮/菜单项等在视觉上变为禁用状态所需的时间要比手动设置其IsEnabled属性的UI控件长得多。

问题在于,从用户体验的角度来看,这看起来非常糟糕;我的一半控件立即变为灰显状态(因为它们的IsEnabled属性被设置为false),然后整整1-2秒钟后,另一半控件才跟进变为禁用状态(因为它们的CanExecute方法最终重新评估)。

所以,我的问题的第1部分:

尽管听起来有些愚蠢,但我能否让 CommandManager.InvalidateRequerySuggested() 更快地完成它的工作?我怀疑是不可能的。

好吧,问题的第二部分:
那么我该如何解决这个问题?我希望所有的控件在同一时间被禁用。否则看起来就不太专业和别扭。有什么想法吗?:-)

7个回答

59
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事件,这些事件会影响它是否可以执行,并且只在需要重新查询时才调用检查。现在,有很多代码(我们内部框架的一部分)来完成这个操作:我使用Prism中的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扩展方法,将命令与属性“绑定”并调用其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事件的标准接口):

public interface INotifyPropertyChangedWithRaise : INotifyPropertyChanged
{
    void OnPropertyChanged(string propertyName);
}

最后一块拼图是这个:
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!
                    }
                })
            );
        }
    }
}

哇!非常感谢您,这看起来非常有帮助。我迫不及待想试试它 :-)! - Rob
不用谢,这是相当多的代码,但你可以使用OnPropertyChanged扩展方法来触发NotifyPropertyChanged - 例如:this.OnPropertyChanged(n => n.MyProperty)。唯一的问题是,事件监听器可能会在某些情况下导致内存泄漏 - 它应该使用一些弱事件监听器而不是observedObject.PropertyChanged += ...,但我还没有找到时间去理解所有涉及其中的.NET内部机制。 - Tomáš Kafka
1
+1,答案很棒!我特别喜欢“ListenOn”的想法,非常聪明和优雅。 - Thomas Levesque
@TomášKafka 看起来是个不错的解决方案,但如果我只调用一个命令的 RaiseCanExecuteChanged 方法,所有其他命令也会被更新,看起来它做的事情与 InvalidateRequerySuggested 相同。你有什么想法吗?我没有使用 ListenOn,而是在 UI 调度程序上调用 RaiseCanExecuteChanged。 - stijn

9
这个解决方案是Tomáš Kafka在这个讨论中提出的解决方案的简化版本(感谢Tomas详细描述他的解决方案)。
在Tomas的解决方案中,他有: 1)DelegateCommand 2)CommandManagerHelper 3)DelegateCommandExtensions 4)NotifyPropertyChangedBaseExtensions 5)INotifyPropertyChangedWithRaise 6)ThreadTools
而这个解决方案中只有: 1)DelegateCommand 2)DelegateCommandExtensions方法和NotifyPropertyChangedBaseExtensions方法在Delegate Command本身中。
注意:由于我们的WPF应用程序遵循MVVM模式,并且我们在viewmodel级别处理命令,这些命令在UI线程中执行,因此我们不需要获取对UI disptacher的引用。
   using System;
   using System.Collections.Generic;
   using System.ComponentModel;
   using System.Linq.Expressions;
   using System.Reflection;
   using System.Windows.Input;
   namespace ExampleForDelegateCommand
   {
   public class DelegateCommand : ICommand
   {

    public Predicate<object> CanExecuteDelegate { get; set; }

    private List<INotifyPropertyChanged> propertiesToListenTo;
    private List<WeakReference> ControlEvent;

    public DelegateCommand()
    {
        ControlEvent= new List<WeakReference>();
    }

    public List<INotifyPropertyChanged> PropertiesToListenTo
    {
        get { return propertiesToListenTo; }
        set
        {
            propertiesToListenTo = value;
        }
    }

    private Action<object> executeDelegate;

    public Action<object> ExecuteDelegate
    {
        get { return executeDelegate; }
        set
        {
            executeDelegate = value;
            ListenForNotificationFrom((INotifyPropertyChanged)executeDelegate.Target);
        }
    }

    public static ICommand Create(Action<object> exec)
    {
        return new SimpleCommand { ExecuteDelegate = exec };
    }



    #region ICommand Members


    public bool CanExecute(object parameter)
    {
        if (CanExecuteDelegate != null)
            return CanExecuteDelegate(parameter);
        return true; // if there is no can execute default to true
    }

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            ControlEvent.Add(new WeakReference(value));
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
            ControlEvent.Remove(ControlEvent.Find(r => ((EventHandler) r.Target) == value));
        }
    }

    public void Execute(object parameter)
    {
        if (ExecuteDelegate != null)
            ExecuteDelegate(parameter);
    }
     #endregion

    public void RaiseCanExecuteChanged()
    {
        if (ControlEvent != null && ControlEvent.Count > 0)
        {
            ControlEvent.ForEach(ce =>
                                     {
                                         if(ce.Target!=null)
                                         ((EventHandler) (ce.Target)).Invoke(null, EventArgs.Empty);
                                     });
        }
    }



    public DelegateCommand ListenOn<TObservedType, TPropertyType>(TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression) where TObservedType : INotifyPropertyChanged
    {
        string propertyName = GetPropertyName(propertyExpression);
        viewModel.PropertyChanged += (PropertyChangedEventHandler)((sender, e) =>
        {
            if (e.PropertyName == propertyName) RaiseCanExecuteChanged();
        });
        return this;
    }

    public void ListenForNotificationFrom<TObservedType>(TObservedType viewModel) where TObservedType : INotifyPropertyChanged
    {
        viewModel.PropertyChanged += (PropertyChangedEventHandler)((sender, e) =>
        {
           RaiseCanExecuteChanged();
        });
    }

    private string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> expression) where T : INotifyPropertyChanged
    {
        var lambda = expression as LambdaExpression;
        MemberInfo memberInfo = GetmemberExpression(lambda).Member;
        return memberInfo.Name;
    }

    private MemberExpression GetmemberExpression(LambdaExpression lambda)
    {
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
            memberExpression = lambda.Body as MemberExpression;
        return memberExpression;
    }

}}
解决方案的说明: 通常,当我们将UI元素(按钮)绑定到ICommand实现时,WPF按钮会在ICommand实现中注册事件“CanExecuteChanged”。如果您的Icommand实现钩住了CommandManager的RequestSuggest事件(阅读本文http://joshsmithonwpf.wordpress.com/2008/06/17/allowing-commandmanager-to-query-your-icommand-objects/),那么每当CommandManager检测到可能改变命令执行能力的条件(例如焦点转移和某些键盘事件)时,就会发生CommandManager的RequerySuggested事件,这将导致调用Button的委托,因为我们在DelegateCommand的CanExecuteChanged实现中将按钮的委托挂钩到了CommandManager的RequerySuggested上。
但问题在于CommandManager无法始终检测到更改。因此,解决方案是当我们的命令实现(DelegateCommand)检测到有更改时,引发“CanExecuteChanged”。通常情况下,当我们在viewmodel中声明ICommand的委托时,我们将绑定到在viewmodel中声明的属性,并且我们的ICommand实现可以监听这些属性上的“propertychanged”事件。这就是DelegateCommand的“ListenForNotificationFrom”方法所做的。如果客户端代码没有注册特定的属性更改,则DelegateCommand默认会监听在命令所在和定义的视图模型上的任何属性更改。
DelegateCommand中的“ControlEvent”是一个存储按钮“CanExecuteChange EventHandler”的EventHandler列表,并被声明为弱引用以避免内存泄漏。
ViewModel如何使用这个DelegateCommand? 有两种使用方式。 (第二种用法更加具体,可以监听你想要命令监听的属性。)
delegateCommand = new DelegateCommand
                                      {
                                          ExecuteDelegate = Search,
                                          CanExecuteDelegate = (r) => !IsBusy
                                      };

          anotherDelegateCommand = new DelegateCommand
                                      {
                                          ExecuteDelegate = SearchOne,
                                          CanExecuteDelegate = (r) => !IsBusyOne
                                      }.ListenOn(this, n => n.IsBusyOne);

一个详细的视图模型
  public class ExampleViewModel
 {
   public SearchViewModelBase()
    {
        delegateCommand = new DelegateCommand
                                      {
                                          ExecuteDelegate = Search,
                                          CanExecuteDelegate = (r) => !IsBusy
                                      };

          anotherDelegateCommand = new DelegateCommand
                                      {
                                          ExecuteDelegate = SearchOne,
                                          CanExecuteDelegate = (r) => !IsBusyOne
                                      }.ListenOn(this, n => n.IsBusyOne);
  }
  private bool isBusy;
   public virtual bool IsBusy
    {
        get { return isBusy; }
        set
        {
            if (isBusy == value) return;
            isBusy = value;
            NotifyPropertyChanged(MethodBase.GetCurrentMethod());
        }
    }
    private bool isBusyOne;
     public virtual bool IsBusyOne
    {
        get { return isBusyOne; }
        set
        {
            if (isBusyOne == value) return;
            isBusyOne = value;
            NotifyPropertyChanged(MethodBase.GetCurrentMethod());
        }
    }


    private void Search(object obj)
    {
        IsBusy = true;
        new SearchService().Search(Callback);
    }  
    public void Callback(ServiceResponse response)
    {
        IsBusy = false;
    }  

    private void Search(object obj)
    {
        IsBusyOne = true;
        new SearchService().Search(CallbackOne);
    }  
    public void CallbackOne(ServiceResponse response)
    {
        IsBusyOne = false;
    }          
    private void NotifyPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    } 

    private void NotifyPropertyChanged(MethodBase methodBase)
    {
        string methodName = methodBase.Name;

        if (!methodName.StartsWith("set_"))
        {
            var ex = new ArgumentException("MethodBase must refer to a Property Setter method.");
            throw ex;
        }
        NotifyPropertyChanged(methodName.Substring(4));
    }

}


我正在使用你的解决方案,但在某些情况下,我并不确定,它无法正常工作。我必须调用InvalidateRequerySuggested。你知道是什么原因导致你的解决方案无法正常工作吗? - MTR

1

Tomas提供了一个不错的解决方案,但请注意存在一个严重的bug,由于以下原因,当将CanExecute绑定到按钮时,它不会始终触发:

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

传递的“null”参数会导致CanExecuteChangedEventManager(由WPF Button类用于侦听绑定到其上的任何命令的更改)出现问题。具体而言,CanExecuteChangedEventManager维护了一个弱事件集合,需要调用它们以确定命令是否可执行,但此集合以“发送方”为键。
修复方法很简单,对我有效-将签名更改为
internal static void CallWeakReferenceHandlers(ICommand sender, List<WeakReference> handlers)
{
....
           handler(sender, EventArgs.Empty);
 }

抱歉我没有描述得太好 - 在花了几个小时弄清楚之后,现在有点匆忙地赶上我的开发进度!


1

有没有办法让CommandManager.InvalidateRequerySuggested()更快地工作?

是的,有一种方法可以使其更快!

  1. 实现Command以保留/缓存CanExecuteState在一个布尔变量中。
  2. 实现RaiseCanExecuteChanged方法来重新计算CanExecuteState,如果它真的改变了就触发CanExecuteChanged事件。
  3. 实现CanExecute方法来简单地返回CanExecuteState
  4. 当调用InvalidateRequerySuggested方法时,Command订阅者将只通过调用CanExecute方法读取CanExecuteState变量并检查它是否已更改。那几乎没有开销。所有的Commands将几乎同时被禁用/启用。
  5. 所有的工作都将在RaiseCanExecuteChanged方法中完成,该方法将仅针对有限的一组Commands调用一次。

1
我建议您研究ReactiveUI,特别是它提供的ICommand实现ReactiveCommand。它使用了一种不同的方法,与DelegateCommand/RelayCommand不同,它们是通过委托实现CanExecute的,必须积极评估。ReactiveCommand的CanExecute值使用IObservables推送。

我考虑过这个问题,但最终得出的结论是我不想触发该事件,而不仅仅是过滤掉它然后不加处理。 - eran otzap

0

尝试编写自己的绑定器,在转换函数内调用RaiseCanExecuteChanged(),这样会更容易。


0

仅澄清一下:

  1. Command属性更改时,您想触发CanExecute的更新
  2. 创建自己的绑定类来检测Command属性中的更改,然后调用RaiseCanExecuteChanged()
  3. CommandParameter中使用此绑定

对我有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接