Question

How are you handling the entry of numeric values in WPF applications?

Without a NumericUpDown control, I've been using a TextBox and handling its PreviewKeyDown event with the code below, but it's pretty ugly.

Has anyone found a more graceful way to get numeric data from the user without relying on a third-party control?

private void NumericEditPreviewKeyDown(object sender, KeyEventArgs e)
{
    bool isNumPadNumeric = (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9) || e.Key == Key.Decimal;
    bool isNumeric = (e.Key >= Key.D0 && e.Key <= Key.D9) || e.Key == Key.OemPeriod;

    if ((isNumeric || isNumPadNumeric) && Keyboard.Modifiers != ModifierKeys.None)
    {
        e.Handled = true;
        return;
    }

    bool isControl = ((Keyboard.Modifiers != ModifierKeys.None && Keyboard.Modifiers != ModifierKeys.Shift)
        || e.Key == Key.Back || e.Key == Key.Delete || e.Key == Key.Insert
        || e.Key == Key.Down || e.Key == Key.Left || e.Key == Key.Right || e.Key == Key.Up
        || e.Key == Key.Tab
        || e.Key == Key.PageDown || e.Key == Key.PageUp
        || e.Key == Key.Enter || e.Key == Key.Return || e.Key == Key.Escape
        || e.Key == Key.Home || e.Key == Key.End);

    e.Handled = !isControl && !isNumeric && !isNumPadNumeric;
}
Was it helpful?

Solution

How about:

protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
{
    e.Handled = !AreAllValidNumericChars(e.Text);
    base.OnPreviewTextInput(e);
}

private bool AreAllValidNumericChars(string str)
{
    foreach(char c in str)
    {
        if(!Char.IsNumber(c)) return false;
    }

    return true;
}

OTHER TIPS

This is how I do it. It uses a regular expression to check if the text that will be in the box is numeric or not.

Regex NumEx = new Regex(@"^-?\d*\.?\d*$");

private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
    if (sender is TextBox)
    {
        string text = (sender as TextBox).Text + e.Text;
        e.Handled = !NumEx.IsMatch(text);
    }
    else
        throw new NotImplementedException("TextBox_PreviewTextInput Can only Handle TextBoxes");
}

There is now a much better way to do this in WPF and Silverlight. If your control is bound to a property, all you have to do is change your binding statement a bit. Use the following for your binding:

<TextBox Text="{Binding Number, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnExceptions=True}"/>

Note that you can use this on custom properties too, all you have to do is throw an exception if the value in the box is invalid and the control will get highlighted with a red border. If you click on the upper right of the red border then the exception message will pop up.

I've been using an attached property to allow the user to use the up and down keys to change the values in the text box. To use it, you just use

<TextBox local:TextBoxNumbers.SingleDelta="1">100</TextBox>

This doesn't actually address the validation issues that are referred to in this question, but it addresses what I do about not having a numeric up/down control. Using it for a little bit, I think I might actually like it better than the old numeric up/down control.

The code isn't perfect, but it handles the cases I needed it to handle:

  • Up arrow, Down arrow
  • Shift + Up arrow, Shift + Down arrow
  • Page Up, Page Down
  • Binding Converter on the text property

Code behind

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

namespace Helpers
{
    public class TextBoxNumbers
    {    
        public static Decimal GetSingleDelta(DependencyObject obj)
        {
            return (Decimal)obj.GetValue(SingleDeltaProperty);
        }

        public static void SetSingleDelta(DependencyObject obj, Decimal value)
        {
            obj.SetValue(SingleDeltaProperty, value);
        }

        // Using a DependencyProperty as the backing store for SingleValue.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SingleDeltaProperty =
            DependencyProperty.RegisterAttached("SingleDelta", typeof(Decimal), typeof(TextBoxNumbers), new UIPropertyMetadata(0.0m, new PropertyChangedCallback(f)));

        public static void f(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            TextBox t = o as TextBox;

            if (t == null)
                return;

            t.PreviewKeyDown += new System.Windows.Input.KeyEventHandler(t_PreviewKeyDown);
        }

        private static Decimal GetSingleValue(DependencyObject obj)
        {
            return GetSingleDelta(obj);
        }

        private static Decimal GetDoubleValue(DependencyObject obj)
        {
            return GetSingleValue(obj) * 10;
        }

        private static Decimal GetTripleValue(DependencyObject obj)
        {
            return GetSingleValue(obj) * 100;
        }

