Нажатие клавиши внутри текстового поля MVVM - PullRequest
13 голосов
/ 10 ноября 2009

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

Обновление: решения пока что все хорошо, если у вас есть blend sdk или у вас нет проблем с dll взаимодействия, что у меня и есть. Есть ли другие более общие решения, чем использование blend sdk?

Ответы [ 4 ]

9 голосов
/ 13 ноября 2009

Прежде всего, если вы хотите связать RoutedUICommand, это просто - просто добавьте в коллекцию UIElement.InputBindings:

<TextBox ...>
  <TextBox.InputBindings>
    <KeyBinding
      Key="Q"
      Modifiers="Control" 
      Command="my:ModelAirplaneViewModel.AddGlueCommand" />

Ваша проблема начинается, когда вы пытаетесь установить Command = "{Binding AddGlueCommand}", чтобы получить ICommand из ViewModel. Поскольку Command не является DependencyProperty, вы не можете установить для него привязку.

Ваша следующая попытка, вероятно, будет заключаться в создании присоединенного свойства BindableCommand с PropertyChangedCallback, которое обновляет Command. Это позволяет вам получить доступ к привязке, но нет способа использовать FindAncestor для поиска вашей ViewModel, поскольку коллекция InputBindings не устанавливает InheritanceContext.

Очевидно, что вы можете создать прикрепленное свойство, которое вы можете применить к TextBox, которое будет проходить через все InputBindings, вызывая BindingOperations.GetBinding для каждого, чтобы найти привязки команд и обновить эти привязки с явным источником, что позволит вам сделать это:

<TextBox my:BindingHelper.SetDataContextOnInputBindings="true">
  <TextBox.InputBindings>
    <KeyBinding
      Key="Q"
      Modifiers="Control" 
      my:BindingHelper.BindableCommand="{Binding ModelGlueCommand}" />

Это присоединенное свойство было бы легко реализовать: на PropertyChangedCallback было бы запланировано «обновление» в DispatcherPriority.Input и настроено событие, чтобы «обновление» было перепланировано при каждом изменении DataContext. Затем в «обновленном» коде просто установите DataContext для каждой InputBinding:

...
public static readonly SetDataContextOnInputBindingsProperty = DependencyProperty.Register(... , new UIPropetyMetadata
{
   PropertyChangedCallback = (obj, e) =>
   {
     var element = obj as FrameworkElement;
     ScheduleUpdate(element);
     element.DataContextChanged += (obj2, e2) =>
     {
       ScheduleUpdate(element);
     };
   }
});
private void ScheduleUpdate(FrameworkElement element)
{
  Dispatcher.BeginInvoke(DispatcherPriority.Input, new Action(() =>
  {
    UpdateDataContexts(element);
  })
}

private void UpdateDataContexts(FrameworkElement target)
{
  var context = target.DataContext;
  foreach(var inputBinding in target.InputBindings)
    inputBinding.SetValue(FrameworkElement.DataContextProperty, context);
}

Альтернативой двум прикрепленным свойствам может быть создание подкласса CommandBinding, который получает перенаправленную команду и активирует связанную команду:

<Window.CommandBindings>
  <my:CommandMapper Command="my:RoutedCommands.AddGlue" MapToCommand="{Binding AddGlue}" />
  ...

в этом случае InputBindings в каждом объекте будет ссылаться на перенаправленную команду, а не на привязку. Затем эта команда будет направлена ​​вверх по представлению и отображена.

Код для CommandMapper относительно тривиален:

public class CommandMapper : CommandBinding
{
  ... // declaration of DependencyProperty 'MapToCommand'

  public CommandMapper() : base(Executed, CanExecute)
  {
  }
  private void Executed(object sender, ExecutedRoutedEventArgs e)
  {
    if(MapToCommand!=null)
      MapToCommand.Execute(e.Parameter);
  }
  private void CanExecute(object sender, CanExecuteRoutedEventArgs e)
  {
    e.CanExecute =
      MapToCommand==null ? null :
      MapToCommand.CanExecute(e.Parameter);
  }
}

На мой вкус, я бы предпочел использовать решение с присоединенными свойствами, так как оно не так много кода и не позволяет мне объявлять каждую команду дважды (как RoutedCommand и как свойство моей ViewModel). Вспомогательный код встречается только один раз и может использоваться во всех ваших проектах.

С другой стороны, если вы делаете только одноразовый проект и не ожидаете что-либо повторно использовать, возможно, даже CommandMapper излишний. Как вы упомянули, можно просто обрабатывать события вручную.

6 голосов
/ 13 февраля 2010

Отличная среда WPF Caliburn прекрасно решает эту проблему.

        <TextBox cm:Message.Attach="[Gesture Key: Enter] = [Action Search]" />

Синтаксис [Action Search] связывается с методом в модели представления. Совсем нет необходимости в ICommands.

4 голосов
/ 11 ноября 2009

Возможно, самым простым переходом от обработки событий с выделенным кодом к командам MVVM были бы триггеры и действия из Образцы смеси выражений .

Вот фрагмент кода, который демонстрирует, как вы можете обработать событие нажатия клавиши внутри текстового поля с помощью команды:

    <TextBox>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="KeyDown">
                <si:InvokeDataCommand Command="{Binding MyCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>
1 голос
/ 10 ноября 2009

Лучшим вариантом, вероятно, будет использование Attached Property для этого. Если у вас есть Blend SDK, класс Behavior<T> делает это намного проще.

Например, было бы очень легко изменить это Поведение TextBox для запуска ICommand при каждом нажатии клавиши вместо нажатия кнопки Enter.

...