1

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;
}
Matt
  • 774
  • 1
  • 10
  • 28
  • Try to remove your ValidationRule or post the code for it...your issue is not reproducible based on the information you have provided. – mm8 Apr 18 '18 at 13:05
  • Even with ` ` removed this still occurs, so the rule can be easily ruled out as the cause. It only seems to not occur when `UpdateSourceTrigger="PropertyChanged"` is removed, with or without the `Binding.ValidationRules`. – Matt Apr 18 '18 at 13:49
  • What type is SomePath? You should read this: https://stackoverflow.com/help/mcve – mm8 Apr 18 '18 at 13:51
  • The type is `Decimal` – Matt Apr 18 '18 at 13:52
  • You can't set a decimal to something like "3.". – mm8 Apr 18 '18 at 13:55
  • It looks like you're right. But I would think you should be able to set a decimal to something like "3.". Although the compiler wont allow you to, `Convert.ToDecimal("1234.")` works just fine as does `Decimal.TryParse("1234.", out myDecimal)` – Matt Apr 18 '18 at 14:03

2 Answers2

1

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>
mm8
  • 163,881
  • 10
  • 57
  • 88
  • Nope, still wont allow a period. – Matt Apr 18 '18 at 14:18
  • It does if you remove the UpdateSourceTrigger attribute. "5." gets converted to 5M and that's the value that you see in the TextBox when using UpdateSourceTrigger=PropertyChanged. The "5." string is not stored somewhere... – mm8 Apr 18 '18 at 14:26
  • @Matt: You may be able to use a converter. See my edit. – mm8 Apr 18 '18 at 14:35
  • 1
    I'll give it a try later on, if not i'll just add a string property to `SomeObject`, bind to that, and in its setter, just do a Convert.ToDecimal and set the decimal property. But since you've spent so much effort on this I feel compelled to accept your answer anyways. Thanks! – Matt Apr 18 '18 at 14:50
0

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>
Andy
  • 11,864
  • 2
  • 17
  • 20