tags:

views:

14601

answers:

14

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

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?

tags2k
You are crazy. :)
F.D.Castel
Speed of data entry. We have data entry operators who hammer data in using the keyboard (and often just the numeric keypad) so it's impractical for them to reach for the mouse half way through an entry screen.
Matt Hamilton
In that case, write your own re-usable NUD control and use it throughout your app!
RQDQ
+19  A: 
Arcturus
That's better than my approach in that it's less code and still allows control keys like the arrow keys and backspace etc.
Matt Hamilton
I just realised that neither approach will prevent the user from pasting non-numeric characters into the control, but that's not a huge issue right now. I'll mark your response as the answer 'coz it's as close as we can get, I think. Thanks!
Matt Hamilton
This will fail on decimals. If the user enters 5.3. It will also fail on negative numbers because "-" is not a number. Easy to fix but just thought I would throw that caution in there.
Kelly
e.Handled = !e.Text.ToCharArray().All(c => Char.IsNumber(c));
Si
+1  A: 

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!

Nidonocu
Yeah I'll definitely be validating anyway. I just like to prevent the user from being able to make mistakes as much as possible, so that there's next to no chance that they'll see an error popup.
Matt Hamilton
+6  A: 

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

.

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;
            if (t == null)
                return;

            Decimal i;
            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();
        }
    }
}
That's a great piece of code. The various NUD controls I've tried have always been buggy. Yours works like charm, thanks. :)
Echilon
A: 

Can you not just use something like the following?

int numericValue = 0;
if(false == int.TryParse(yourInput, out numericValue))
{
    // handle non-numeric input
}
Mark Allen
A: 
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...)

Sadly, my solution has holes since the Oem characters are not converted to strings with single characters (e.g. '.' = OemPeriod).
+4  A: 

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.

Eric
+1 for calling out the binding method.
fatcat1111
A: 
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.

Ritz
A: 
    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;
            }
        }
    }
bcd
A: 

A user could still copy and paste alphanumeric characters when only numeric are expected. How do we prohibit that?

Tushar
+1  A: 

For an application wide solution you can subclass TextBox and override the OnPreviewTextInput methods

public class NumericTextBox : TextBox
{
    protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
    {
        short val;
        if (!Int16.TryParse(e.Text, out val))
        {
            e.Handled = true;
        }
        else
        {
            base.OnPreviewTextInput(e);
        }
    }
}

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

juanagui
A: 

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();
            }
        }
    }
BrownBot
Interesting. Does it need to be wrapped in an IsDataBound call?
Matt Hamilton
+2  A: 

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);
Erode