Я создаю приложение с помощью шаблона разработки MVVM, и я хочу использовать RoutedUICommands, определенный в классе ApplicationCommands. Так как свойством CommandBindings Представления (читает UserControl) не является DependencyProperty, мы не можем связать CommandBindings, определенный в ViewModel к Представлению непосредственно. Я решил это путем определения абстрактного класса Представления, который связывает это программно, на основе интерфейса ViewModel, который гарантирует, что каждый ViewModel имеет ObservableCollection CommandBindings. Это все хорошо работает, однако, в некоторых сценариях я хочу выполнить логику, которая определяется в различных классах (Представление и ViewModel) та же команда. Например, при сохранении документа.
В ViewModel код сохраняет документ на диск:
private void InitializeCommands()
{
CommandBindings = new CommandBindingCollection();
ExecutedRoutedEventHandler executeSave = (sender, e) =>
{
document.Save(path);
IsModified = false;
};
CanExecuteRoutedEventHandler canSave = (sender, e) =>
{
e.CanExecute = IsModified;
};
CommandBinding save = new CommandBinding(ApplicationCommands.Save, executeSave, canSave);
CommandBindings.Add(save);
}
На первый взгляд предыдущий код - все, что я хотел сделать, но TextBox в Представлении, с которым связывается документ, только обновляет свой Источник, когда он теряет свой фокус. Однако я могу сохранить документ, не теряя фокус путем нажатия Ctrl+S. Это означает, что документ сохраняется перед изменениями, где Обновлено в источнике, эффективно игнорируя изменения. Но начиная с изменения UpdateSourceTrigger к PropertyChanged не жизнеспособный вариант по причинам производительности, что-то еще должно вызвать обновление перед сохранением. Таким образом, я думал, позволяет, используют событие PreviewExecuted для принуждения обновления в событии PreviewExecuted, как так:
//Find the Save command and extend behavior if it is present
foreach (CommandBinding cb in CommandBindings)
{
if (cb.Command.Equals(ApplicationCommands.Save))
{
cb.PreviewExecuted += (sender, e) =>
{
if (IsModified)
{
BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
}
e.Handled = false;
};
}
}
Однако присвоение обработчика к событию PreviewExecuted, кажется, отменяет событие в целом, даже когда я явно установил свойство Handled на ложь. Таким образом, executeSave eventhandler, который я определил в предыдущем примере кода, больше не выполняется. Обратите внимание на это, когда я изменю cb. PreviewExecuted к cb. Выполняемый обе части кода выполняются, но не в правильном порядке.
Я думаю, что это - Ошибка в .NET, потому что необходимо смочь добавить обработчик к PreviewExecuted и Выполняемый и сделать, чтобы они были выполнены в порядке, если Вы не отмечаете событие, как обработано.
Кто-либо может подтвердить это поведение? Или я неправильно? Существует ли обходное решение для этой Ошибки?
EDIT 2: Из просмотра исходного кода кажется, что внутренне это работает так:
- Элемент
UIElement
вызываетCommandManager.TranslateInput()
в ответ на ввод пользователя (мышь или клавиатура).- Затем
CommandManager
проходит черезCommandBindings
на разных уровнях в поисках команды, связанной с вводом.- Когда команда найдена, вызывается ее метод
CanExecute()
, и если он возвращаетtrue
, вызываетсяExecuted()
.- В случае
RoutedCommand
каждый из методов делает по сути одно и то же - вызывает пару присоединенных событийCommandManager.PreviewCanExecuteEvent
иCommandManager. CanExecuteEvent
(илиPreviewExecutedEvent
иExecutedEvent
) наUIElement
, который инициировал процесс. На этом первая фаза завершена.- Теперь в
UIElement
зарегистрированы обработчики класса для этих четырех событий, и эти обработчики просто вызываютCommandManager.OnCanExecute()
иCommandManager.CanExecute()
(как для событий предварительного просмотра, так и для фактических событий).- Только здесь в методах
CommandManager.OnCanExecute()
иCommandManager.OnExecute()
вызываются обработчики, зарегистрированные вCommandBinding
. Если таковых не найдено,CommandManager
передает событие родителюUIElement
, и начинается новый цикл, пока команда не будет обработана или не будет достигнут корень визуального дерева.
Если вы посмотрите на исходный код класса CommandBinding, там есть метод OnExecuted(), который отвечает за вызов обработчиков, зарегистрированных вами для событий PreviewExecuted и Executed через CommandBinding. Там есть такой бит:
PreviewExecuted(sender, e);
e.Handled = true;
это устанавливает событие как обработанное сразу после возврата обработчика PreviewExecuted и поэтому Executed не вызывается.
EDIT 1: При рассмотрении событий CanExecute и PreviewCanExecute есть ключевое различие:
PreviewCanExecute(sender, e); if (e.CanExecute) { e.Handled = true; }
установка Handled в true здесь условна, и поэтому программист решает, продолжать или нет выполнение CanExecute. Просто не устанавливайте значение CanExecute для CanExecuteRoutedEventArgs в true в обработчике PreviewCanExecute, и обработчик CanExecute будет вызван.
Что касается свойства
ContinueRouting
события Preview - при установке значения false оно предотвращает дальнейшую маршрутизацию события Preview, но никак не влияет на следующее основное событие.
Обратите внимание, что это работает только в том случае, если обработчики зарегистрированы через CommandBinding.
Если вы все же хотите, чтобы выполнялись и PreviewExecuted, и Executed, у вас есть два варианта:
Execute()
метод маршрутизируемой команды из обработчика PreviewExecuted. Просто подумав об этом, вы можете столкнуться с проблемами синхронизации, поскольку вы вызываете обработчик Executed до того, как PreviewExecuted завершен. На мой взгляд, это не очень хороший способ. CommandManager.AddPreviewExecutedHandler()
. Он будет вызываться непосредственно из класса UIElement и не будет задействовать CommandBinding. EDIT 2: Посмотрите на пункт 4 в начале поста - это события, для которых мы добавляем обработчики.
Судя по всему, так сделано специально. Почему? Можно только догадываться...
я строю следующую работу, чтобы получить недостающее поведение ContinueRouting:
foreach (CommandBinding cb in CommandBindings)
{
if (cb.Command.Equals(ApplicationCommands.Save))
{
ExecutedRoutedEventHandler f = null;
f = (sender, e) =>
{
if (IsModified)
{
BindingExpression be = rtb.GetBindingExpression(TextBox.TextProperty);
be.UpdateSource();
// There is a "Feature/Bug" in .Net which cancels the route when adding PreviewExecuted
// So we remove the handler and call execute again
cb.PreviewExecuted -= f;
cb.Command.Execute(null);
}
};
cb.PreviewExecuted += f;
}
}