20

I am attempting to bind an event with Caliburn Micro and I am having some issues getting the right messages to the method. I would like to add the ability to press the 'Enter' key after changing the value in a text box and it execute the same method that the button next to is bound to. However, regardless of which key is pressed, I get the following exceptions:

A first chance exception of type 'System.InvalidCastException' occurred in MyApp.exe

A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll

A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in WindowsBase.dll

At the suggestion of another, similar question Binding KeyDown Event Silverlight, I've tried using ActionExecutionContext, but to no avail.

Here is the xaml:

<TextBox Name="Threshold"                     
              Margin="5"
              Grid.Column="1"
              >
     <i:Interaction.Triggers>
         <i:EventTrigger EventName="KeyDown">
             <cal:ActionMessage MethodName="ExecuteFilterView">
                 <cal:Parameter Value="$executionContext"/>
             </cal:ActionMessage>
         </i:EventTrigger>
     </i:Interaction.Triggers>
</TextBox>

And the method:

 public void ExecuteFilterView(ActionExecutionContext context)
    {
        //Do stuff...
    }

I understand that I could probably save myself some headaches and simply do a standard event handler in the code behind, but this app is an exercise in MVVM and learning to utilize Caliburn.Micro, so I would like to stick with making this particular approach work.

Am I just trying to send the wrong information from the event? Is my xaml not coded properly to get what I want? Or I have missed something else entirely?

Community
  • 1
  • 1
spugm1r3
  • 3,381
  • 3
  • 17
  • 18

3 Answers3

55

Just threw a test together, both of these work for me:

Using full syntax:

    <TextBox Name="Threshold"                     
          Margin="5"
          Grid.Column="1">
        <i:Interaction.Triggers>
            <i:EventTrigger EventName="KeyDown">
                <cal:ActionMessage MethodName="ExecuteFilterView">
                    <cal:Parameter Value="$executionContext"/>
                </cal:ActionMessage>
            </i:EventTrigger>
        </i:Interaction.Triggers>
    </TextBox>

