Matt
Matt

Reputation: 774

WPF TextBox input filtering doesn't work as expected with a decimal

I am trying to filter the input to a WPF TextBox to prevent the user from entering non-numerical strings. I have configured PreviewKeyDown and am checking the characters entered with the code from this question to convert the key codes to characters. Everything works as expected except when the user enters a period. The code does detect a period was entered, yet when I return from PreviewKeyDown with setting KeyEventArgs's Handled to false, it doesn't allow the period to be entered.

XAML

<TextBox PreviewKeyDown="TextBox_PreviewKeyDown">
    <TextBox.Text>
        <Binding Source="{StaticResource SomeObject}" Path="SomePath" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:MyValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

C#

private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
    char character = GetCharFromKey(e.Key);

    e.Handled = false;
    if (character >= '0' && character <= '9')
        return;
    if (character == '.')
        return;
    switch(e.Key)
    {
        case Key.Delete:
        case Key.Back:
            return;
    }
    e.Handled = true;
}

Upvotes: 1

Views: 2094

Answers (2)

mm8
mm8

Reputation: 169210

Can't you handle the PreviewTextInput event? Something like this:

private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
    string str = ((TextBox)sender).Text + e.Text;
    decimal i;
    e.Handled = !decimal.TryParse(str, System.Globalization.NumberStyles.AllowDecimalPoint, System.Globalization.CultureInfo.InvariantCulture, out i);
}

XAML:

<TextBox Text="{Binding SomePath}" PreviewTextInput="TextBox_PreviewTextInput" />

Edit: The problem with using a an UpdateSourceTrigger of PropertyChanged is that the string "5." gets converted to 5M and that's the value that you see in the TextBox. The "5." string is not stored somewhere.

You could possible overcome this by using a converter that keeps track of the latest known string:

public class DecimalToStringConverter : IValueConverter
{
    private string _lastConvertedValue;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return _lastConvertedValue ?? value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string str = value?.ToString();
        decimal d;
        if (decimal.TryParse(str, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out d))
        {
            _lastConvertedValue = str;
            return d;
        }

        _lastConvertedValue = null;
        return Binding.DoNothing;
    }
}

XAML:

<TextBox PreviewTextInput="TextBox_PreviewTextInput">
    <TextBox.Text>
        <Binding Path="SomePath" UpdateSourceTrigger="PropertyChanged">
            <Binding.Converter>
                <local:DecimalToStringConverter />
            </Binding.Converter>
        </Binding>
    </TextBox.Text>
</TextBox>

Upvotes: 1

Andy
Andy

Reputation: 12276

