views:

1418

answers:

4

I am just getting started with MVVM and im having problems figuring out how I can bind a key press inside a textbox to an ICommand inside the view model. I know I can do it in the code-behind but im trying to avoid that as much as possible.

Update: The solutions so far are all well and good if you have the blend sdk or your not having problems with the interaction dll which is what i'm having. Is there any other more generic solutions than having to use the blend sdk?

A: 

The best option would probably be to use an Attached Property to do this. If you have the Blend SDK, the Behavior<T> class makes this much simpler.

For example, it would be very easy to modify this TextBox Behavior to fire an ICommand on every key press instead of clicking a button on Enter.

Reed Copsey
+1  A: 

Perhaps the easiest transition from code-behind event handling to MVVM commands would be Triggers and Actions from Expression Blend Samples.

Here's a snippet of code that demonstrates how you can handle key down event inside of the text box with the command:

    <TextBox>
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="KeyDown">
                <si:InvokeDataCommand Command="{Binding MyCommand}"/>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>
PL
And what if I only want the command to fire on a specific key?
Lee Treveil
If your trigger becomes more specific, you will have to implement your own. E.g. KeyPressTrigger with a property Key that would fire (execute the specified action - in the snippet above it's InvokeDataCommand) only when a specified Key is pressed.
PL
+3  A: 

First of all, if you want to bind a RoutedUICommand it is easy - just add to the UIElement.InputBindings collection:

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

Your trouble starts when you try to set Command="{Binding AddGlueCommand}" to get the ICommand from the ViewModel. Since Command is not a DependencyProperty you can't set a Binding on it.

Your next attempt would probably be to create an attached property BindableCommand that has a PropertyChangedCallback that updates Command. This does allow you to access the binding but there is no way to use FindAncestor to find your ViewModel since the InputBindings collection doesn't set an InheritanceContext.

Obviously you could create an attached property that you could apply to the TextBox that would run through all the InputBindings calling BindingOperations.GetBinding on each to find Command bindings and updating those Bindings with an explicit source, allowing you to do this:

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

This attached property would be easy to implement: On PropertyChangedCallback it would schedule a "refresh" at DispatcherPriority.Input and set up an event so the "refresh" is rescheduled on every DataContext change. Then in the "refresh" code just, just set DataContext on each 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);
}

An alternative to the two attached properties would be to create a CommandBinding subclass that receives a routed command and activates a bound command:

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

in this case, the InputBindings in each object would reference the routed command, not the binding. This command would then be routed up the the view and mapped.

The code for CommandMapper is relatively trivial:

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);
  }
}

For my taste, I would prefer to go with the attached properties solution, since it is not much code and keeps me from having to declare each command twice (as a RoutedCommand and as a property of my ViewModel). The supporting code only occurs once and can be used in all of your projects.

On the other hand if you're only doing a one-off project and don't expect to reuse anything, maybe even the CommandMapper is overkill. As you mentioned, it is possible to simply handle the events manually.

Ray Burns
+1  A: 

The excellent WPF framework Caliburn solves this problem beautifully.

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

The syntax [Action Search] binds to a method in the view model. No need for ICommands at all.

Lee Treveil