Vincular TextBox ao pressionar a tecla Enter

108

A ligação de dados padrão TextBoxé TwoWaye confirma o texto para a propriedade apenas quando TextBoxperde o foco.

Existe alguma maneira XAML fácil de fazer a ligação de dados acontecer quando pressiono a Entertecla TextBox?. Eu sei que é muito fácil fazer no código por trás, mas imagine se isso TextBoxestá dentro de algum complexo DataTemplate.

Jobi Joy
fonte

Respostas:

137

Você pode se tornar uma abordagem XAML pura criando um comportamento anexado .

Algo assim:

public static class InputBindingsManager
{

    public static readonly DependencyProperty UpdatePropertySourceWhenEnterPressedProperty = DependencyProperty.RegisterAttached(
            "UpdatePropertySourceWhenEnterPressed", typeof(DependencyProperty), typeof(InputBindingsManager), new PropertyMetadata(null, OnUpdatePropertySourceWhenEnterPressedPropertyChanged));

    static InputBindingsManager()
    {

    }

    public static void SetUpdatePropertySourceWhenEnterPressed(DependencyObject dp, DependencyProperty value)
    {
        dp.SetValue(UpdatePropertySourceWhenEnterPressedProperty, value);
    }

    public static DependencyProperty GetUpdatePropertySourceWhenEnterPressed(DependencyObject dp)
    {
        return (DependencyProperty)dp.GetValue(UpdatePropertySourceWhenEnterPressedProperty);
    }

    private static void OnUpdatePropertySourceWhenEnterPressedPropertyChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
    {
        UIElement element = dp as UIElement;

        if (element == null)
        {
            return;
        }

        if (e.OldValue != null)
        {
            element.PreviewKeyDown -= HandlePreviewKeyDown;
        }

        if (e.NewValue != null)
        {
            element.PreviewKeyDown += new KeyEventHandler(HandlePreviewKeyDown);
        }
    }

    static void HandlePreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
        {
            DoUpdateSource(e.Source);
        }
    }

    static void DoUpdateSource(object source)
    {
        DependencyProperty property =
            GetUpdatePropertySourceWhenEnterPressed(source as DependencyObject);

        if (property == null)
        {
            return;
        }

        UIElement elt = source as UIElement;

        if (elt == null)
        {
            return;
        }

        BindingExpression binding = BindingOperations.GetBindingExpression(elt, property);

        if (binding != null)
        {
            binding.UpdateSource();
        }
    }
}

Em seguida, em seu XAML, você define a InputBindingsManager.UpdatePropertySourceWhenEnterPressedPropertypropriedade para aquela que deseja atualizar quando a Entertecla é pressionada. Como isso

<TextBox Name="itemNameTextBox"
         Text="{Binding Path=ItemName, UpdateSourceTrigger=PropertyChanged}"
         b:InputBindingsManager.UpdatePropertySourceWhenEnterPressed="TextBox.Text"/>

(Você só precisa ter certeza de incluir uma referência xmlns clr-namespace para "b" no elemento raiz de seu arquivo XAML apontando para qualquer namespace em que você colocou o InputBindingsManager).

Samuel Jack
fonte
11
Em um esforço para poupar aos leitores futuros alguns minutos de mexer, acabei de usar isso em meu projeto, e o XAML de amostra fornecido acima não funcionou direito. A fonte era atualizada a cada mudança de personagem. Alterar "UpdateSourceTrigger = PropertyChanged" para "UpdateSourceTrigger = Explicit" corrigiu o problema. Agora tudo funciona como desejado.
ihake de
1
@ihake: Acho que sua mudança recomendada também evitará a atualização do foco perdido
VoteCoffee
4
UpdateSourceTrigger = PropertyChanged pode ser inconveniente ao digitar um número real. Por exemplo "3". causa um problema quando ligado a um flutuador. Eu recomendo não especificar UpdateSourceTrigger (ou definir como LostFocus) além do comportamento anexado. Isso dá o melhor dos dois mundos.
David Hollinshead
2
Excelente trabalho! Miniatura minúscula: Quando as UpdatePropertySourceWhenEnterPressedmudanças de um valor válido para um valor válido diferente, você está cancelando e re-assinando o PreviewKeyDownevento desnecessariamente. Em vez disso, tudo que você precisa é verificar se o e.NewValueé nullou não. Se não null, então se inscreva; caso contrário, se null, cancele a inscrição.
Nicholas Miller
1
Eu amo essa solução, obrigado por postá-la! Para quem precisa anexar esse comportamento a vários TextBoxes em seu aplicativo, postei uma extensão para esta resposta sobre como você pode fazer isso facilmente. Veja a postagem aqui
Nik
50