I have a behaviour I use for this, I think it's based on something I got off the web. You could just use it as is, or work out why your version isn't working. Note the code handles pasting in though.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace UILib
{
    public class TextBoxDecimalRangeBehaviour : Behavior<TextBox>
    {
        public string EmptyValue { get; set; } = "0";

        public double Minimum
        {
            get { return (double)GetValue(MinimumProperty); }
            set { SetValue(MinimumProperty, value); }
        }
        public static readonly DependencyProperty MinimumProperty =
            DependencyProperty.Register("Minimum", typeof(double), typeof(TextBoxDecimalRangeBehaviour), new PropertyMetadata(0.0));


        public double Maximum
        {
            get { return (double)GetValue(MaximumProperty); }
            set { SetValue(MaximumProperty, value); }
        }
        public static readonly DependencyProperty MaximumProperty =
            DependencyProperty.Register("Maximum", typeof(double), typeof(TextBoxDecimalRangeBehaviour), new PropertyMetadata(10.0));



        public int MaxInteger
        {
            get { return (int)GetValue(MaxIntegerProperty); }
            set { SetValue(MaxIntegerProperty, value); }
        }
        public static readonly DependencyProperty MaxIntegerProperty =
            DependencyProperty.Register("MaxInteger", typeof(int), typeof(TextBoxDecimalRangeBehaviour), new PropertyMetadata(1));



        public int MaxDecimals
        {
            get { return (int)GetValue(MaxDecimalsProperty); }
            set { SetValue(MaxDecimalsProperty, value); }
        }

        public static readonly DependencyProperty MaxDecimalsProperty =
            DependencyProperty.Register("MaxDecimals", typeof(int), typeof(TextBoxDecimalRangeBehaviour), new PropertyMetadata(2));



        /// <summary>
        ///     Attach our behaviour. Add event handlers
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();

            AssociatedObject.PreviewTextInput += PreviewTextInputHandler;
            AssociatedObject.PreviewKeyDown += PreviewKeyDownHandler;
            DataObject.AddPastingHandler(AssociatedObject, PastingHandler);
        }

        /// <summary>
        ///     Deattach our behaviour. remove event handlers
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.PreviewTextInput -= PreviewTextInputHandler;
            AssociatedObject.PreviewKeyDown -= PreviewKeyDownHandler;
            DataObject.RemovePastingHandler(AssociatedObject, PastingHandler);
        }

        void PreviewTextInputHandler(object sender, TextCompositionEventArgs e)
        {
            string text;
            if (this.AssociatedObject.Text.Length < this.AssociatedObject.CaretIndex)
                text = this.AssociatedObject.Text;
            else
            {
                //  Remaining text after removing selected text.
                string remainingTextAfterRemoveSelection;

                text = TreatSelectedText(out remainingTextAfterRemoveSelection)
                    ? remainingTextAfterRemoveSelection.Insert(AssociatedObject.SelectionStart, e.Text)
                    : AssociatedObject.Text.Insert(this.AssociatedObject.CaretIndex, e.Text);
            }

            e.Handled = !ValidateText(text);
        }

        /// <summary>
        ///     PreviewKeyDown event handler
        /// </summary>
        void PreviewKeyDownHandler(object sender, KeyEventArgs e)
        {
            if (string.IsNullOrEmpty(this.EmptyValue))
            {
                return;
            }


            string text = null;

            // Handle the Backspace key
            if (e.Key == Key.Back)
            {
                if (!this.TreatSelectedText(out text))
                {
                    if (AssociatedObject.SelectionStart > 0)
                        text = this.AssociatedObject.Text.Remove(AssociatedObject.SelectionStart - 1, 1);
                }
            }
            // Handle the Delete key
            else if (e.Key == Key.Delete)
            {
                // If text was selected, delete it
                if (!this.TreatSelectedText(out text) && this.AssociatedObject.Text.Length > AssociatedObject.SelectionStart)
                {
                    // Otherwise delete next symbol
                    text = this.AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, 1);
                }
            }

            if (text == string.Empty)
            {
                this.AssociatedObject.Text = this.EmptyValue;
                if (e.Key == Key.Back)
                    AssociatedObject.SelectionStart++;
                e.Handled = true;
            }
        }

        private void PastingHandler(object sender, DataObjectPastingEventArgs e)
        {
            if (e.DataObject.GetDataPresent(DataFormats.Text))
            {
                string text = Convert.ToString(e.DataObject.GetData(DataFormats.Text));

                if (!ValidateText(text))
                    e.CancelCommand();
            }
            else
                e.CancelCommand();
        }

        /// <summary>
        ///     Validate certain text by our regular expression and text length conditions
        /// </summary>
        /// <param name="text"> Text for validation </param>
        /// <returns> True - valid, False - invalid </returns>
        private bool ValidateText(string text)
        {
            double number;
            if (!Double.TryParse(text, out number))
            {
                return false;
            }
            if(number < Minimum)
            {
                return false;
            }
            if (number > Maximum)
            {
                return false;
            }
            int dotPointer = text.IndexOf('.');
            // No point entered so the decimals must be ok
            if(dotPointer == -1)
            {
                return true;
            }
            if (dotPointer > MaxInteger)
            {
                return false;
            }
            if(text.Substring(dotPointer +1).Length > MaxDecimals)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        ///     Handle text selection
        /// </summary>
        /// <returns>true if the character was successfully removed; otherwise, false. </returns>
        private bool TreatSelectedText(out string text)
        {
            text = null;
            if (AssociatedObject.SelectionLength <= 0)
                return false;

            var length = this.AssociatedObject.Text.Length;
            if (AssociatedObject.SelectionStart >= length)
                return true;

            if (AssociatedObject.SelectionStart + AssociatedObject.SelectionLength >= length)
                AssociatedObject.SelectionLength = length - AssociatedObject.SelectionStart;

            text = this.AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, AssociatedObject.SelectionLength);
            return true;
        }
    }
}

Usage:

<TextBox Text="{Binding ......>
    <i:Interaction.Behaviors>
        <ui:TextBoxDecimalRangeBehaviour MaxDecimals="2" 
                                         MaxInteger="1" 
                                         Minimum="{StaticResource Zero}" 
                                         Maximum="{StaticResource Ten}" />
        <ui:SelectAllTextBoxBehavior/>
    </i:Interaction.Behaviors>
</TextBox>

Upvotes: 0

Related Questions