Search code examples
c#wpfvalidationinputtextbox

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

Solution

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