Caliburn.Micro and KeyDown commands

Aug 2, 2010 at 12:22 PM

I'm just learning the ropes and wondering how would one go about implementing KeyDown bindings in MVVM?

For instance, in traditional event-driven apps, I'd write an event handler for the "KeyDown" event on a datagrid, and if Delete Key was pressed, delete the currently selected row.

If there is no clean way to do it, how would a view programmatically call methods on it's viewmodel? Is the currently bound ViewModel accessible from the view?

 

Chui

Aug 2, 2010 at 1:58 PM

Hi I'm a newbie on Caliburn.Micro as well, so not too sure if this is the best way but in my view model I have

    Public Sub KeyPress(ByVal args As KeyEventArgs)

        If args.Key = System.Windows.Input.Key.OemPeriod Then
            System.Windows.MessageBox.Show("Hi there")
        End If

    End Sub

and in my view I have

    <local:BindableRichTextBox
                    x:Name="mainRTB"
                    cal:Message.Attach="[Event KeyDown] = [Action KeyPress($eventArgs)]">
    </local:BindableRichTextBox>

But as you can see I have posted a question along these lines as well

S


Coordinator
Aug 2, 2010 at 2:49 PM
Edited Aug 2, 2010 at 2:50 PM

You have a couple of options at least. The one shown here works and I have used this myself in the past. You could also create a custom Trigger, perhaps one with built-in key filtering. You could inherit from System.Windows.Interaction.TriggerBase, wire up the events in the OnAttach method, do the proper key filtering when the event is fired, and if the correct key was pressed, invoke the action. After that, CM's ActionMessage will take over the rest. This is probably a good feature for our recipes section in the future. In the mean time, you can search around for custom Behaviors and Triggers. You may find that what you want has already been built or that you can easily adapt some else's code.

Aug 3, 2010 at 8:00 AM
Edited Aug 3, 2010 at 10:53 AM

Chui

