Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Key press inside of textbox MVVM

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?

like image 283
Lee Treveil Avatar asked Nov 10 '09 20:11

Lee Treveil


2 Answers

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.

like image 162
Ray Burns Avatar answered Nov 05 '22 21:11

Ray Burns


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.

like image 6
Lee Treveil Avatar answered Nov 05 '22 22:11

Lee Treveil