Question

How can I get Caliburn.Micro to map a key gesture to an action method on my ViewModel?

For example, I want to implement a tabbed interface, and I want my ShellViewModel to have a NewTab method, which the user should to be able to invoke by pressing Ctrl+T on the keyboard.

I know that the full Caliburn framework has support for gestures, but how can I do this using Caliburn.Micro? Is there perhaps some way to bind an action to a RoutedCommand (since RoutedCommands already support input gestures)? Or some other way to get gesture support?

Was it helpful?

Solution

You can do it by deriving from System.Windows.Interactivity.TriggerBase. Here is an example.

OTHER TIPS

I modified example to enable support for global key-bindings. You just need to add the folowing code to your view:

<i:Interaction.Triggers>
        <common:InputBindingTrigger>
            <common:InputBindingTrigger.InputBinding>
                <KeyBinding Modifiers="Control" Key="D"/>
            </common:InputBindingTrigger.InputBinding>
            <cl:ActionMessage MethodName="DoTheMagic"/>
        </common:InputBindingTrigger>
    </i:Interaction.Triggers>

And whenever Ctr+D is pressed the method DoTheMagic will be exexuted. Here is the modified InputBindingTrigger code:

public class InputBindingTrigger : TriggerBase<FrameworkElement>, ICommand
  {
    public static readonly DependencyProperty InputBindingProperty =
      DependencyProperty.Register("InputBinding", typeof (InputBinding)
        , typeof (InputBindingTrigger)
        , new UIPropertyMetadata(null));

    public InputBinding InputBinding
    {
      get { return (InputBinding) GetValue(InputBindingProperty); }
      set { SetValue(InputBindingProperty, value); }
    }

    public event EventHandler CanExecuteChanged = delegate { };

    public bool CanExecute(object parameter)
    {
      // action is anyway blocked by Caliburn at the invoke level
      return true;
    }

    public void Execute(object parameter)
    {
      InvokeActions(parameter);
    }

    protected override void OnAttached()
    {
      if (InputBinding != null)
      {
        InputBinding.Command = this;        
        AssociatedObject.Loaded += delegate {
          var window = GetWindow(AssociatedObject);
          window.InputBindings.Add(InputBinding);
        };
      }
      base.OnAttached();
    }

    private Window GetWindow(FrameworkElement frameworkElement)
    {
      if (frameworkElement is Window)
        return frameworkElement as Window;

      var parent = frameworkElement.Parent as FrameworkElement;      
      Debug.Assert(parent != null);

      return GetWindow(parent);
    }
  }

Caliburn.Micro's Actions mechanism is built on top of System.Windows.Interactivity. So, you can create a custom trigger based on TriggerBase to do whatever you want, including global keyboard gestures. Then, just plug the ActionMessage into your trigger and viola!

Inherit from Caliburn's ActionMessage (which is a TriggerAction) and attach the derived trigger to the KeyDown event in XAML and set the ActionMessage.MethodName property. Add a property to the derived trigger of what key combination you are looking for and override the Invoke method to filter by that key combination, calling base.Invoke(...) if the key matches.

If you marshal a command through the View to the View Model you can control the CanExecute from the View Model. I've been using this method in multiple Caliburn projects. Might not be as "slick" as using Interactivity, but CanExecute works.

<UserControl x:Class="MyView"
      ...
      Name="View"
>

  <UserControl.InputBindings>
    <KeyBinding Key="F5" 
                Command="{Binding RefreshCommand, ElementName=View, Mode=OneWay}" />
  </UserControl.InputBindings>

  <Button Command="{Binding Path=RefreshCommand, ElementName=View, Mode=OneWay}"/>

In your View class, you wire the command to the View Model which is referenced in the MyView.DataContext property.

Class MyView

    Public Property RefreshCommand As _
    New RelayCommand(AddressOf Refresh,
                     Function()
                         If ViewModel Is Nothing Then
                             Return False
                         Else
                             Return ViewModel.CanRefresh
                         End If
                     End Function)

    Private Sub Refresh()
        ViewModel.Refresh()
    End Sub

    Private ReadOnly Property ViewModel As MyViewModel
        Get
            Return DirectCast(DataContext, MyViewModel)
        End Get
    End Property

End Class
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top