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:
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:
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:
private static void OnTextInput(object sender, TextCompositionEventArgs e)
{
string adjustedText = string.Empty;
var dependencyObject = sender as DependencyObject;
var txtBox = sender as 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;
}
void HandleSigns()
{
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;
}
}
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;
}
}
}
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:
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:
<TextBox Style="{StaticResource EditFormTextBoxNumericStyle}"
behaviours:NumberOnlyBehaviour.Mode="PositiveWholeNumber"
Text="{Binding Sequence,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
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