Click here to Skip to main content
15,511,776 members
Articles / Desktop Programming / WPF
Tip/Trick
Posted 1 Oct 2015

Tagged as

Stats

35.6K views
1.1K downloads
17 bookmarked

Number Only Behavior for WPF

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
1 Oct 2015CPOL3 min read
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)


Written By
Software Developer (Senior) Clifford Nelson Consulting
United States United States
Has been working as a C# developer on contract for the last several years, including 3 years at Microsoft. Previously worked with Visual Basic and Microsoft Access VBA, and have developed code for Word, Excel and Outlook. Started working with WPF in 2007 when part of the Microsoft WPF team. For the last eight years has been working primarily as a senior WPF/C# and Silverlight/C# developer. Currently working as WPF developer with BioNano Genomics in San Diego, CA redesigning their UI for their camera system. he can be reached at qck1@hotmail.com.

Comments and Discussions

 
QuestionPeriod Not Allowed? Pin
Kevin Marois9-Apr-20 14:44
professionalKevin Marois9-Apr-20 14:44 
AnswerRe: Period Not Allowed? Pin
Jakub Olichwier27-Jul-20 5:54
Jakub Olichwier27-Jul-20 5:54 
GeneralRe: Period Not Allowed? Pin
Kevin Marois28-Dec-21 9:08
professionalKevin Marois28-Dec-21 9:08 
For some reason this won't work for a TextBox bound to a decimal.
If it's not broken, fix it until it is.
Everything makes sense in someone's mind.
Ya can't fix stupid.

AnswerRe: Period Not Allowed? Pin
Clifford Nelson25-Jan-22 7:02
Clifford Nelson25-Jan-22 7:02 
GeneralRe: Period Not Allowed? Pin
Kevin Marois25-Jan-22 16:04
professionalKevin Marois25-Jan-22 16:04 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson27-Jan-22 15:53
Clifford Nelson27-Jan-22 15:53 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson30-Jan-22 19:14
Clifford Nelson30-Jan-22 19:14 
GeneralRe: Period Not Allowed? Pin
Kevin Marois31-Jan-22 15:32
professionalKevin Marois31-Jan-22 15:32 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson1-Feb-22 4:42
Clifford Nelson1-Feb-22 4:42 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson1-Feb-22 15:58
Clifford Nelson1-Feb-22 15:58 
GeneralRe: Period Not Allowed? Pin
Kevin Marois2-Feb-22 13:23
professionalKevin Marois2-Feb-22 13:23 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson3-Feb-22 4:56
Clifford Nelson3-Feb-22 4:56 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson3-Feb-22 7:08
Clifford Nelson3-Feb-22 7:08 
AnswerRe: Period Not Allowed? Pin
Clifford Nelson7-Feb-22 13:56
Clifford Nelson7-Feb-22 13:56 
GeneralRe: Period Not Allowed? Pin
Kevin Marois7-Feb-22 16:11
professionalKevin Marois7-Feb-22 16:11 
QuestionWorking good Pin
hbehar4-Jun-17 1:10
hbehar4-Jun-17 1:10 
AnswerRe: Working good Pin
Clifford Nelson7-Jun-17 7:36
Clifford Nelson7-Jun-17 7:36 
QuestionSpace key? Pin
George Swan4-Oct-15 21:41
George Swan4-Oct-15 21:41 
AnswerRe: Space key? Pin
Clifford Nelson3-Mar-16 11:54
Clifford Nelson3-Mar-16 11:54 
QuestionI've done it in the model Pin
marc borgers3-Oct-15 22:25
marc borgers3-Oct-15 22:25 
AnswerRe: I've done it in the model Pin
Clifford Nelson4-Oct-15 10:01
Clifford Nelson4-Oct-15 10:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.