我已经确定 ContextMenu 中至少有两个错误导致其 CanExecute 调用在不同情况下不可靠。设置命令后,它会立即调用 CanExecute。后来的电话是不可预测的,当然也不可靠。
有一次,我花了整整一夜的时间试图找出它会失败的确切条件并寻找解决方法。最后我放弃并切换到触发所需命令的 Click 处理程序。
我确实确定我的问题之一是更改 ContextMenu 的 DataContext 可能会导致 CanExecute 在新的 Command 或 CommandParameter 被绑定之前被调用。
我知道这个问题的最佳解决方案是使用您自己的 Command 和 CommandBinding 附加属性,而不是使用内置属性:
设置附加的 Command 属性后,订阅 MenuItem 上的 Click 和 DataContextChanged 事件,并订阅 CommandManager.RequerySuggested。
当 DataContext 更改、RequerySuggested 出现时,或者您的两个附加属性中的任何一个发生更改时,请使用 Dispatcher.BeginInvoke 安排调度程序操作,该操作将调用您的 CanExecute() 并更新 MenuItem 上的 IsEnabled。
当 Click 事件触发时,执行 CanExecute 操作,如果通过,则调用 Execute()。
用法就像常规的 Command 和 CommandParameter,但使用附加的属性:
<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" />
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" />
该解决方案有效并绕过了 ContextMenu 的 CanExecute 处理中的所有错误问题。
希望有一天微软会解决 ContextMenu 的问题,并且不再需要这种解决方法。我有一个复制案例坐在这里的某个地方,我打算提交给 Connect。也许我应该接球并真正做到这一点。
什么是 RequerySuggested,为什么要使用它?
RequerySuggested 机制是 RoutedCommand 有效处理 ICommand.CanExecuteChanged 的方式。在非 RoutedCommand 世界中,每个 ICommand 都有自己的 CanExecuteChanged 订阅者列表,但对于 RoutedCommand,订阅 ICommand.CanExecuteChanged 的任何客户端实际上都会订阅 CommandManager.RequerySuggested。这个更简单的模型意味着任何时候 RoutedCommand 的 CanExecute 可能发生变化,只需调用 CommandManager.InvalidateRequerySuggested(),这将执行与触发 ICommand.CanExecuteChanged 相同的操作,但同时在后台线程上对所有 RoutedCommand 执行此操作。此外,RequerySuggested 调用组合在一起,因此如果发生许多更改,则 CanExecute 只需要调用一次。
我建议您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged 的原因是:1. 您不需要代码来删除旧订阅并在每次命令附加属性的值更改时添加新订阅,以及 2. CommandManager.RequerySuggested 具有内置的弱引用功能,允许您设置事件处理程序并仍然被垃圾收集。对 ICommand 执行相同操作需要您实现自己的弱引用机制。
另一方面,如果您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged,您将只能获得 RoutedCommands 的更新。我专门使用 RoutedCommands,所以这对我来说不是问题,但我应该提到,如果你使用常规 ICommands,有时你应该考虑做一些额外的工作,即弱订阅 ICommand.CanExecutedChanged。请注意,如果您这样做,您也不需要订阅 RequerySuggested,因为 RoutedCommand.add_CanExecutedChanged 已经为您完成了这项工作。