        static void t_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
        {
            TextBox t = sender as TextBox;
            Decimal i;

            if (t == null)
                return;

            if (!Decimal.TryParse(t.Text, out i))
                return;

            switch (e.Key)
            {
                case System.Windows.Input.Key.Up:
                    if (Keyboard.Modifiers == ModifierKeys.Shift)
                        i += GetDoubleValue(t);
                    else
                        i += GetSingleValue(t);
                    break;

                case System.Windows.Input.Key.Down:
                    if (Keyboard.Modifiers == ModifierKeys.Shift)
                        i -= GetDoubleValue(t);
                    else
                        i -= GetSingleValue(t);
                    break;

                case System.Windows.Input.Key.PageUp:
                    i += GetTripleValue(t);
                    break;

                case System.Windows.Input.Key.PageDown:
                    i -= GetTripleValue(t);
                    break;

                default:
                    return;
            }

            if (BindingOperations.IsDataBound(t, TextBox.TextProperty))
            {
                try
                {
                    Binding binding = BindingOperations.GetBinding(t, TextBox.TextProperty);
                    t.Text = (string)binding.Converter.Convert(i, null, binding.ConverterParameter, binding.ConverterCulture);
                }
                catch
                {
                    t.Text = i.ToString();
                }
            }
            else
                t.Text = i.ToString();
        }
    }
}

I decided to simplify the reply marked as the answer on here to basically 2 lines using a LINQ expression.

e.Handled = !e.Text.All(Char.IsNumber);
base.OnPreviewTextInput(e);

I use a custom ValidationRule to check if text is numeric.

public class DoubleValidation : ValidationRule
{
    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (value is string)
        {
            double number;
            if (!Double.TryParse((value as string), out number))
                return new ValidationResult(false, "Please enter a valid number");
        }

        return ValidationResult.ValidResult;
    }

Then when I bind a TextBox to a numeric property, I add the new custom class to the Binding.ValidationRules collection. In the example below the validation rule is checked everytime the TextBox.Text changes.

<TextBox>
    <TextBox.Text>
        <Binding Path="MyNumericProperty" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:DoubleValidation/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

Why don't you just try using the KeyDown event rather than the PreviewKeyDown Event. You can stop the invalid characters there, but all the control characters are accepted. This seems to work for me:

private void NumericKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
    bool isNumPadNumeric = (e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9);
    bool isNumeric =((e.Key >= Key.D0 && e.Key <= Key.D9) && (e.KeyboardDevice.Modifiers == ModifierKeys.None));
    bool isDecimal = ((e.Key == Key.OemPeriod || e.Key == Key.Decimal) && (((TextBox)sender).Text.IndexOf('.') < 0));
    e.Handled = !(isNumPadNumeric || isNumeric || isDecimal);
}
public class NumericTextBox : TextBox
{
    public NumericTextBox()
        : base()
    {
        DataObject.AddPastingHandler(this, new DataObjectPastingEventHandler(CheckPasteFormat));
    }

    private Boolean CheckFormat(string text)
    {
        short val;
        return Int16.TryParse(text, out val);
    }

    private void CheckPasteFormat(object sender, DataObjectPastingEventArgs e)
    {
        var isText = e.SourceDataObject.GetDataPresent(System.Windows.DataFormats.Text, true);

        if (isText)
        {
            var text = e.SourceDataObject.GetData(DataFormats.Text) as string;
            if (CheckFormat(text))
            {
                return;
            }
        }

        e.CancelCommand();
    }

    protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
    {
        if (!CheckFormat(e.Text))
        {
            e.Handled = true;
        }
        else
        {
            base.OnPreviewTextInput(e);
        }
    }
}

Additionally you may customize the parsing behavior by providing appropriate dependency properties.

Combining the ideas from a few of these answers, I have created a NumericTextBox that

  • Handles decimals
  • Does some basic validation to ensure any entered '-' or '.' is valid
  • Handles pasted values

Please feel free to update if you can think of any other logic that should be included.

public class NumericTextBox : TextBox
{
    public NumericTextBox()
    {
        DataObject.AddPastingHandler(this, OnPaste);
    }

