52 votes

Comment puis-je formater une décimale liée à une TextBox sans mettre en colère mes utilisateurs ?

J'essaie d'afficher une décimale formatée dans un TextBox en utilisant la liaison de données dans WPF.

Objectifs

Objectif 1 : Lors de la définition d'une propriété décimale dans le code, afficher 2 décimales dans la TextBox.

Objectif 2 : Lorsqu'un utilisateur interagit avec (tape dans) la TextBox, ne le mettez pas en colère.

Objectif 3 : Les liaisons doivent mettre à jour la source sur PropertyChanged.

Tentatives

Tentative 1 : Pas de formatage.

Ici, nous partons presque de zéro.

<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" />

Viole l'objectif 1. SomeDecimal = 4.5 affichera "4.50000" dans la boîte de texte.

Tentative 2 : Utilisez StringFormat dans le Binding.

<TextBox Text="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged, StringFormat=F2}" />

Viole l'objectif 2. Disons que SomeDecimal est 2.5, et que la TextBox affiche "2.50". Si nous sélectionnons tout et tapons "13.5", nous nous retrouvons avec "13.5.00" dans la zone de texte parce que le formateur insère "utilement" les décimales et les zéros.

Tentative 3 : utiliser la MaskedTextBox de l'Extended WPF Toolkit.

http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox

Si je lis correctement la documentation, le masque ##0.00 signifie "deux chiffres facultatifs, suivis d'un chiffre obligatoire, d'un point décimal et de deux autres chiffres obligatoires". Cela m'oblige à dire "le plus grand nombre possible qui peut entrer dans ce champ de texte est 999.99" mais disons que cela me convient.

<xctk:MaskedTextBox Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" Mask="##0.00" />

Viole l'objectif 2. La boîte de texte commence par ___.__ et en le sélectionnant et en tapant 5,75, on obtient 575.__ . La seule façon d'obtenir 5.75 est de sélectionner le TextBox et de taper <space><space>5.75 .

Tentative 4 : utiliser le spinner DecimalUpDown de l'Extended WPF Toolkit.

http://wpftoolkit.codeplex.com/wikipage?title=DecimalUpDown

<xctk:DecimalUpDown Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" FormatString="F2" />

Viole l'objectif 3. DecimalUpDown ignore heureusement UpdateSourceTrigger=PropertyChanged. L'un des coordinateurs de la page Codeplex Extended WPF Toolkit suggère un ControlTemplate modifié à l'adresse suivante http://wpftoolkit.codeplex.com/discussions/352551/ . Cela satisfait l'objectif 3, mais viole l'objectif 2, en présentant le même comportement que dans la tentative 2.

Tentative 5 : Utiliser des matrices de style, n'utiliser le formatage que si l'utilisateur n'édite pas.

Liaison à un double avec StringFormat sur une TextBox

Même si celui-ci répondait à ces trois objectifs, je ne voudrais pas l'utiliser. (A) Les zones de texte deviennent 12 lignes chacune au lieu de 1, et mon application contient de très nombreuses zones de texte. (B) Toutes mes zones de texte ont déjà un attribut Style qui pointe vers une StaticResource globale qui définit la Marge, la Hauteur, et d'autres choses. (C) Vous avez peut-être remarqué que le code ci-dessous définit deux fois le binding Path, ce qui viole le principe DRY.

<TextBox>
    <TextBox.Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="Text" Value="{Binding Path=SomeDecimal, StringFormat=F2}" />
            <Style.Triggers>
                <Trigger Property="IsFocused" Value="True">
                    <Setter Property="Text" Value="{Binding Path=SomeDecimal, UpdateSourceTrigger=PropertyChanged}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </TextBox.Style>
</TextBox>

Toutes ces choses inconfortables mises à part...

Viole l'objectif 2. Tout d'abord, en cliquant sur une TextBox qui affiche "13.50", celle-ci affiche soudainement "13.5000", ce qui est inattendu. Deuxièmement, si je commence par une TextBox vide, et que je tape "13.50", la TextBox contiendra "1350". Je ne peux pas expliquer pourquoi, mais appuyer sur la touche de point n'insère pas les décimales si le curseur est à l'extrémité droite de la chaîne dans la TextBox. Si le TextBox contient une chaîne de longueur > 0, et que je repositionne le curseur n'importe où sauf à l'extrémité droite de la chaîne, je peux alors insérer les décimales.

Tentative 6 : le faire moi-même.

Je suis sur le point de m'embarquer dans un voyage de souffrance, soit en sous-classant TextBox, soit en créant une propriété attachée, et en écrivant le code de formatage moi-même. Ce sera plein de manipulations de chaînes de caractères, et provoquera une importante perte de cheveux.