Using CM syntax (prefer this as it's way more readable)

    <TextBox Name="Threshold"                     
          Margin="5"
          Grid.Column="1"
          cal:Message.Attach="[Event KeyDown] = [Action ExecuteFilterView($executionContext)]" />            

This was the test VM:

public class MainWindowViewModel
{
    public void ExecuteFilterView(ActionExecutionContext context)
    { 
        // This method is hit and the context is present and correct
    }
}

Can you post your full code - are you sure you have the framework setup correctly? (have you followed the getting started example?

http://caliburnmicro.codeplex.com/wikipage?title=Basic%20Configuration%2c%20Actions%20and%20Conventions&referringTitle=Documentation

Edit:

Ok after your clarification I can give you some examples of how to do this - I'll tell you my personal preference and why, but choose the one that fits you best

  1. Using ActionExecutionContext and casting the eventargs:
    cal:Message.Attach="[Event KeyDown] = [Action ExecuteFilterView($executionContext)]"
    public void ExecuteFilterView(ActionExecutionContext context)
    {
        var keyArgs = context.EventArgs as KeyEventArgs;

        if (keyArgs != null && keyArgs.Key == Key.Enter)
        {
            // Do Stuff
        }
    }
  1. Using EventArgs directly
    cal:Message.Attach="[Event KeyDown] = [Action ExecuteFilterView($eventArgs)]"
    public void ExecuteFilterView(KeyEventArgs keyArgs)
    {
        if (keyArgs.Key == Key.Enter)
        {
            // Do Stuff
        }
    }
  1. My personal fave, creating your own SpecialValues dictionary entry:

In your Bootstrapper.Configure method...

MessageBinder.SpecialValues.Add("$pressedkey", (context) =>
{
    // NOTE: IMPORTANT - you MUST add the dictionary key as lowercase as CM
    // does a ToLower on the param string you add in the action message, in fact ideally
    // all your param messages should be lowercase just in case. I don't really like this
    // behaviour but that's how it is!
    var keyArgs = context.EventArgs as KeyEventArgs;

    if (keyArgs != null)
        return keyArgs.Key;

    return null;
});

Your action:

cal:Message.Attach="[Event KeyDown] = [Action ExecuteFilterView($pressedKey)]"

And the code:

public void ExecuteFilterView(Key key)
{
    if (key == Key.Enter)
    {
        // Do Stuff
    }
}

The reason this is my favourite? It means that your VM just receives the value you want (most of the time you don't care about a lot of the other parameters) and you don't need to know how to or bother to cast the eventargs - you can just operate on the value. Obviously use whatever is best for you

It's also worth noting, that if you have other types of controls that subclass KeyEventArgs this will work for them. If they don't subclass KeyEventArgs but they still return a value of type Key this will still work too as you can just add another cast to the delegate if the first one fails:

e.g.

MessageBinder.SpecialValues.Add("$pressedkey", (context) =>
{
    var keyArgs = context.EventArgs as KeyEventArgs;

    if (keyArgs != null)
        return keyArgs.Key;

    // Ok so it wasn't KeyEventArgs... check for some other type - maybe a 3rd party implementation
    var thirdPartyKeyArgs = context.EventArgs as ThirdPartyKeyArgs;

    if (thirdPartyKeyArgs != null)
        return thirdPartyKeyArgs.KeyProperty;

    return null;
});
xmedeko
  • 7,336
  • 6
  • 55
  • 85
Charleh
  • 13,749
  • 3
  • 37
  • 57
  • 1
    So I guess I should clarify, I don't have any issues with the method being hit. The problem is, once the method fires, I can't do anything with the information without getting the exceptions. Basically, I'm trying to figure out how to use ActionExecutionContext to determine if the key pressed was the 'Enter" key and, if so, do something with it. – spugm1r3 May 24 '13 at 16:32
  • 1
    I'd probably use `MessageBinder.SpecialValues` for this as it abstracts away the actual event args cast and gives you a nice reusable way to do this. I'll update my post with all the options – Charleh May 24 '13 at 17:04
  • 1
    Beautiful! The fact that it is reuasable is even better, because I was going to be doing the same thing for 4 different views. In implementing your suggestion I discovered another error that I had missed. I was using System.Windows.Forms for KeyEventArgs, which did not allow for the cast I was trying. When I switched to System.Windows.Input, everything fell into place. Thanks for the well thought out answer. – spugm1r3 May 24 '13 at 18:05
  • the SpecialValues method worked for me, but the others passed in null instead of the key. – samuelesque Apr 02 '14 at 18:40
  • The other methods won't pass the key, rather they will pass certain types of event args (e.g. `KeyEventArgs`) which you need to either cast or inspect in the VM. This is the reason that special values is used, to abstract away that layer of indirection – Charleh Apr 03 '14 at 14:51
3

If using keyeventargs as parameter type , and it gives u back null ,then look at this: You propably using System.Windows.Forms.KeyEventArgs and KeyEventArgs, as parameter type is refering to this, try with System.Windows.Input.KeyEventArgs (it works for me).

public void ExecuteFilterView(System.Windows.Input.KeyEventArgs context)
{
     if(context.Key==System.Windows.Input.Key.Return)
     {
         //action here
     }      
}
Alex Filatov
  • 2,232
  • 3
  • 32
  • 39
0

Have at look at Caliburn.Micro samples, Scenario.Keybinding.

You can do this:

<TextBox Name="Threshold"                     
          Margin="5"
          Grid.Column="1"
          cal:Message.Attach="[Key Enter] = [ExecuteFilterView($this.Text)]"/>

Which will send the textbox' text to the ExecuteFilterView method.

To make it work, you need to add the code below (which I copied from the sample) to your bootstrapper.

protected override void Configure()
{
    var defaultCreateTrigger = Parser.CreateTrigger;

    Parser.CreateTrigger = (target, triggerText) => 
    {
        if (triggerText == null)
        {
            return defaultCreateTrigger(target, null);
        }

        var triggerDetail = triggerText
            .Replace("[", string.Empty)
            .Replace("]", string.Empty);

        var splits = triggerDetail.Split((char[])null, StringSplitOptions.RemoveEmptyEntries);

        switch (splits[0])
        {
            case "Key":
                var key = (Key)Enum.Parse(typeof(Key), splits[1], true);
                return new KeyTrigger { Key = key };

            case "Gesture":
                var mkg = (MultiKeyGesture)(new MultiKeyGestureConverter()).ConvertFrom(splits[1]);
                return new KeyTrigger { Modifiers = mkg.KeySequences[0].Modifiers, Key = mkg.KeySequences[0].Keys[0] };
        }

        return defaultCreateTrigger(target, triggerText);
    };
}
Magnus
  • 353
  • 3
  • 8