Don't know if this is what Rob had in mind, but I've been playing around with his suggestion and ended up with this - so far. (Sorry if you don't like VB!)

 

        <local:BindableRichTextBox x:Name="Editor">
            <i:Interaction.Triggers>                
                <local:KeyPressTrigger KeyEvent="KeyDown" TriggerValue="OemPeriod">        
                    <cal:ActionMessage MethodName="Search"/>
                </local:KeyPressTrigger>
            </i:Interaction.Triggers>
        </local:BindableRichTextBox>

 

And a local class

 

Imports System.Windows
Imports System.Windows.Input
Imports System.Windows.Interactivity
Imports System.ComponentModel

Public Enum KeyEventAction
    KeyUp
    KeyDown
End Enum

Public Class KeyPressTrigger
    Inherits TriggerBase(Of FrameworkElement)

    Public Shared ReadOnly TriggerValueProperty As DependencyProperty =
        DependencyProperty.Register("TriggerValue", GetType(Key), GetType(KeyPressTrigger), New PropertyMetadata(Nothing))

    Public Shared ReadOnly KeyActionProperty As DependencyProperty =
        DependencyProperty.Register("KeyAction", GetType(KeyEventAction), GetType(KeyPressTrigger), New PropertyMetadata(Nothing))

    <Category("KeyPress Properties")>
    Public Property TriggerValue As Key
        Get
            Return CType(GetValue(TriggerValueProperty), Key)
        End Get
        Set(ByVal value As Key)
            SetValue(TriggerValueProperty, value)
        End Set
    End Property

    <Category("KeyPress Properties")>
    Public Property KeyEvent As KeyEventAction
        Get
            Return CType(GetValue(KeyActionProperty), KeyEventAction)
        End Get
        Set(ByVal value As KeyEventAction)
            SetValue(KeyActionProperty, value)
        End Set
    End Property

    Protected Overrides Sub OnAttached()

        MyBase.OnAttached()

        If KeyEvent = KeyEventAction.KeyUp Then
            AddHandler AssociatedObject.KeyUp, AddressOf OnKeyPress
        ElseIf KeyEvent = KeyEventAction.KeyDown Then
            AddHandler AssociatedObject.KeyDown, AddressOf OnKeyPress
        End If

    End Sub

    Protected Overrides Sub OnDetaching()

        MyBase.OnDetaching()

        If KeyEvent = KeyEventAction.KeyUp Then
            RemoveHandler AssociatedObject.KeyUp, AddressOf OnKeyPress
        ElseIf KeyEvent = KeyEventAction.KeyDown Then
            RemoveHandler AssociatedObject.KeyDown, AddressOf OnKeyPress
        End If

    End Sub

    Private Sub OnKeyPress(ByVal sender As Object, ByVal args As KeyEventArgs)

        If args.Key.Equals(TriggerValue) Then
            InvokeActions(Nothing)
        End If

    End Sub

 

(Obviously Rob - or anyone else - if you've any corrections/suggestions for improvements ... that'd be great. Thx)

Simon

PS. Don't know if anyone can shed any light on this but the above implementation - although it runs fine - generates a design time error

Error 1 System.ArgumentException was thrown on "C:\Users\Simon\Documents\Visual Studio 2010\Projects\MyControl\MyControl\Views\EditorView.xaml": GenericArguments[0], 'System.Object', on 'System.Windows.Interactivity.TriggerBase`1[T]' violates the constraint of type 'T'. C:\Users\Simon\Documents\Visual Studio 2010\Projects\MyControl\MyControl\Views\EditorView.xaml 1 1 MyControl

 

The framework element is a local class which inherits a RichTextBox.

Thx again

Coordinator
Aug 3, 2010 at 2:16 PM

That's exactly what I had in mind :)

Dec 1, 2010 at 11:11 PM

Thanks for posting this KeyPressTrigger trigger!

I'm curious if Caliburn.Micro will get a fancier parser to allow Message.Attach to handle triggers like this.

Just in case it's useful or saves someone a few minutes, here is a C# conversion of the VB KeyPressTrigger code above.

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.ComponentModel;

namespace Your.CaliburnExt
{

    public enum KeyEventAction
    {
        KeyUp,
        KeyDown
    }

    public class KeyPressTrigger : TriggerBase<FrameworkElement>
    {
        public static readonly DependencyProperty TriggerValueProperty = DependencyProperty.Register("TriggerValue", typeof(Key), typeof(KeyPressTrigger), new PropertyMetadata(null));
        public static readonly DependencyProperty KeyActionProperty = DependencyProperty.Register("KeyAction", typeof(KeyEventAction), typeof(KeyPressTrigger), new PropertyMetadata(null));

        [Category("KeyPress Properties")]
        public Key TriggerValue
        {
            get { return (Key)GetValue(TriggerValueProperty); }
            set { SetValue(TriggerValueProperty, value); }
        }

        [Category("KeyPress Properties")]
        public KeyEventAction KeyEvent
        {
            get { return (KeyEventAction)GetValue(KeyActionProperty); }
            set { SetValue(KeyActionProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();

            if (KeyEvent == KeyEventAction.KeyUp)
            {
                AssociatedObject.KeyUp += OnKeyPress;
            }
            else if (KeyEvent == KeyEventAction.KeyDown)
            {
                AssociatedObject.KeyDown += OnKeyPress;
            }
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            if (KeyEvent == KeyEventAction.KeyUp)
            {
                AssociatedObject.KeyUp -= OnKeyPress;
            }
            else if (KeyEvent == KeyEventAction.KeyDown)
            {
                AssociatedObject.KeyDown -= OnKeyPress;
            }
        }

        private void OnKeyPress(object sender, KeyEventArgs args)
        {
            if (args.Key.Equals(TriggerValue))
            {
                InvokeActions(null);
            }
        }
    }
}

 

Dec 30, 2011 at 7:30 PM

I'm not sure if I'm missing something but I've tried implementing the above solution (c#) in a SL5 application with a AutoCompleteBox and I'm getting the following design time error:

A value of type 'ActionMessage' cannot be added to a collection or dictionary of type 'TriggerActionCollection'. 

What have I done wrong?

Thanks

jack 

 

<sdk:AutoCompleteBox x:Name="MyEpmSearch" FilterMode="Contains" 
                                             IsTextCompletionEnabled="True"
                                             ItemsSource="{Binding MyEpmSearchTerms}"
                                             TextBoxStyle="{StaticResource SearchTextBoxStyle}"
                                             ValueMemberPath="SearchTerm"
                                             SelectedItem="{Binding SelectedSearchTerm, Mode=TwoWay}" MinimumPopulateDelay="250"
                            >
                            <i:Interaction.Triggers>
                                <Helpers:KeyPressTrigger KeyEvent="KeyDown" TriggerValue="Enter">
                                    <cal:ActionMessage MethodName="DoSearchOnEnterKey"/>
                                </Helpers:KeyPressTrigger>
                            </i:Interaction.Triggers>

                            <sdk:AutoCompleteBox.ItemTemplate>
                                <DataTemplate>
                                    <!--  <ContentControl cal:View.Model="{Binding}"/>  -->
                                    <StackPanel Orientation="Vertical">
                                        <StackPanel Orientation="Horizontal">
                                            <Image Source="{Binding ImageUri}" />
                                            <TextBlock Margin="10 0 0 0" VerticalAlignment="Center" Text="{Binding Label}" />
                                        </StackPanel>
                                        <TextBlock Margin="2" Text="{Binding Description}" />
                                    </StackPanel>
                                </DataTemplate>
                            </sdk:AutoCompleteBox.ItemTemplate>
                        </sdk:AutoCompleteBox>
Coordinator
Dec 31, 2011 at 2:56 AM

Make sure you are using the latest code from source, which has a build specific to SL5.

Jan 5, 2012 at 7:04 AM

That did the trick thanks.

Apr 21, 2012 at 3:42 AM

This works great, however if we use woopsie's example, if a "CanSearch" method on the viewmodel returns false, then the textbox will be disabled by caliburn micro.  That can't work, since CanSearch is dependant on text being entered :)

In this scenario, is there any way to get the textbox to be enabled, but not execute the "Search" action unless CanSearch is true?

Apr 21, 2012 at 4:30 AM

If anyone else is stuck on this, I didn't like the idea of creating an additional method in my viewmodel to support this behavior - so I looked elsewhere.

I ended up using an attached-property implementation of the 'default button' concept:

http://stackoverflow.com/questions/2683891/silverlight-4-default-button-service

Did the trick nicely.

Apr 23, 2012 at 5:13 PM

It'd also be nice if the implementation supported modifiers, ie. Shift, Ctrl, Alt, etc.

Aug 7, 2012 at 3:20 AM
Edited Aug 7, 2012 at 3:24 AM
<i:Interaction.Triggers>
    <ui:KeyPressTrigger KeyAction="KeyUp"    Gesture="ctrl+shift+alt+enter" >
        <cal:ActionMessage MethodName="MethodName" />
    </ui:KeyPressTrigger>
</i:Interaction.Triggers>
Local Class

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.ComponentModel;

public enum KeyEventAction
{
    KeyUp,
    KeyDown
}

public class KeyPressTrigger : TriggerBase<FrameworkElement>
{
    public static readonly DependencyProperty KeyActionProperty =
        DependencyProperty.Register("KeyAction", typeof(KeyEventAction), typeof(KeyPressTrigger),
        new PropertyMetadata(null));

    public static readonly DependencyProperty GestureProperty =
        DependencyProperty.Register("Gesture", typeof(InputGesture), typeof(KeyPressTrigger),
        new PropertyMetadata(null));

    [TypeConverterAttribute(typeof(KeyGestureConverter)), Category("KeyPress Properties")]
    public InputGesture Gesture
    {
        get { return (InputGesture)GetValue(GestureProperty); }
        set { SetValue(GestureProperty, value); }
    }

    [Category("KeyPress Properties")]
    public KeyEventAction KeyAction
    {
        get { return (KeyEventAction)GetValue(KeyActionProperty); }
        set { SetValue(KeyActionProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();

        if (KeyAction == KeyEventAction.KeyUp)
            AssociatedObject.KeyUp += OnKeyPress;
        else if (KeyAction == KeyEventAction.KeyDown)
            AssociatedObject.KeyDown += OnKeyPress;
        else
            throw new ArgumentOutOfRangeException("KeyAction", string.Format("{0} is not support.", KeyAction));
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (KeyAction == KeyEventAction.KeyUp)
            AssociatedObject.KeyUp -= OnKeyPress;
        else if (KeyAction == KeyEventAction.KeyDown)
            AssociatedObject.KeyDown -= OnKeyPress;
        else
            throw new ArgumentOutOfRangeException("KeyAction", string.Format("{0} is not support.", KeyAction));
    }

    private void OnKeyPress(object sender, KeyEventArgs args)
    {
        KeyGesture kGesture = Gesture as KeyGesture;
        if (kGesture == null)
            return;

        if (kGesture.Matches(null, args))
            this.InvokeActions(null);
    }
}
Mouse trigger will be similar. typeconverterattribute change to MouseGestureConverter and other part according to that. 
Feb 12, 2013 at 12:32 PM
Edited Feb 12, 2013 at 12:37 PM
I'm trying out your sample code in Silverlight 5 with CM 1.4, but the "InputGesture" type cannot be found.

Do I need to reference some assembly?