2010-02-17 4 views
5

Я создаю приложение с использованием шаблона проектирования MVVM, и я хочу использовать RoutedUICommands, определенные в классе ApplicationCommands. Поскольку свойство CommandBindings для View (read UserControl) не является DependencyProperty, мы не можем напрямую связать CommandBindings, определенные в ViewModel, с представлением. Я решил это, определив абстрактный класс View, который связывает это программно, на основе интерфейса ViewModel, который гарантирует, что у каждого ViewModel есть ObservableCollection CommandBindings. Все это прекрасно работает, однако в некоторых сценариях я хочу выполнить логику, определенную в разных классах (View и ViewModel) той же самой командой. Например, при сохранении документа.RoutedUICommand PreviewExecuted Bug?

В 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 кажется отменить мероприятие в целом, даже когда я явно установить Обработано свойство false. Таким образом, обработчик события executeSave, который я определил в предыдущем примере кода, больше не выполняется. Обратите внимание, что когда я меняю cb.PreviewExecuted на cb.Сделал обе части кода do выполнить, но не в правильном порядке.

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

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

ответ

3

EDIT 2: Глядя на исходный код, кажется, что внутренне он работает так:

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

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

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

это устанавливает событие, как обрабатывается сразу же после вашего возвращения PreviewExecuted обработчика и поэтому Выполненная не называется.

EDIT 1: Глядя на CanExecute & PreviewCanExecute событий есть ключевое отличие:

PreviewCanExecute(sender, e); 
    if (e.CanExecute) 
    { 
    e.Handled = true; 
    } 

установка обрабатывались истина условна здесь и поэтому программист, который решает, следует ли продолжить CanExecute , Просто не устанавливайте CanExecute CanExecuteRoutedEventArgs в true в обработчике PreviewCanExecute и вызывается обработчик CanExecute.

Что касается ContinueRouting свойство события предварительного просмотра - если установлено значение false, это предотвращает событие предварительного просмотра от дальнейшей маршрутизации, но никак не влияет на следующее основное событие.

Обратите внимание, что он работает только таким образом, когда обработчики зарегистрированы через CommandBinding.

Если вы все еще хотите иметь как PreviewExecuted и Выполненный запустить у вас есть два варианта:

  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.

Из-за внешнего вида - это было сделано специально. Зачем? Можно только догадываться ...

+0

Сюжета утолщается ... Так что я смотрел на исходном коде вы упомянули, и они делают то же самое в OnCanExecute с PreviewCanExecute. Тем не менее, существует важное различие между CanExecuteRoutedEventArgs из OnCanExecute и ExecutedRoutedEventArgs из OnExecuted. Как и следовало ожидать, CanExecuteRoutedEventArgs содержит свойство ContinueRouting, которое делает именно это, но по какой-либо причине ExecutedRoutedEventArgs обойдется. На самом деле я действительно не могу обойти этот выбор от Microsoft. – elmar

+0

Я думаю, что ContinueRouting не участвует в этом процессе - см. Мой EDIT 2 в сообщении. Что касается того, почему они сделали это так ...Посмотрите на две части метода CommandBinding.OnExecuted(), они почти точно такие же - это может быть классический случай копирования/вставки :), а затем это ошибка. Серьезно, хотя, я не думаю, что это так. Мне действительно нравится знать, в чем их причина. –

1

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