Я определил, что в ContextMenu есть как минимум две ошибки, из-за которых его вызовы CanExecute ненадежны в различных обстоятельствах. Он вызывает CanExecute немедленно, когда команда установлена. Последующие звонки непредсказуемы и, конечно, ненадежны.
Однажды я провел целую ночь, пытаясь отследить точные условия, при которых он потерпит неудачу, и искал обходной путь. Наконец я сдался и переключился на обработчики Click, которые запускали нужные команды.
Я определил, что одной из моих проблем было то, что изменение DataContext в ContextMenu может привести к вызову CanExecute до того, как будет привязан новый Command или CommandParameter.
Лучшее решение этой проблемы, которое я знаю, - это использовать ваши собственные присоединенные свойства для Command и CommandBinding вместо использования встроенных:
Когда установлено ваше присоединенное свойство Command, подпишитесь на события Click и DataContextChanged в MenuItem, а также подпишитесь на CommandManager.RequerySuggested.
Когда DataContext изменяется, вступает в силу RequerySuggested или изменяется любое из двух ваших вложенных свойств, запланируйте операцию диспетчера с помощью Dispatcher.BeginInvoke, который будет вызывать ваш CanExecute () и обновлять IsEnabled для MenuItem.
Когда срабатывает событие Click, выполните действие CanExecute, а если оно пройдет, вызовите Execute ().
Использование аналогично обычным Command и CommandParameter, но вместо этого используется присоединенное свойство:
<Setter Property="my:ContexrMenuFixer.Command" Value="{Binding}" />
<Setter Property="my:ContextMenuFixer.CommandParameter" Value="{Binding Source=... }" />
Это решение работает и обходит все проблемы с ошибками в обработке CanExecute ContextMenu.
Надеюсь, когда-нибудь Microsoft исправит проблемы с ContextMenu, и этот обходной путь больше не понадобится. У меня тут где-то есть репродукция, которую я собираюсь передать в Connect. Возможно, я должен взять мяч и действительно сделать это.
Что такое RequerySuggested и зачем его использовать?
Механизм RequerySuggested - это способ RoutedCommand для эффективной обработки ICommand.CanExecuteChanged. В мире, отличном от RoutedCommand, каждая ICommand имеет свой собственный список подписчиков CanExecuteChanged, но для RoutedCommand любой клиент, подписавшийся на ICommand.CanExecuteChanged, фактически подписывается на CommandManager.RequerySuggested. Эта более простая модель означает, что каждый раз, когда CanExecute RoutedCommand может измениться, все, что необходимо, это вызвать 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 уже сделает это за вас.