我已经确定在ContextMenu中至少存在两个错误,导致其CanExecute调用在不同情况下不可靠。它会在设置Command时立即调用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处理中出现的所有问题。
希望有一天Microsoft能够解决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内置了弱引用功能,允许您设置事件处理程序并仍然进行垃圾回收。如果使用ICommand执行相同的操作,则需要实现自己的弱引用机制。
这个方法的一个缺点是,如果您订阅CommandManager.RequerySuggested而不是ICommand.CanExecuteChanged,那么您只会得到RoutedCommands的更新。我专门使用RoutedCommands,因此对我来说这不是问题,但我应该提到,如果有时您也使用常规的ICommands,应该考虑额外工作,弱订阅ICommand.CanExecutedChanged。请注意,如果这样做,您不需要订阅RequerySuggested,因为RoutedCommand.add_CanExecutedChanged已经为您完成了这项工作。
CanExecute
。 - Cubi73