Search code examples
wpfdecimaldata-entry

WPF TextBox to enter decimal values


Is there any decent way to get a WPF control which is bound to a decimal value?

When I just bind the TextBox or DataGridTextColumn to a decimal, data entry is a problem.

<TextBox Text="{Binding MyDecimal, UpdateSourceTrigger=PropertyChanged, 
    ValidatesOnDataErrors=True}"/>

When I try to enter "0,5" in this TextBox I'll get "5" as a result. It is nearly impossible to enter "0,5" at all (apart from entering 1,5 and replacing the "1" with a "0").

When I use StringFormat, data entry is only slightly improved:

<TextBox Text="{Binding MyDecimal, StringFormat=F1, UpdateSourceTrigger=PropertyChanged,
    ValidatesOnDataErrors=True}"/>

Now, when I try to enter "0,5" I'll end up with "0,5,0", which still is wrong but at least I can remove the trailing ",0" without much difficulty.

Still, entering decimal types using WPF is very awkward, because these TextBoxes are very prone to data entry errors, which is a real pain especially for values!

So what am I supposed to use for decimal data entry in WPF? Or does Microsoft not support decimal data??


Solution

  • I currently use this behavior for digital and decimal input:

    public class TextBoxInputBehavior : Behavior<TextBox>
    {
        const NumberStyles validNumberStyles = NumberStyles.AllowDecimalPoint |
                                                   NumberStyles.AllowThousands |
                                                   NumberStyles.AllowLeadingSign;
        public TextBoxInputBehavior()
        {
            this.InputMode = TextBoxInputMode.None;
            this.JustPositivDecimalInput = false;
        }
    
        public TextBoxInputMode InputMode { get; set; }
    
    
        public static readonly DependencyProperty JustPositivDecimalInputProperty =
         DependencyProperty.Register("JustPositivDecimalInput", typeof(bool),
         typeof(TextBoxInputBehavior), new FrameworkPropertyMetadata(false));
    
        public bool JustPositivDecimalInput
        {
            get { return (bool)GetValue(JustPositivDecimalInputProperty); }
            set { SetValue(JustPositivDecimalInputProperty, value); }
        }
    
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
            AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;
    
            DataObject.AddPastingHandler(AssociatedObject, Pasting);
    
        }
    
        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
            AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;
    
            DataObject.RemovePastingHandler(AssociatedObject, Pasting);
        }
    
        private void Pasting(object sender, DataObjectPastingEventArgs e)
        {
            if (e.DataObject.GetDataPresent(typeof(string)))
            {
                var pastedText = (string)e.DataObject.GetData(typeof(string));
    
                if (!this.IsValidInput(this.GetText(pastedText)))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.CancelCommand();
                }
            }
            else
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
         }
    
         private void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
         {
            if (e.Key == Key.Space)
            {
                if (!this.IsValidInput(this.GetText(" ")))
                {
                    System.Media.SystemSounds.Beep.Play();
                    e.Handled = true;
                }
            }
         }
    
         private void AssociatedObjectPreviewTextInput(object sender, TextCompositionEventArgs e)
         {
            if (!this.IsValidInput(this.GetText(e.Text)))
            {
                System.Media.SystemSounds.Beep.Play();
                e.Handled = true;
            }
         }
    
         private string GetText(string input)
         {
            var txt = this.AssociatedObject;
    
            int selectionStart = txt.SelectionStart;
            if (txt.Text.Length < selectionStart) 
                selectionStart = txt.Text.Length;
    
            int selectionLength = txt.SelectionLength;
            if (txt.Text.Length < selectionStart + selectionLength) 
                selectionLength = txt.Text.Length - selectionStart;
    
            var realtext = txt.Text.Remove(selectionStart, selectionLength);
    
            int caretIndex = txt.CaretIndex;
            if (realtext.Length < caretIndex) 
                caretIndex = realtext.Length;
    
            var newtext = realtext.Insert(caretIndex, input);
    
            return newtext;
         }
    
         private bool IsValidInput(string input)
         {
            switch (InputMode)
            {
                case TextBoxInputMode.None:
                    return true;
                case TextBoxInputMode.DigitInput:
                    return CheckIsDigit(input);
    
                case TextBoxInputMode.DecimalInput:
                    decimal d;
                    //wen mehr als ein Komma
                    if (input.ToCharArray().Where(x => x == ',').Count() > 1)
                        return false;
    
    
                    if (input.Contains("-"))
                    {
                         if (this.JustPositivDecimalInput) 
                            return false;
    
    
                         if (input.IndexOf("-",StringComparison.Ordinal) > 0) 
                              return false;
    
                          if(input.ToCharArray().Count(x=>x=='-') > 1)
                              return false;
    
                            //minus einmal am anfang zulässig
                           if (input.Length == 1) 
                               return true;
                        }
    
                        var result = decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                        return result;
    
    
    
                default: throw new ArgumentException("Unknown TextBoxInputMode");
    
            }
            return true;
         }
    
         private bool CheckIsDigit(string wert)
         {
            return wert.ToCharArray().All(Char.IsDigit);
         }
    }
    
     public enum TextBoxInputMode
     {
      None,
      DecimalInput,
      DigitInput
      }
    

    The XAML usage looks like this:

    <TextBox Text="{Binding Sum}">
        <i:Interaction.Behaviors>
            <Behaviors:TextBoxInputBehavior InputMode="DecimalInput"/>
        </i:Interaction.Behaviors>
    </TextBox>