Подтвердить что ты не робот

Ввод текста внутри текстового поля MVVM

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

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

Следующей попыткой, вероятно, будет создание вложенного свойства BindableCommand с PropertyChangedCallback, который обновляет команду. Это позволяет вам получить доступ к привязке, но нет способа использовать 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. Затем в коде "refresh" просто установите 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 переполнен. Как вы упомянули, можно просто обрабатывать события вручную.

Ответ 3

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

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

    <TextBox>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="KeyDown">
                <si:InvokeDataCommand Command="{Binding MyCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>

Ответ 4

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

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