    private void OnPaste(object sender, DataObjectPastingEventArgs dataObjectPastingEventArgs)
    {
        var isText = dataObjectPastingEventArgs.SourceDataObject.GetDataPresent(System.Windows.DataFormats.Text, true);

        if (isText)
        {
            var text = dataObjectPastingEventArgs.SourceDataObject.GetData(DataFormats.Text) as string;
            if (IsTextValid(text))
            {
                return;
            }
        }

        dataObjectPastingEventArgs.CancelCommand();
    }

    private bool IsTextValid(string enteredText)
    {
        if (!enteredText.All(c => Char.IsNumber(c) || c == '.' || c == '-'))
        {
            return false;
        }

        //We only validation against unselected text since the selected text will be replaced by the entered text
        var unselectedText = this.Text.Remove(SelectionStart, SelectionLength);

        if (enteredText == "." && unselectedText.Contains("."))
        {
            return false;
        }

        if (enteredText == "-" && unselectedText.Length > 0)
        {
            return false;
        }

        return true;
    }

    protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
    {
        e.Handled = !IsTextValid(e.Text);
        base.OnPreviewTextInput(e);
    }
}

You can also try using data validation if users commit data before you use it. Doing that I found was fairly simple and cleaner than fiddling about with keys.

Otherwise, you could always disable Paste too!

My Version of Arcturus answer, can change the convert method used to work with int / uint / decimal / byte (for colours) or any other numeric format you care to use, also works with copy / paste

protected override void OnPreviewTextInput( System.Windows.Input.TextCompositionEventArgs e )
{
    try
    {
        if ( String.IsNullOrEmpty( SelectedText ) )
        {
            Convert.ToDecimal( this.Text.Insert( this.CaretIndex, e.Text ) );
        }
        else
        {
            Convert.ToDecimal( this.Text.Remove( this.SelectionStart, this.SelectionLength ).Insert( this.SelectionStart, e.Text ) );
        }
    }
    catch
    {
        // mark as handled if cannot convert string to decimal
        e.Handled = true;
    }

    base.OnPreviewTextInput( e );
}

N.B. Untested code.

Add this to the main solution to make sure the the binding is updated to zero when the textbox is cleared.

protected override void OnPreviewKeyUp(System.Windows.Input.KeyEventArgs e)
{
    base.OnPreviewKeyUp(e);

    if (BindingOperations.IsDataBound(this, TextBox.TextProperty))
    {
        if (this.Text.Length == 0)
        {
            this.SetValue(TextBox.TextProperty, "0");
            this.SelectAll();
        }
    }
}

Call me crazy, but why not put plus and minus buttons at either side of the TextBox control and simply prevent the TextBox from receiving cursor focus, thereby creating your own cheap NumericUpDown control?

private void txtNumericValue_PreviewKeyDown(object sender, KeyEventArgs e)
{
    KeyConverter converter = new KeyConverter();

    string key = converter.ConvertToString(e.Key);

    if (key != null && key.Length == 1)
    {
        e.Handled = Char.IsDigit(key[0]) == false;
    }
}

This is the easiest technique I've found to accomplish this. The down side is that the context menu of the TextBox still allows non-numerics via Paste. To resolve this quickly I simply added the attribute/property: ContextMenu="{x:Null}" to the TextBox thereby disabling it. Not ideal but for my scenario it will suffice.

Obviously you could add a few more keys/chars in the test to include additional acceptable values (e.g. '.', '$' etc...)

Private Sub Value1TextBox_PreviewTextInput(ByVal sender As Object, ByVal e As TextCompositionEventArgs) Handles Value1TextBox.PreviewTextInput
    Try
        If Not IsNumeric(e.Text) Then
            e.Handled = True
        End If
    Catch ex As Exception
    End Try
End Sub

Worked for me.

Can you not just use something like the following?

int numericValue = 0;

if (false == int.TryParse(yourInput, out numericValue))
{
    // handle non-numeric input
}
void PreviewTextInputHandler(object sender, TextCompositionEventArgs e)
{
    string sVal = e.Text;
    int val = 0;

    if (sVal != null && sVal.Length > 0)
    {
        if (int.TryParse(sVal, out val))
        {
            e.Handled = false;
        }
        else
        {
            e.Handled = true;
        }
    }
}

Can also use a converter like:

public class IntegerFormatConverter : IValueConverter
{
    public object Convert(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int result;
        int.TryParse(value.ToString(), out result);
        return result;
    }

    public object ConvertBack(object value, System.Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int result;
        int.TryParse(value.ToString(), out result);
        return result;
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top