Ввод текста внутри текстового поля MVVM
Я только начинаю с MVVM, и у меня возникают проблемы с выяснением того, как я могу привязать нажатие клавиши внутри текстового поля к ICommand внутри модели представления. Я знаю, что могу сделать это в коде, но я стараюсь избегать этого как можно больше.
Обновление. Решения до сих пор хорошо и хорошо, если у вас есть blend sdk или у вас нет проблем с DLL взаимодействия, что и есть у меня. Есть ли другие более общие решения, чем использовать blend sdk?
Ответы
Ответ 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.