Foi assim que resolvi esse problema. Criei um manipulador de eventos especial que foi inserido no código por trás de:

private void TextBox_KeyEnterUpdate(object sender, KeyEventArgs e)
{
    if (e.Key == Key.Enter)
    {
        TextBox tBox = (TextBox)sender;
        DependencyProperty prop = TextBox.TextProperty;

        BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop);
        if (binding != null) { binding.UpdateSource(); }
    }
}

Então, acabei de adicionar isso como um manipulador de eventos KeyUp no XAML:

<TextBox Text="{Binding TextValue1}" KeyUp="TextBox_KeyEnterUpdate" />
<TextBox Text="{Binding TextValue2}" KeyUp="TextBox_KeyEnterUpdate" />

O manipulador de eventos usa sua senderreferência para fazer com que sua própria vinculação seja atualizada. Como o manipulador de eventos é independente, ele deve funcionar em um DataTemplate complexo. Esse manipulador de eventos agora pode ser adicionado a todas as caixas de texto que precisam desse recurso.

Ben
fonte
8
Algo me diz que esta é uma postagem subestimada. Muitas respostas são populares porque são "XAML-y". Mas este parece economizar espaço na maioria das ocasiões.
j riv
3
+1 Isso também permite que você deixe o UpdateSourceTrigger sozinho no caso de você já ter meticulosamente feito seus vínculos TextBox se comportarem da maneira que você deseja (com estilos, validação, twoway, etc.), mas atualmente não receberá entrada após pressionar Enter .
Jamin
2
Esta é a abordagem mais limpa que encontrei e, em minha opinião, deve ser marcada como a resposta. Adicionada uma verificação de nulo adicional no remetente, uma verificação em Key.Return (a tecla Enter no meu teclado retorna Key.Return) e Keyboard.ClearFocus () para remover o foco do TextBox após atualizar a propriedade de origem. Fiz edições na resposta que está aguardando revisão por pares.
Oystein
2
Concorde com os comentários acima. Mas, para mim, o evento KeyDown foi mais apropriado
Allender
1
Eu usei KeyBindingno XAML para disparar esse método porque minha IU tem um controle "Padrão" que captura a tecla enter. Você precisa capturá-lo dentro deste TextBox para impedir que ele se propague na árvore da interface do usuário para o controle "Padrão".
CAD cara de
46

Não acredito que haja uma maneira "puro XAML" de fazer o que você está descrevendo. Você pode configurar uma vinculação para que seja atualizada sempre que o texto em um TextBox mudar (em vez de quando o TextBox perder o foco) definindo a propriedade UpdateSourceTrigger , como este:

<TextBox Name="itemNameTextBox"
    Text="{Binding Path=ItemName, UpdateSourceTrigger=PropertyChanged}" />

Se você definir UpdateSourceTrigger como "Explicit" e, em seguida, manipular o evento PreviewKeyDown do TextBox (procurando a tecla Enter), poderá obter o que deseja, mas exigiria code-behind. Talvez algum tipo de propriedade anexada (semelhante à minha propriedade EnterKeyTraversal ) funcione para você.

