Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Number Only Behavior for WPF

5.00/5 (11 votes)
1 Oct 2015CPOL3 min read 47.1K   1.2K  
Presents a behavior to prevent entry of anything but digits into a control
The original verion of this control only allowed numeric entry, an update allowed the minus sign and period. If the minus sign is pressed the sign of the number is reversed. If the period is pressed, the that will set the position, or new position of the decimal point

Introduction

I have struggled to try and create a good behavior to allow users only to enter number keys. Of course, most of my problem was I did not know the right approach. Seemed that the event to use was the KeyPress event, but it is not because there is no easy way to distinguish if the user has entered a digit or not because the event arguments return only the key, and not the resulting character. The only good way that I know of is to attach to the TextInput event. In this event, I use LINQ to check each if any of the characters of the string are not a digit, and if they are not, set the Handled equal to true. I have seen a lot of implementations that use a Regular Expression to check if all the characters are digits, but I am sure that this is a lot heavier than just checking that all are digits.

A modification was made  on 01/27/2022 to handle a period, plus or minus sign being entered. Minus sign will change the sign of the result, either removing the negative sign at the beginning or putting a negative sign at the beginning. The plus sign will force a positive number The period will place the decimal point at the current locaiton even if there is already a decimal point.

I also use the DataObject.AddPastingHandler to add an event handler that will ensure that pasted values are text and only contain digits. This still should be updated for decimal point and sign entries. Right now it only will allow digits to be input

The Code

There is one DependencyProperty for this behavior:

C#
public static readonly DependencyProperty ModeProperty =
      DependencyProperty.RegisterAttached("Mode", typeof(NumberOnlyBehaviourModes?),
      typeof(NumberOnlyBehaviour), new UIPropertyMetadata(null, OnValueChanged));

  public static NumberOnlyBehaviourModes? GetMode(DependencyObject o)
      { return (NumberOnlyBehaviourModes?)o.GetValue(ModeProperty); }

  public static void SetMode(DependencyObject o, NumberOnlyBehaviourModes? value)
      { o.SetValue(ModeProperty, value); }

I orignially used the bool IsEnabled property but when I enabled the typing of sign keys and decimal, allowed the specifying behavior to support only input of numbers, or allowing also signs to support all whole numbers, or signs and decminal key to allow entry of decimal numbers.

The event handler for this DespendencyProperty is OnValueChanged. The code is as follows:

C#
private static void OnValueChanged(DependencyObject dependencyObject,
  DependencyPropertyChangedEventArgs e)
{
  var uiElement = dependencyObject as Control;
  if (uiElement == null) return;
  if (e.NewValue is bool && (bool)e.NewValue)
  {
    uiElement.PreviewTextInput += OnTextInput;
    uiElement.PreviewKeyDown += OnPreviewKeyDown;
    DataObject.AddPastingHandler(uiElement, OnPaste);
  }

  else
  {
    uiElement.PreviewTextInput -= OnTextInput;
    uiElement.PreviewKeyDown -= OnPreviewKeyDown;
    DataObject.RemovePastingHandler(uiElement, OnPaste);
  }
}

Basically this event handler subscribes to the two events that will filter out any user input that is not a number, and also add a handler that will deal with pasting of text when the behavior is enabled by setting the IsEnabled equal to true, and otherwise removes the handlers.

The two keyboard event handlers are:

