Posted in:

I recently attempted to upgrade a WPF application to compile for Silverlight as well. One of the many issues I ran into was that in the WPF version, pressing the Enter key while I was in a TextBox caused the OK button I had on the form to be clicked by virtue of the fact that I could set the IsDefault property on the button. However, after porting to Silverlight, that no longer worked, since the IsDefault property is missing..

A web-search revealed that someone had made a “behavior” that allows a specified button to be clicked when you press Enter within a TextBox (available for download here). However, it had one big problem: at the point that the command fired in my ViewModel, the value bound to the textbox contents had not been updated, since the textbox had not lost focus.

The original WPF binding I had an UpdateSourceTrigger ensuring that the ViewModel was always kept up to date with what was in the TextBox:

<TextBox Text="{Binding Answer, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

but in Silverlight, the PropertyChanged UpdateSourceTrigger is not available, so  I was left with the following:

<TextBox Text="{Binding Answer, Mode=TwoWay}" />

I decided to make my own EnterKeyCommand binding that would allow you to specify for a TextBox which command on the ViewModel should be run. Here’s the code:

public static class EnterKeyHelpers
{
    public static ICommand GetEnterKeyCommand(DependencyObject target)
    {
        return (ICommand)target.GetValue(EnterKeyCommandProperty);
    }

    public static void SetEnterKeyCommand(DependencyObject target, ICommand value)
    {
        target.SetValue(EnterKeyCommandProperty, value);
    }

    public static readonly DependencyProperty EnterKeyCommandProperty =
        DependencyProperty.RegisterAttached(
            "EnterKeyCommand",
            typeof(ICommand),
            typeof(EnterKeyHelpers),
            new PropertyMetadata(null, OnEnterKeyCommandChanged));

    static void OnEnterKeyCommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        ICommand command = (ICommand)e.NewValue;
        FrameworkElement fe = (FrameworkElement)target;
        Control control = (Control)target;
        control.KeyDown += (s, args) =>
        {
            if (args.Key == Key.Enter)
            {
                // make sure the textbox binding updates its source first
                BindingExpression b = control.GetBindingExpression(TextBox.TextProperty);
                if (b != null)
                {
                    b.UpdateSource();
                }
                command.Execute(null);
            }
        };
    }
}

Most of it is pretty simple, and it will allow an Enter key command to be specified for any control, not just textboxes. However, if you have bound to a textbox, it will call UpdateSource on any Text binding you have made, to ensure your ViewModel operates on the latest data.

Here’s how you use it in XAML:

<TextBox 
    Text="{Binding Answer, Mode=TwoWay}" 
    my:EnterKeyHelpers.EnterKeyCommand="{Binding SubmitAnswerCommand}"/>

It also has the advantage of being considerably more succinct than the equivalent XAML for using the behavior I linked to earlier.

Comments

Comment by blorq

Be careful using that inside ItemsControls as that will be a memory leak. The += on the KeyDown event is never cleaned up.

Comment by Mark H

thanks blorq. Would be interested if you have an idea for what would be a safer way.

I think I need to update it with unsubscription because I got a strange case recently where OnEnterKeyCommandChanged seemed to get called twice on the same object, meaning I double-subscribed to the key-down event.

Comment by Anonymous

This shouldn't cause a memory leak of the textbox, the behavior/event handler is a static class. In the reverse,where a class instance subscribes to a static event or a event of a long-lived object, you may end up with 'leaks'

Anonymous
Comment by Drew Noakes

Here's another take on this that avoids the leak (you can set the attached property to null to clear it).


public static class KeyboardExtensions
{
public static readonly DependencyProperty EnterKeyCommandProperty = DependencyProperty.RegisterAttached(
"EnterKeyCommand",
typeof(ICommand),
typeof(KeyboardExtensions),
new PropertyMetadata(
(d, e) =>
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (e.NewValue != null)
((UIElement)d).KeyDown += OnKeyDown;
else
((UIElement)d).KeyDown -= OnKeyDown;
}));

private static void OnKeyDown(object sender, KeyEventArgs args)
{
if (args.Key == Key.Enter)
{
var command = GetEnterKeyCommand((UIElement)sender);

if (command?.CanExecute(null) == true)
command.Execute(null);
}
}

public static ICommand GetEnterKeyCommand(UIElement element) => (ICommand)element.GetValue(EnterKeyCommandProperty);
public static void SetEnterKeyCommand(UIElement element, ICommand value) => element.SetValue(EnterKeyCommandProperty, value);
}

Drew Noakes