Matt Hamilton
fonte
3
Essa resposta pode ser mais simples do que aquela marcada como resposta, mas tem algumas limitações. Imagem que você deseja fazer algum tipo de verificação na função de definição da propriedade vinculada (verificando se a entrada é válida). Você não quer chamar essa função de verificação após cada tecla pressionada pelo usuário, não é (especialmente quando a função leva algum tempo).
sth_Weird
25

Você pode criar facilmente seu próprio controle herdado de TextBox e reutilizá-lo em todo o projeto.

Algo semelhante a isso deve funcionar:

public class SubmitTextBox : TextBox
{
    public SubmitTextBox()
        : base()
    {
        PreviewKeyDown += new KeyEventHandler(SubmitTextBox_PreviewKeyDown);
    }

    void SubmitTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        if (e.Key == Key.Enter)
        {
            BindingExpression be = GetBindingExpression(TextBox.TextProperty);
            if (be != null)
            {
                be.UpdateSource();
            }
        }
    }
}

Pode haver uma maneira de contornar esta etapa, mas caso contrário, você deve vincular assim (usando Explícito):

<custom:SubmitTextBox
    Text="{Binding Path=BoundProperty, UpdateSourceTrigger=Explicit}" />
Ryan Versaw
fonte
9
No WPF / Silverlight, você nunca deve usar herança - ela bagunça estilos e não é tão flexível quanto os comportamentos anexados. Por exemplo, com Attached Behaviors, você pode ter Watermark e UpdateOnEnter na mesma caixa de texto.
Mikhail
14

Se você combinar as soluções Ben e ausadmin, terminará com uma solução muito amigável para MVVM:

<TextBox Text="{Binding Txt1, Mode=TwoWay, UpdateSourceTrigger=Explicit}">
    <TextBox.InputBindings>
        <KeyBinding Gesture="Enter" 
                    Command="{Binding UpdateTextBoxBindingOnEnterCommand}"
                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type TextBox}}}" />
    </TextBox.InputBindings>
</TextBox>

... o que significa que você está passando o TextBoxpróprio como parâmetro para o Command.

Isso faz com que você Commandfique assim (se estiver usando uma DelegateCommandimplementação de estilo em sua VM):

    public bool CanExecuteUpdateTextBoxBindingOnEnterCommand(object parameter)
    {
        return true;
    }

    public void ExecuteUpdateTextBoxBindingOnEnterCommand(object parameter)
    {
        TextBox tBox = parameter as TextBox;
        if (tBox != null)
        {
            DependencyProperty prop = TextBox.TextProperty;
            BindingExpression binding = BindingOperations.GetBindingExpression(tBox, prop);
            if (binding != null) 
                binding.UpdateSource();
        }
    }

Essa Commandimplementação pode ser usada para qualquer TextBoxe, melhor de tudo, nenhum código no code-behind, embora você possa querer colocá-lo em sua própria classe para que não haja dependências System.Windows.Controlsem sua VM. Depende de quão rígidas são as suas diretrizes de código.

Toadflakz
fonte
Agradável. Muito limpo. Se uma ligação múltipla estiver envolvida, pode ser necessário usar BindingOperations.GetBindingExpressionBase (tBox, prop); e então binding.UpdateTarget ();
VoteCoffee
4

Aqui está uma abordagem que para mim parece bastante direta e mais fácil do que adicionar um AttachedBehaviour (que também é uma solução válida). Usamos o UpdateSourceTrigger padrão (LostFocus para TextBox) e, em seguida, adicionamos um InputBinding à tecla Enter, vinculado a um comando.

O xaml é o seguinte

       <TextBox Grid.Row="0" Text="{Binding Txt1}" Height="30" Width="150">
        <TextBox.InputBindings>
            <KeyBinding Gesture="Enter" 
                        Command="{Binding UpdateText1Command}"
                        CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type TextBox}},Path=Text}" />
        </TextBox.InputBindings>
    </TextBox>

Então os métodos de comando são

Private Function CanExecuteUpdateText1(ByVal param As Object) As Boolean
    Return True
