Lier TextBox en appuyant sur la touche Entrée

108

La liaison de données par défaut sur TextBoxest TwoWayet elle valide le texte dans la propriété uniquement lorsqu'elle TextBoxperd son focus.

Existe-t-il un moyen XAML simple pour que la liaison de données se produise lorsque j'appuie sur la Entertouche du TextBox?. Je sais que c'est assez facile à faire dans le code derrière, mais imaginez si TextBoxc'est à l'intérieur d'un complexe DataTemplate.

Joie Jobi
la source

Réponses:

137

Vous pouvez créer une approche XAML pure en créant un comportement attaché .

Quelque chose comme ça:

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

Ensuite, dans votre XAML, vous définissez la InputBindingsManager.UpdatePropertySourceWhenEnterPressedPropertypropriété sur celle que vous souhaitez mettre à jour lorsque la Entertouche est enfoncée. Comme ça

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

(Vous devez simplement vous assurer d'inclure une référence xmlns clr-namespace pour "b" dans l'élément racine de votre fichier XAML pointant vers l'espace de noms dans lequel vous avez placé InputBindingsManager).

Samuel Jack
la source
11
Dans le but de sauver les futurs lecteurs de quelques minutes de bidouillage, je viens de l'utiliser dans mon projet et l'exemple de XAML fourni ci-dessus ne fonctionnait pas correctement. La source a été mise à jour à chaque changement de personnage. Le remplacement de «UpdateSourceTrigger = PropertyChanged» par «UpdateSourceTrigger = Explicit» a résolu le problème. Maintenant, tout fonctionne comme vous le souhaitez.
ihake
1
@ihake: Je pense que votre changement recommandé empêchera également la mise à jour sur la perte de concentration
VoteCoffee
4
UpdateSourceTrigger = PropertyChanged peut être gênant lors de la saisie d'un nombre réel. Par exemple "3." cause un problème lorsqu'il est lié à un flotteur. Je recommanderais de ne pas spécifier UpdateSourceTrigger (ou de définir LostFocus) en plus du comportement attaché. Cela donne le meilleur des deux mondes.
David Hollinshead
2
Excellent travail! Minuscule sélection: lorsque le UpdatePropertySourceWhenEnterPressedchangement d'une valeur valide à une valeur valide différente, vous vous désabonnez et vous réinscrivez à l' PreviewKeyDownévénement inutilement. Au lieu de cela, tout ce que vous avez besoin est de vérifier si oui ou non l' e.NewValueest nullou non. Sinon null, abonnez-vous; sinon, si null, alors désabonnez-vous.
Nicholas Miller
1
J'adore cette solution, merci de l'avoir postée! Pour tous ceux qui ont besoin d'attacher ce comportement à de nombreuses TextBoxes dans votre application, j'ai publié une extension à cette réponse sur la façon dont vous pouvez y parvenir très facilement. Voir l'article ici
Nik
50

C'est ainsi que j'ai résolu ce problème. J'ai créé un gestionnaire d'événements spécial qui est entré dans le code derrière:

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

Ensuite, je viens d'ajouter ceci en tant que gestionnaire d'événements KeyUp dans le XAML:

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

Le gestionnaire d'événements utilise sa senderréférence pour provoquer la mise à jour de sa propre liaison. Étant donné que le gestionnaire d'événements est autonome, il doit fonctionner dans un DataTemplate complexe. Ce gestionnaire d'événement unique peut maintenant être ajouté à toutes les zones de texte qui ont besoin de cette fonctionnalité.

Ben
la source
8
Quelque chose me dit que c'est un article sous-estimé. Beaucoup de réponses sont populaires parce qu'elles sont "XAML-y". Mais celui-ci semble économiser de l'espace dans la plupart des cas.
j riv
3
+1 Cela vous permet également de laisser UpdateSourceTrigger seul au cas où vous auriez déjà minutieusement fait en sorte que vos liaisons TextBox se comportent déjà comme vous le souhaitez (avec styles, validation, bidirectionnel, etc.), mais actuellement, vous ne recevrez pas d'entrée après avoir appuyé sur Entrée .
Jamin
2
C'est l'approche la plus propre que j'ai trouvée et, à mon avis, elle devrait être marquée comme la réponse. Ajout d'un contrôle nul supplémentaire sur l'expéditeur, un contrôle sur Key.Return (la touche Entrée de mon clavier renvoie Key.Return) et Keyboard.ClearFocus () pour supprimer le focus de la zone de texte après la mise à jour de la propriété source. J'ai apporté des modifications à la réponse en attente d'examen par les pairs.
Oystein
2
D'accord avec les commentaires ci-dessus. Mais pour moi, l'événement KeyDown était plus approprié
Allender
1
J'ai utilisé KeyBindingdans le XAML pour déclencher cette méthode car mon interface utilisateur a un contrôle "Default" qui attrape la clé d'entrée. Vous devez l'attraper dans cette zone de texte pour l'empêcher de se propager dans l'arborescence de l'interface utilisateur vers le contrôle "Par défaut".
CAD bloke
46

Je ne crois pas qu'il existe une manière "XAML pur" de faire ce que vous décrivez. Vous pouvez configurer une liaison afin qu'elle se mette à jour chaque fois que le texte d'un TextBox change (plutôt que lorsque le TextBox perd le focus) en définissant la propriété UpdateSourceTrigger , comme ceci:

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

Si vous définissez UpdateSourceTrigger sur "Explicit" et que vous gérez ensuite l'événement PreviewKeyDown de la zone de texte (à la recherche de la touche Entrée), vous pouvez obtenir ce que vous voulez, mais cela nécessiterait un code-behind. Peut-être une sorte de propriété attaché (semblable à mon EnterKeyTraversal propriété) de travail woudld pour vous.

Matt Hamilton
la source
3
Cette réponse peut être plus simple que celle marquée comme réponse, mais elle présente certaines limites. Image que vous voulez faire une sorte de vérification dans la fonction set de la propriété liée (vérifier si l'entrée est valide). Vous ne voulez pas appeler cette fonction de vérification après chaque touche sur laquelle l'utilisateur a appuyé, n'est-ce pas (surtout pas lorsque la fonction prend un certain temps).?
sth_Weird
25

Vous pouvez facilement créer votre propre contrôle héritant de TextBox et le réutiliser tout au long de votre projet.

Quelque chose de similaire devrait fonctionner:

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

Il peut y avoir un moyen de contourner cette étape, mais sinon, vous devriez lier comme ceci (en utilisant Explicit):

<custom:SubmitTextBox
    Text="{Binding Path=BoundProperty, UpdateSourceTrigger=Explicit}" />
Ryan Versaw
la source
9
Dans WPF / Silverlight, vous ne devez jamais utiliser l'héritage - cela perturbe les styles et n'est pas aussi flexible que les comportements attachés. Par exemple, avec les comportements attachés, vous pouvez avoir à la fois Watermark et UpdateOnEnter dans la même zone de texte.
Mikhail
14

Si vous combinez à la fois les solutions de Ben et d'ausadmin, vous vous retrouvez avec une solution très conviviale pour 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>

... ce qui signifie que vous passez le TextBoxlui - même comme paramètre au Command.

Cela vous conduit à Commandressembler à ceci (si vous utilisez une DelegateCommandimplémentation -style dans votre 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();
        }
    }

Cette Commandimplémentation peut être utilisée pour n'importe quel TextBoxet le meilleur de tous aucun code dans le code-behind bien que vous souhaitiez peut-être le mettre dans sa propre classe afin qu'il n'y ait pas de dépendances System.Windows.Controlsdans votre VM. Cela dépend de la rigueur de votre code.

toadflakz
la source
Agréable. Très propre. Si une multibinding est impliquée, vous devrez peut-être utiliser BindingOperations.GetBindingExpressionBase (tBox, prop); puis binding.UpdateTarget ();
VoteCoffee
4

Voici une approche qui me semble assez simple, et plus simple que l'ajout d'un AttachedBehaviour (qui est également une solution valable). Nous utilisons le UpdateSourceTrigger par défaut (LostFocus pour TextBox), puis ajoutons un InputBinding à la clé Entrée, lié à une commande.

Le xaml est le suivant

       <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>

Ensuite, les méthodes de commande sont

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

Et le TextBox est lié à la propriété

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

Jusqu'à présent, cela semble bien fonctionner et capture l'événement Enter Key dans la zone de texte.

ausadmin
la source
4

Ce n'est pas une réponse à la question initiale, mais plutôt une extension de la réponse acceptée par @Samuel Jack. J'ai fait ce qui suit dans ma propre application et j'étais impressionné par l'élégance de la solution de Samuel. Il est très propre et très réutilisable, car il peut être utilisé sur n'importe quel contrôle, pas seulement le TextBox. J'ai pensé que cela devrait être partagé avec la communauté.

Si vous avez une fenêtre avec un millier TextBoxesqui nécessitent tous de mettre à jour la source de liaison sur Entrée, vous pouvez attacher ce comportement à tous en incluant le XAML ci-dessous dans votre Window Resourcesplutôt que de l'attacher à chaque zone de texte. Vous devez d'abord implémenter le comportement ci-joint conformément au message de Samuel , bien sûr.

<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>

Vous pouvez toujours limiter la portée, si nécessaire, en mettant le style dans les ressources de l'un des éléments enfants de la fenêtre (c'est-à-dire a Grid) qui contient les TextBoxes cibles.

Nik
la source
2

Si vous utilisez MultiBinding avec votre TextBox, vous devez utiliser BindingOperations.GetMultiBindingExpressionmethod au lieu 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
la source
1

Cela fonctionne pour moi:

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

Personnellement, je pense qu'avoir une extension de balisage est une approche plus propre.

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>
moi a toi
la source
0

Une solution différente (n'utilisant pas xaml mais toujours assez propre je pense).

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