即使设置了CommandParameter,ICommand.CanExecute也会传递null

我有一个棘手的问题,我将ContextMenu绑定到一组ICommand派生的对象,并通过样式在每个MenuItem上设置CommandCommandParameter属性:

       ... 

但是,当ICommand.Execute( object )按原样传递选定的注释集时, ICommand.CanExecute( object ) (在创建菜单时调用)将被传递为null。 我已经检查过并且在调用之前正确地实例化了所选的注释集合(实际上它在其声明中被赋值,因此它永远不会为null )。 我无法弄清楚CanEvaluate为何被传递为null

我已经确定ContextMenu中至少有两个错误会导致其CanExecute调用在不同情况下不可靠。 它在设置命令时立即调用CanExecute。 后来的电话是不可预测的,当然不可靠。

我花了整整一夜的时间试图找出它失败的确切条件并寻找解决方法。 最后,我放弃并切换到触发了所需命令的Click处理程序。

我确定我的一个问题是更改ContextMenu的DataContext会导致在绑定新的Command或CommandParameter之前调用CanExecute。

我知道这个问题的最佳解决方案是使用自己的Command和CommandBinding附加属性,而不是使用内置的属性:

  • 设置附加的Command属性后,订阅MenuItem上的Click和DataContextChanged事件,并订阅CommandManager.RequerySuggested。

  • 当DataContext更改,RequerySuggested进入,或者您的两个附加属性发生更改时,使用Dispatcher.BeginInvoke调度调度程序操作,该调用将调用CanExecute()并更新MenuItem上的IsEnabled。

  • 触发Click事件时,执行CanExecute事件,如果它通过,则调用Execute()。

用法就像常规的Command和CommandParameter一样,但使用附加的属性:

   

此解决方案可以解决ContextMenu的CanExecute处理中的错误所带来的所有问题。

希望有一天微软将解决ContextMenu的问题,这种解决方法将不再是必要的。 我有一个责备案例坐在这里,我打算提交给Connect。 也许我应该接受球并且实际上做到了。

什么是RequerySuggested,为什么要使用它?

RequerySuggested机制是RoutedCommand有效处理ICommand.CanExecuteChanged的方法。 在非RoutedCommand世界中,每个ICommand都有自己的CanExecuteChanged订户列表,但对于RoutedCommand,任何订阅ICommand.CanExecuteChanged的客户端实际上都会订阅CommandManager.RequerySuggested。 这个更简单的模型意味着只要RoutedCommand的CanExecute可能发生变化,所有必要的就是调用CommandManager.InvalidateRequerySuggested(),它将执行与触发ICommand.CanExecuteChanged相同的操作,但同时为所有RoutedCommands和后台线程执行此操作。 此外,将RequerySuggested调用组合在一起,以便在发生许多更改时,只需要调用一次CanExecute。

我建议您订阅CommandManager.RequerySuggested而不是ICommand.CanExecuteChanged的原因是:1。每次Command附加属性的值更改时,您不需要代码来删除旧订阅并添加新订阅,以及2。 CommandManager.RequerySuggested内置了一个弱引用function,允许您设置事件处理程序并仍然是垃圾回收。 对ICommand执行相同操作需要您实现自己的弱引用机制。

另一方面,如果您订阅CommandManager.RequerySuggested而不是ICommand.CanExecuteChanged,那么您将只获得RoutedCommands的更新。 我只使用RoutedCommands这对我来说不是问题,但我应该提到,如果你经常使用常规ICommands,你应该考虑做一些弱订阅ICommand.CanExecutedChanged的额外工作。 请注意,如果您这样做,您也不需要订阅RequerySuggested,因为RoutedCommand.add_CanExecutedChanged已经为您执行此操作。

我相信这与此处记录的连接问题有关:

https://connect.microsoft.com/VisualStudio/feedback/details/504976/command-canexecute-still-not-requeried-after-commandparameter-change?wa=wsignin1.0

我的解决方法如下:

  1. 使用附加的依赖项属性创建一个带有绑定命令参数的静态类
  2. 创建自定义界面以在自定义命令上手动引发CanExecuteChanged
  3. 在需要了解参数更改的每个命令中实现接口。

     public interface ICanExecuteChanged : ICommand { void RaiseCanExecuteChanged(); } public static class BoundCommand { public static object GetParameter(DependencyObject obj) { return (object)obj.GetValue(ParameterProperty); } public static void SetParameter(DependencyObject obj, object value) { obj.SetValue(ParameterProperty, value); } public static readonly DependencyProperty ParameterProperty = DependencyProperty.RegisterAttached("Parameter", typeof(object), typeof(BoundCommand), new UIPropertyMetadata(null, ParameterChanged)); private static void ParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var button = d as ButtonBase; if (button == null) { return; } button.CommandParameter = e.NewValue; var cmd = button.Command as ICanExecuteChanged; if (cmd != null) { cmd.RaiseCanExecuteChanged(); } } } 

命令实现:

  public class MyCustomCommand : ICanExecuteChanged { public void Execute(object parameter) { // Execute the command } public bool CanExecute(object parameter) { Debug.WriteLine("Parameter changed to {0}!", parameter); return parameter != null; } public event EventHandler CanExecuteChanged; public void RaiseCanExecuteChanged() { EventHandler temp = this.CanExecuteChanged; if (temp != null) { temp(this, EventArgs.Empty); } } } 

Xaml用法:

   

这是我能想到的最简单的修复方法,它可以解决MVVM风格的实现问题。 您还可以在BoundCommand参数更改中调用CommandManager.InvalidateRequerySuggested(),以便它也适用于RoutedCommands。

我在DataGrid上遇到了这种情况,我需要上下文菜单来识别是否根据所选行启用或禁用特定命令。 我发现是的,传递给命令的对象是null,并且只对所有行执行一次,无论是否有更改。

我所做的是在特定命令上调用RaiseCanExecuteChanged ,这将触RaiseCanExecuteChanged格选择更改事件中的启用或禁用。


 private void MyGrid_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { VM.DeleteItem.RaiseCanExecuteChanged(); } 

命令绑定分配

 VM.DeleteItem = new OperationCommand((o) => MessageBox.Show("Delete Me"), (o) => (myGrid.SelectedItem as Order)?.InProgress == false ); 

结果

如果InProgresstrue则不启用delete命令

在此处输入图像描述

XAML