RoutedUICommand PreviewExecuted Bug? - PullRequest
       5

RoutedUICommand PreviewExecuted Bug?

5 голосов
/ 17 февраля 2010

Я создаю приложение, используя шаблон проектирования MVVM, и я хочу использовать RoutedUICommands, определенные в классе ApplicationCommands. Так как свойство CommandBindings для View (чтение UserControl) не является DependencyProperty, мы не можем напрямую связывать CommandBindings, определенные в ViewModel, с View. Я решил это путем определения абстрактного класса View, который связывает это программно, на основе интерфейса ViewModel, который гарантирует, что каждый ViewModel имеет ObservableCollection of CommandBindings. Это все работает нормально, однако, в некоторых сценариях я хочу выполнить логику, которая определена в разных классах (View и 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 значение false. Таким образом, обработчик событий executeSave, который я определил в предыдущем примере кода, больше не выполняется. Обратите внимание, что когда я изменяю cb.PreviewExecuted на cb.Executed, обе части кода do выполняются, но не в правильном порядке.

Я думаю, что это ошибка в .Net, потому что вы должны иметь возможность добавить обработчик в PreviewExecuted и Executed и выполнять их по порядку, если вы не помечаете событие как обработанное.

Кто-нибудь может подтвердить это поведение? Или я не прав? Есть ли обходной путь для этой ошибки?

Ответы [ 2 ]

3 голосов
/ 22 февраля 2010

РЕДАКТИРОВАТЬ 2: глядя на исходный код, кажется, что внутри он работает так:

  1. UIElement вызывает CommandManager.TranslateInput() в ответ на пользовательский ввод (мышь или клавиатура).
  2. Затем CommandManager проходит через CommandBindings на разных уровнях в поисках команды, связанной с вводом.
  3. Когда команда найдена, вызывается ее метод CanExecute(), и если она возвращает true, вызывается Executed().
  4. В случае RoutedCommand каждый из методов делает по существу одно и то же - он вызывает пару прикрепленных событий CommandManager.PreviewCanExecuteEvent и CommandManager.CanExecuteEvent (или PreviewExecutedEvent и ExecutedEvent) на UIElement, который инициировал процесс. На этом заканчивается первый этап.
  5. Теперь в UIElement зарегистрированы обработчики классов для этих четырех событий, и эти обработчики просто вызывают CommandManager.OnCanExecute() и CommandManager.CanExecute() (как для предварительного просмотра, так и для реальных событий).
  6. Только здесь в CommandManager.OnCanExecute() и CommandManager.OnExecute() методах вызываются обработчики, зарегистрированные с CommandBinding. Если ничего не найдено, CommandManager передает событие до родительского элемента UIElement, и новый цикл начинается, пока команда не будет обработана или пока не будет достигнут корень визуального дерева.

Если вы посмотрите на исходный код класса CommandBinding, существует метод OnExecuted (), который отвечает за вызов обработчиков, которые вы регистрируете для событий PreviewExecuted и Executed через CommandBinding. Там есть этот бит:

PreviewExecuted(sender, e); 
e.Handled = true;

устанавливает событие как обработанное сразу после возврата обработчика PreviewExecuted, и поэтому Executed не вызывается.

РЕДАКТИРОВАТЬ 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, у вас есть два варианта:

  1. Вы можете вызвать Execute() метод перенаправленной команды из обработчика PreviewExecuted. Просто подумайте об этом - вы можете столкнуться с проблемами синхронизации при вызове обработчика Executed до завершения PreviewExecuted. Для меня это не похоже на хороший путь.
  2. Вы можете зарегистрировать обработчик PreviewExecuted отдельно с помощью CommandManager.AddPreviewExecutedHandler() статического метода. Это будет вызвано непосредственно из класса UIElement и не будет включать CommandBinding. EDIT 2: Look at the point 4 at the beginning of the post - these are the events we're adding the handlers for.

Судя по всему, это было сделано специально. Зачем? Можно только догадываться ...

1 голос
/ 23 февраля 2010

Я строю следующий обходной путь, чтобы получить недостающее поведение 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;
    }
}
...