我已确定 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 类似,但使用附加属性:
<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" />
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" />
该解决方案有效并绕过了 ContextMenu 的 CanExecute 处理中的错误的所有问题。
希望有一天 Microsoft 能够解决 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. 您不需要代码来删除旧订阅并在每次 Command 附加属性的值发生变化时添加新订阅,2. CommandManager.RequerySuggested 具有内置的弱引用功能,允许您设置事件处理程序并仍然被垃圾收集。对 ICommand 执行相同操作需要您实现自己的弱引用机制。
另一方面,如果您订阅 CommandManager.RequerySuggested 而不是 ICommand.CanExecuteChanged,您将只能获得 RoutedCommands 的更新。我专门使用 RoutedCommands,因此这对我来说不是问题,但我应该提到,如果您有时使用常规 ICommand,则应该考虑执行弱订阅 ICommand.CanExecutedChanged 的额外工作。请注意,如果您执行此操作,则无需订阅 RequerySuggested,因为 RoutedCommand.add_CanExecutedChanged 已为您执行此操作。