End Function
Private Sub ExecuteUpdateText1(ByVal param As Object)

    If TypeOf param Is String Then
        Txt1 = CType(param, String)
    End If
End Sub

E o TextBox está vinculado à propriedade

 Public Property Txt1 As String
    Get
        Return _txt1
    End Get
    Set(value As String)
        _txt1 = value
        OnPropertyChanged("Txt1")
    End Set
End Property

Até agora, isso parece funcionar bem e captura o evento Enter Key no TextBox.

ausadmin
fonte
4

Esta não é uma resposta à pergunta original, mas sim uma extensão da resposta aceita por @Samuel Jack. Fiz o seguinte em meu próprio requerimento e fiquei maravilhado com a elegância da solução de Samuel. É muito limpo e muito reutilizável, pois pode ser usado em qualquer controle, não apenas no TextBox. Achei que isso deveria ser compartilhado com a comunidade.

Se você tiver uma janela com mil TextBoxesque exigem a atualização da Binding Source no Enter, você pode anexar esse comportamento a todos eles incluindo o XAML abaixo em seu em Window Resourcesvez de anexá-lo a cada TextBox. Primeiro você deve implementar o comportamento anexado de acordo com a postagem de Samuel, é claro.

<Window.Resources>
    <Style TargetType="{x:Type TextBox}" BasedOn="{StaticResource {x:Type TextBox}}">
        <Style.Setters>
            <Setter Property="b:InputBindingsManager.UpdatePropertySourceWhenEnterPressed" Value="TextBox.Text"/>
        </Style.Setters>
    </Style>
</Window.Resources>

Você sempre pode limitar o escopo, se necessário, colocando o estilo nos recursos de um dos elementos filhos da janela (ou seja, a Grid) que contém as TextBoxes de destino.

Nik
fonte
2

Caso você esteja usando MultiBinding com sua TextBox, você precisa usar o BindingOperations.GetMultiBindingExpressionmétodo em vez de BindingOperations.GetBindingExpression.

// Get the correct binding expression based on type of binding
//(simple binding or multi binding.
BindingExpressionBase binding = 
  BindingOperations.GetBindingExpression(element, prop);
if (binding == null)
{
    binding = BindingOperations.GetMultiBindingExpression(element, prop);
}

if (binding != null)
{
     object value = element.GetValue(prop);
     if (string.IsNullOrEmpty(value.ToString()) == true)
     {
         binding.UpdateTarget();
     }
     else
     {
          binding.UpdateSource();
     }
}
Akjoshi
fonte
1

Isso funciona para mim:

        <TextBox                 
            Text="{Binding Path=UserInput, UpdateSourceTrigger=PropertyChanged}">
            <TextBox.InputBindings>
                <KeyBinding Key="Return" 
                            Command="{Binding Ok}"/>
            </TextBox.InputBindings>
        </TextBox>
Valery
fonte
0

Pessoalmente, acho que ter uma extensão de marcação é uma abordagem mais limpa.

public class UpdatePropertySourceWhenEnterPressedExtension : MarkupExtension
{
    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return new DelegateCommand<TextBox>(textbox => textbox.GetBindingExpression(TextBox.TextProperty).UpdateSource());
    }
}


<TextBox x:Name="TextBox"
             Text="{Binding Text}">
        <TextBox.InputBindings>
            <KeyBinding Key="Enter"
                        Command="{markupExtensions:UpdatePropertySourceWhenEnterPressed}" 
                        CommandParameter="{Binding ElementName=TextBox}"/>
        </TextBox.InputBindings>
</TextBox>
metoyou
fonte
0

Uma solução diferente (sem usar xaml, mas ainda bem limpa, eu acho).

class ReturnKeyTextBox : TextBox
{
    protected override void OnKeyUp(KeyEventArgs e)
    {
        base.OnKeyUp(e);
        if (e.Key == Key.Return)
            GetBindingExpression(TextProperty).UpdateSource();
    }
}
Hauk
fonte