Quelqu'un a-t-il des suggestions pour le formatage des décimales liées aux zones de texte qui satisfasse tous les objectifs ci-dessus ?

5voto

Vladimir Dorokhov Points 2581

Essayez de résoudre ce problème au niveau du ViewModel. C'est ça :

public class FormattedDecimalViewModel : INotifyPropertyChanged
    {
        private readonly string _format;

        public FormattedDecimalViewModel()
            : this("F2")
        {

        }

        public FormattedDecimalViewModel(string format)
        {
            _format = format;
        }

        private string _someDecimalAsString;
        // String value that will be displayed on the view.
        // Bind this property to your control
        public string SomeDecimalAsString
        {
            get
            {
                return _someDecimalAsString;
            }
            set
            {
                _someDecimalAsString = value;
                RaisePropertyChanged("SomeDecimalAsString");
                RaisePropertyChanged("SomeDecimal");
            }
        }

        // Converts user input to decimal or initializes view model
        public decimal SomeDecimal
        {
            get
            {
                return decimal.Parse(_someDecimalAsString);
            }
            set
            {
                SomeDecimalAsString = value.ToString(_format);
            }
        }

        // Applies format forcibly
        public void ApplyFormat()
        {
            SomeDecimalAsString = SomeDecimal.ToString(_format);
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

ÉCHANTILLON

Xaml :

<TextBox x:Name="tb" Text="{Binding Path=SomeDecimalAsString, UpdateSourceTrigger=PropertyChanged}" />

Code derrière :

public MainWindow()
{
    InitializeComponent();
    FormattedDecimalViewModel formattedDecimalViewModel = new FormattedDecimalViewModel { SomeDecimal = (decimal)2.50 };
    tb.LostFocus += (s, e) => formattedDecimalViewModel.ApplyFormat(); // when user finishes to type, will apply formatting
    DataContext = formattedDecimalViewModel;
}

2voto

Alex Hope O'Connor Points 2175

J'ai créé le comportement personnalisé suivant pour déplacer le curseur de l'utilisateur après la virgule lors de l'utilisation de la fonction StringFormat={}{0:0.00} qui force la présence d'une décimale, mais cela peut causer le problème suivant :

Viole l'objectif 2. Disons que SomeDecimal est 2.5, et que la TextBox affiche "2.50". Si nous sélectionnons tout et tapons "13.5", nous nous retrouvons avec "13.5.00" dans la zone de texte parce que le formateur insère "utilement" les décimales et les zéros.

J'ai contourné ce problème en utilisant un comportement personnalisé qui déplace le curseur de l'utilisateur après la décimale lorsqu'il appuie sur la touche :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace GUI.Helpers.Behaviors
{
    public class DecimalPlaceHotkeyBehavior : Behavior<TextBox>
    {
        #region Methods
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.PreviewKeyDown += AssociatedObject_PreviewKeyDown;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.PreviewKeyDown -= AssociatedObject_PreviewKeyDown;
        }

        protected override Freezable CreateInstanceCore()
        {
            return new DecimalPlaceHotkeyBehavior();
        }
        #endregion

        #region Event Methods
        private void AssociatedObject_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            if (e.Key == System.Windows.Input.Key.OemPeriod || e.Key == System.Windows.Input.Key.Decimal)
            {
                var periodIndex = AssociatedObject.Text.IndexOf('.');
                if (periodIndex != -1)
                {
                    AssociatedObject.CaretIndex = (periodIndex + 1);
                    e.Handled = true;
                }
            }
        }
        #endregion

        #region Initialization
        public DecimalPlaceHotkeyBehavior()
            : base()
        {
        }
        #endregion
    }
}

Je l'utilise comme suit :

<TextBox xmlns:Behaviors="clr-namespace:GUI.Helpers.Behaviors" 
         xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
         Text="{Binding Value, UpdateSourceTrigger=PropertyChanged, StringFormat={}{0:0.00}}">
        <i:Interaction.Behaviors>
            <Behaviors:DecimalPlaceHotkeyBehavior></Behaviors:DecimalPlaceHotkeyBehavior>
        </i:Interaction.Behaviors>
</TextBox>

1voto

Xcalibur37 Points 1433

Essayez la boîte de texte masquée WPF Extended Tookit pour mettre en œuvre un masque d'entrée : http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox

Ejemplo:

<toolkit:MaskedTextBox Mask="(000) 000-0000" Value="(555) 123-4567" 
IncludeLiterals="True" />

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X