在XAML中按下"ENTER"键时调用命令

15

我想在按下TextBox中的ENTER键时调用一个命令。考虑以下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>

而且我的命令如下:

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

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

在上述代码中,我的命令会在每次按键时被调用。我该如何限制命令只在按下“ENTER”键时被调用?

我知道可以使用 Expression Blend 中的条件,但这些似乎只能限制元素,无法考虑事件参数。

我还发现了 SLEX,它提供了自己的 InvokeCommandAction 实现,构建在 Systems.Windows.Interactivity 实现之上,可以实现我需要的功能。另一个考虑是编写自己的触发器,但我希望有一种方法可以不使用外部工具包来完成。

5个回答

24

在Expression Blend中有 KeyTrigger

<UserControl
xmlns:i="clr-namespace:System.Windows.Interactivity;
         assembly=System.Windows.Interactivity"
xmlns:iex="clr-namespace:Microsoft.Expression.Interactivity.Input;
           assembly=Microsoft.Expression.Interactions" ...>    
     <TextBox>
         <i:Interaction.Triggers>
            <iex:KeyTrigger Key="Enter">
               <i:InvokeCommandAction Command="{Binding PasswordLoginCommand}" />
            </iex:KeyTrigger>
         </i:Interaction.Triggers>
     </TextBox>    
</UserControl>

在官方的Nuget包中,System.Windows.InteractivityMicrosoft.Expression.Interactions程序集都可用于WPF。


我知道我有点晚了,但这应该是一个答案。 - Dennis
啊,我原本发表和回答这个问题时不认为这些是可用的。在SO社区,随着技术变化更新回答是否是惯例? - bitxwise
1
这可能行不通,因为 <i:Interaction.Triggers> 的父类是 System.Windows.TriggerBase,而 <iex:KeyTrigger> 的父类是 System.Windows.Interactivity.TriggerBase,所以 KeyTrigger 无法添加到 Triggers 中。我在 XAML 和 .cs 文件中都尝试过,但都失败了。 - user1108069
@user1108069,请指定.NET版本和环境(WP,Silverlight,WPF),答案是在2013年,代码可以更改。 - Artur A

17

我喜欢scottrudy的方法(我已经给了一个+1),他的自定义触发器方法符合我的初衷。我在下面包含了它的修改版本,使用依赖属性而不是反射信息,这样就可以直接绑定到ICommand。如果需要,我还包括了一种使用附加属性来避免使用System.Windows.Interactivity的方法。后一种方法的警告是你失去了从事件中多次调用的功能,但可以更普遍地应用。


自定义触发器方法

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);
        }
    }
}

我的用户控件.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>
附加属性方法

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
}

我的用户控件.xaml

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

只是想说,这是一个解决我的问题的好方法。非常感谢 :) - Rumplin
1
我现在有一个问题,当我按回车键时,值还没有设置,命令就被执行了。能帮忙吗? - Rumplin
@Rumplin - 你使用哪种方法(即自定义触发器或附加属性)?我认为为了组织起见,最好你发布一个新问题,但在你的帖子中引用这个问题。 - bitxwise
以上所有方法都不适用于使用数据绑定时,因为它们都在绑定之前触发。 <TextBox custom:EnterKeyDown.Command="{Binding SearchClickCommand}" Text="{Binding InputName, Mode=TwoWay}" /> - Rumplin
@Rumplin - 我上面的两个例子都使用了绑定...但是,我鼓励你发布一个新问题,并在帖子中引用这个问题,而不是有一长串与你的问题特定而不是一般问题相关的评论。 - bitxwise
我通过创建自己的文本框 'public class TextBox:System.Windows.Controls.TextBox' 并覆盖 'OnKeyUp' 方法并检查是否按下了 'ENTER' 键来解决了这个问题: BindingExpression binding = this.GetBindingExpression(TextBox.TextProperty); binding.UpdateSource(); base.OnKeyUp(e); - Rumplin

4

我昨天也遇到了同样的问题,并使用自定义触发器解决了它。一开始可能会有些困难,但我发现这个通用模式可用于执行许多我曾在视图中直接使用事件处理程序(例如双击事件)完成的任务。第一步是创建一个可以接受参数的触发器操作,因为我们稍后将需要它。

public class ExecuteCommandAction : TriggerAction<FrameworkElement>
{
    public string Command { get; set; }

    protected override void Invoke(object o)
    {
        if (Command != null)
        {
            object ctx = AssociatedObject.DataContext;
            if (ctx != null)
            {
                var cmd = ctx.GetType().GetProperty(Command)
                    .GetValue(ctx, null) as ICommand;
                if (cmd != null && cmd.CanExecute(o))
                {
                    cmd.Execute(o);
                }
            }
        }
    }
}

下一步是创建触发器。你可以使用基类来进行一些有趣的操作,使其更加通用,以捕获不同类型的按键操作,但我们将保持简单。
public class TextBoxEnterKeyTrigger: TriggerBase<UIElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.KeyUp += AssociatedObject_KeyUp;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.KeyUp -= AssociatedObject_KeyUp;
    }

    void AssociatedObject_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
        {
            TextBox textBox = AssociatedObject as TextBox;
            object o = textBox == null ? null : textBox.Text;
            if (o != null)
            {
                InvokeActions(o);
            }
        }
    }
}

请记住,即使您已经在TextBox值上设置了数据绑定,属性更改事件也不会触发,因为您的textbox尚未失去焦点。因此,我将TextBox.Text属性的值传递给命令。最后一步是在XAML中使用此功能。您需要确保包括Interactivity命名空间以及包含上述代码的命名空间。

<UserControl
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:common="clr-namespace:My.UI;assembly=My.UI">
    <TextBox Text="{Binding Path=MyText, Mode=TwoWay}" IsEnabled="{Binding CanMyCommand}">
        <i:Interaction.Triggers>
            <common:TextBoxEnterKeyTrigger>
                <common:ExecuteCommandAction Command=MyCommand" />
            </common:TextBoxEnterKeyTrigger>
        </i:Interaction.Triggers>
    </TextBox>
</UserControl>

+1 对于自定义触发器方法的支持 - 我在我的回答中进行了修改并提供了另一种方法。 - bitxwise
有趣的是,我在使用ExecuteCommandAction时遇到了麻烦,但由于我已经有了i:InvokeCommandAction的引用,所以我就把它加进去了,结果它像个冠军一样工作。 - itchi

2

我在我的应用程序中使用了scottrudy的代码,然而我的文本框文本绑定在viewmodel类中的某个属性上,在按下ENTER键后,由于我的文本框尚未失去焦点,因此该属性尚未更新。因此,为了解决这个问题,我在AssociatedObject_KeyUp方法中的InvokeActions(o)之前添加了以下代码片段,更新后的text属性已经在viewmodel类中得到更新。

                    BindingExpression bindingExpression = (textBox).GetBindingExpression(TextBox.TextProperty);
                bindingExpression.UpdateSource();

-1

在我脑海中.. 你可以将事件参数传递给命令,然后在ViewModel中检查e.KeyPress = Keys.Enter.. 这不是真正的代码 :) 我没有在这台电脑上安装VS.. 这只是一个想法 :)


这样做并不比挂钩文本框的KeyUp或KeyPress事件,在事件处理程序中调用命令,如果e.Key == ENTER,那么这将需要紧密耦合,而命令旨在避免这种情况...我正在寻找XAML中的解决方案;在ViewModel中检查键只是增加了另一层处理带有代码后台的事件的负担,这可以很容易地在视图中完成(而不是在视图模型中)。此外,这样的事件处理不应该发生在视图模型中 =) - bitxwise

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接