C#
private static void OnTextInput(object sender, TextCompositionEventArgs e)
{
  string adjustedText = string.Empty;
  var dependencyObject = sender as DependencyObject;
  var txtBox = sender as TextBox;
  // Right now only handle special cases if TextBox
  var mode = txtBox == null ? NumberOnlyBehaviourModes.PositiveWholeNumber 
       : GetMode(dependencyObject);

  switch (mode)
  {
    case NumberOnlyBehaviourModes.Decimal:
       if (e.Text.Any(c => !char.IsDigit(c))) e.Handled = true;
       HandleSigns();
       HandleDecimalPoint();
       break;
    case NumberOnlyBehaviourModes.PositiveWholeNumber:
       if (e.Text.Any(c => !char.IsDigit(c))) e.Handled = true;
       break;
    case NumberOnlyBehaviourModes.WholeNumber:
       if (e.Text.Any(c => !char.IsDigit(c))) e.Handled = true;
       HandleSigns();
       break;
  }

  // Handle plus and minus signs. Plus always makes positive, minus reverses sign
  void HandleSigns()
  {
    // Handle minus sign, changing sign of number if pressed
    if (e.Text[0] == '-')
    {
       var nonSelectedTest = GetNonSelectedTest(txtBox);

       if (nonSelectedTest.Length == 0)
       {
          e.Handled = false;
       }
       else if (nonSelectedTest.First() == '-')
       {
          var startPos = txtBox.SelectionStart;
          txtBox.Text = nonSelectedTest.Substring(1);
          txtBox.SelectionStart = startPos - 1;
       }
       else
       {
          var startPos = txtBox.SelectionStart;
          txtBox.Text = "-" + nonSelectedTest;
          txtBox.SelectionStart = startPos + 1;
       }
    }

    // Handle plus sign, forcing number to be positive
    else if (e.Text[0] == '+')
    {
       var nonSelectedTest = GetNonSelectedTest(txtBox);

       if (nonSelectedTest.Length > 0 && nonSelectedTest.First() == '-')
       {
          var startPos = txtBox.SelectionStart;
          txtBox.Text = nonSelectedTest.Substring(1);
          txtBox.SelectionStart = startPos - 1;
       }
    }
  }

  // Handle decimal sign, always putting decimal at current position even when 
  // there is a decimal point
  void HandleDecimalPoint()
  {
    if (e.Text[0] == '.')
    {
       var nonSelectedTest = GetNonSelectedTest(txtBox);

       if (nonSelectedTest.Contains("."))
       {
          var startPos = txtBox.SelectionStart;
          var decimalIndex = nonSelectedTest.IndexOf(".");
          var newText = nonSelectedTest.Replace(".", "");
          if (startPos > decimalIndex) startPos--;
          txtBox.Text = newText.Substring(0, startPos) + "."
             + newText.Substring(startPos);
          txtBox.SelectionStart = startPos + 1;
          e.Handled = true;
       }
       else
       {
          e.Handled = false;
       }
    }
  }
}


private static void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
 if (e.Key == Key.Space)e.Handled = true;
}

The OnPreviewKeyDown event handler is required because the PreviewTextInput event does not fire when the space key is pressed, so also have include an event handler for the PreviewKeyDown. All other key presses cause a change in the TextInput, so the checking if the text contains a non-digit value. Unfortunately it is very difficult to know the effect of the key press on the KeyDown or KeyUp events because the KeyEventArgs only provide the enumeration of the Key and not the effect, so handling only one KeyPress event is very hard to implement.

And finally the paste handler:

C#
private static void OnPaste(object sender, DataObjectPastingEventArgs e)
{
 if (e.DataObject.GetDataPresent(DataFormats.Text))
 {
  var text = Convert.ToString(e.DataObject.GetData(DataFormats.Text)).Trim();
  if (text.Any(c => !char.IsDigit(c))) { e.CancelCommand(); }
 }
 else
 {
  e.CancelCommand();
 }
}

How to use this behaviour

To use this behavior is very easy. On the control you want to allow only numbers, you would add this behavior as shown in bold:

XML
<TextBox Style="{StaticResource EditFormTextBoxNumericStyle}"
	behaviours:NumberOnlyBehaviour.Mode="PositiveWholeNumber"
	Text="{Binding Sequence,
		UpdateSourceTrigger=PropertyChanged,
		ValidatesOnDataErrors=True}" />

Image 1

History

  • 2015/10/15: Initial version
  • 2016/03/04 added updated source code to handle the space key as recommended by George Swan.
  • 2022/01/27: Added Decimal and negative number capability
  • 2022/02/01: Fixed bug when enter a minus with no text, and added plus sign handling
  • 2022/02/03: Rearchitected the handling of keystrokes to fix bugs associated with having selected text. Also added mode so can select for only numbers (positive whole numbers), numbers and a sign (whole numbers), or decimal numbers with sign

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)