web-dev-qa-db-fra.com

Comment formater une décimale liée à TextBox sans irriter mes utilisateurs?

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

Buts

Objectif 1: lors de la définition d'une propriété décimale dans le code, affichez 2 décimales dans la zone de texte.

Objectif 2: Lorsqu'un utilisateur interagit avec (saisit) la TextBox, ne le fâchez pas.

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

Tentative 2: utilisez StringFormat dans la liaison.

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

Ne respecte pas l'objectif 2. Dites que SomeDecimal est égal à 2,5 et que la zone de texte affiche "2,50". Si nous sélectionnons tout et tapons "13,5", nous nous retrouvons avec "13,5,00" dans la TextBox car le formateur insère "utilement" des décimales et des zéros.

Tentative 3: utilisez MaskedTextBox de la boîte à outils WPF étendue.

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

En supposant que je lis correctement la documentation, le masque ## 0.00 signifie "deux chiffres facultatifs, suivis d'un chiffre requis, d'un séparateur décimal et de deux autres chiffres obligatoires. Cela m'oblige à dire" le plus grand nombre possible pouvant entrer dans ce TextBox est 999,99 "mais disons que je suis d'accord avec ça.

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

Ne respecte pas l'objectif 2. La TextBox commence par ___.__ et en le sélectionnant et en tapant 5,75 donne 575.__. La seule façon d'obtenir 5.75 est de sélectionner la TextBox et de taper <space><space>5.75.

Tentative 4: utilisez le spinner DecimalUpDown d'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 joyeusement UpdateSourceTrigger = PropertyChanged. L'un des coordinateurs de la page Codeplex de la boîte à outils WPF étendue suggère un ControlTemplate modifié sur http://wpftoolkit.codeplex.com/discussions/352551/ . Cela satisfait l'objectif 3, mais viole l'objectif 2, présentant le même comportement que dans la tentative 2.

Tentative 5: en utilisant des déclencheurs de style, n'utilisez la mise en forme que si l'utilisateur ne modifie pas.

Liaison à un double avec StringFormat sur un TextBox

Même si celui-ci répondait aux 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 chemin de liaison, ce qui viole le principal 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 à part ...

Viole l'objectif 2. Premièrement, cliquer sur une TextBox qui affiche "13.50" fait soudainement apparaître "13.5000", ce qui est inattendu. Deuxièmement, si je commence par une TextBox vierge et que je tape "13.50", la TextBox contiendra "1350". Je ne peux pas expliquer pourquoi, mais appuyer sur la touche point n'insère pas de décimales si le curseur se trouve à 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 des points décimaux.

Tentative 6: faites-le moi-même.

Je suis sur le point de me lancer dans un jeu de douleur et de souffrance, soit en sous-classant TextBox, soit en créant une propriété jointe et en écrivant moi-même le code de mise en forme. Il sera plein de manipulation de cordes et provoquera des pertes de cheveux substantielles.


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

52
epalm

Essayez de résoudre ce problème au niveau de ViewModel. Qu'il:

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;
}
5
Vladimir Dorokhov

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

Ne respecte pas l'objectif 2. Supposons que SomeDecimal vaut 2,5 et que TextBox affiche "2,50". Si nous sélectionnons tout et tapons "13,5", nous nous retrouvons avec "13,5,00" dans la TextBox car le formateur insère "utilement" des décimales et des zéros.

J'ai piraté cela en utilisant un comportement personnalisé qui déplacera le curseur des utilisateurs après la décimale lorsqu'ils appuieront sur. clé:

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>
2
Alex Hope O'Connor

Essayez le WPF Extended Tookit Masked TextBox pour implémenter un masque de saisie: http://wpftoolkit.codeplex.com/wikipage?title=MaskedTextBox

Exemple:

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