Search code examples
silverlightxamleventtriggerinvoke-command

Invoke Command When "ENTER" Key Is Pressed In XAML


I want to invoke a command when ENTER is pressed in a TextBox. Consider the following XAML:

<UserControl
     ...
     xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
     ...>    
     ...    
     <TextBox>
          <i:Interaction.Triggers>
               <i:EventTrigger EventName="KeyUp">
                    <i:InvokeCommandAction Command="{Binding MyCommand}"
                                           CommandParameter="{Binding Text}" />
               </i:EventTrigger>
          </i:Interaction.Triggers>
     </TextBox>    
     ...    
</UserControl>

and that MyCommand is as follows:

public ICommand MyCommand {
     get { return new DelegateCommand<string>(MyCommandExecute); }
}

private void MyCommandExecute(string s) { ... }

With the above, my command is invoked for every key press. How can I restrict the command to only invoke when the ENTER key is pressed?

I understand that with Expression Blend I can use Conditions but those seem to be restricted to elements and can't consider event arguments.

I have also come across SLEX which offers its own InvokeCommandAction implementation that is built on top of the Systems.Windows.Interactivity implementation and can do what I need. Another consideration is to write my own trigger, but I'm hoping there's a way to do it without using external toolkits.


Solution

  • I like scottrudy's approach (to which I've given a +1) with the custom triggers approach as it stays true to my initial approach. I'm including a modified version of it below to use dependency properties instead of reflection info so that it's possible to bind directly to the ICommand. I'm also including an approach using attached properties to avoid using System.Windows.Interactivity if desired. The caveat to the latter approach is that you lose the feature of multiple invokations from an event, but you can apply it more generally.


    Custom Triggers Approach

    ExecuteCommandAction.cs

    public class ExecuteCommandAction : TriggerAction<DependencyObject> {
        #region Properties
        public ICommand Command {
            get { return (ICommand)base.GetValue(CommandProperty); }
            set { base.SetValue(CommandProperty, value); }
        }
    
        public static ICommand GetCommand(DependencyObject obj) {
            return (ICommand)obj.GetValue(CommandProperty);
        }
    
        public static void SetCommand(DependencyObject obj, ICommand value) {
            obj.SetValue(CommandProperty, value);
        }
    
        // We use a DependencyProperty so we can bind commands directly rather
        // than have to use reflection info to find them
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.Register("Command", typeof(ICommand), typeof(ExecuteCommandAction), null);
        #endregion Properties
    
        protected override void Invoke(object parameter) {
            ICommand command = Command ?? GetCommand(AssociatedObject);
            if (command != null && command.CanExecute(parameter)) {
                command.Execute(parameter);
            }
        }
    }
    

    TextBoxEnterKeyTrigger.cs

    public class TextBoxEnterKeyTrigger : TriggerBase<UIElement> {
        protected override void OnAttached() {
            base.OnAttached();
            TextBox textBox = this.AssociatedObject as TextBox;
    
            if (textBox != null) {
                this.AssociatedObject.KeyUp += new System.Windows.Input.KeyEventHandler(AssociatedObject_KeyUp);
            }
            else {
                throw new InvalidOperationException("This behavior only works with TextBoxes");
            }
        }
    
        protected override void OnDetaching() {
            base.OnDetaching();
            AssociatedObject.KeyUp -= new KeyEventHandler(AssociatedObject_KeyUp);
        }
    
        private void AssociatedObject_KeyUp(object sender, KeyEventArgs e) {
            if (e.Key == Key.Enter) {
                TextBox textBox = AssociatedObject as TextBox;
    
                //This checks for an mvvm style binding and updates the source before invoking the actions.
                BindingExpression expression = textBox.GetBindingExpression(TextBox.TextProperty);
                if (expression != null)
                    expression.UpdateSource();
    
                InvokeActions(textBox.Text);
            }
        }
    }
    

    MyUserControl.xaml

    <UserControl
        ...
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:b="clr-namespace:MyNameSpace.Interactivity"
        ...
        <TextBox>
            <i:Interaction.Triggers>
                <b:TextBoxEnterKeyTrigger>
                    <b:ExecuteCommandAction Command="{Binding MyCommand}" />
                </b:TextBoxEnterKeyTrigger>
            </i:Interaction.Triggers>
        </TextBox>
        ...
    </UserControl>
    

    Attached Properties Approach

    EnterKeyDown.cs

    public sealed class EnterKeyDown {
    
        #region Properties
    
        #region Command
    
        public static ICommand GetCommand(DependencyObject obj) {
            return (ICommand)obj.GetValue(CommandProperty);
        }
    
        public static void SetCommand(DependencyObject obj, ICommand value) {
            obj.SetValue(CommandProperty, value);
        }
    
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(EnterKeyDown),
                new PropertyMetadata(null, OnCommandChanged));
    
        #endregion Command
    
        #region CommandArgument
    
        public static object GetCommandArgument(DependencyObject obj) {
            return (object)obj.GetValue(CommandArgumentProperty);
        }
    
        public static void SetCommandArgument(DependencyObject obj, object value) {
            obj.SetValue(CommandArgumentProperty, value);
        }
    
        public static readonly DependencyProperty CommandArgumentProperty =
            DependencyProperty.RegisterAttached("CommandArgument", typeof(object), typeof(EnterKeyDown),
                new PropertyMetadata(null, OnCommandArgumentChanged));
    
        #endregion CommandArgument
    
        #region HasCommandArgument
    
    
        private static bool GetHasCommandArgument(DependencyObject obj) {
            return (bool)obj.GetValue(HasCommandArgumentProperty);
        }
    
        private static void SetHasCommandArgument(DependencyObject obj, bool value) {
            obj.SetValue(HasCommandArgumentProperty, value);
        }
    
        private static readonly DependencyProperty HasCommandArgumentProperty =
            DependencyProperty.RegisterAttached("HasCommandArgument", typeof(bool), typeof(EnterKeyDown),
                new PropertyMetadata(false));
    
    
        #endregion HasCommandArgument
    
        #endregion Propreties
    
        #region Event Handling
    
        private static void OnCommandArgumentChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
            SetHasCommandArgument(o, true);
        }
    
        private static void OnCommandChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) {
            FrameworkElement element = o as FrameworkElement;
            if (element != null) {
                if (e.NewValue == null) {
                    element.KeyDown -= new KeyEventHandler(FrameworkElement_KeyDown);
                }
                else if (e.OldValue == null) {
                    element.KeyDown += new KeyEventHandler(FrameworkElement_KeyDown);
                }
            }
        }
    
        private static void FrameworkElement_KeyDown(object sender, KeyEventArgs e) {
            if (e.Key == Key.Enter) {
                DependencyObject o = sender as DependencyObject;
                ICommand command = GetCommand(sender as DependencyObject);
    
                FrameworkElement element = e.OriginalSource as FrameworkElement;
                if (element != null) {
                    // If the command argument has been explicitly set (even to NULL)
                    if (GetHasCommandArgument(o)) {
                        object commandArgument = GetCommandArgument(o);
    
                        // Execute the command
                        if (command.CanExecute(commandArgument)) {
                            command.Execute(commandArgument);
                        }
                    }
                    else if (command.CanExecute(element.DataContext)) {
                        command.Execute(element.DataContext);
                    }
                }
            }
        }
    
        #endregion
    }
    

    MyUserControl.xaml

    <UserControl
        ...
        xmlns:b="clr-namespace:MyNameSpace.Interactivity"
        ...
        <TextBox b:EnterKeyDown.Command="{Binding AddNewDetailCommand}"
                 b:EnterKeyDown.CommandArgument="{Binding Path=Text,RelativeSource={RelativeSource Self}}" />
        ...
    </UserControl>