Introduction
There exist a lot of TextBox's subclass implementations that allow to type in numbers only. Generally a rejection of malformed numbers is provided on an additional validation stage and such freaks as 1+234 or .+1e23.4 are allowed during typing stage. We will set typing constraints so that only well-formed numbers may be typed in.
Regular expression approach
We want only numbers of form [[-+]integer-part][.fractional-part][{eE}[-+]exponent] to be allowed for typing in.
That is, such numbers as 123 , +123 , 123.45 , 12e+45 , etc. are allowed for typing in and such numbers as 123. , 12.34.5 , 1+2345e , etc. are forbidden. At the same time we wouldn't care about numbers convertibility to machine form (e.g., by value). We will check the syntactical form of the number only.
Good old regular expressions that were invented by great Stephen Kleene back in the 1950s pave our way to the goal. We will use C# dialect of the regular expressions.
We will implement a NumericalBox - a little subclass of the System.Windows.Controls.TextBox class. The implementation consists of 3 method overrides and an auxiliary method only. Let us consider these overrides step by step.
OnKey override
OnKey is a method that gives the right of way to allowed keys only. We hide the visibility of the OnKey event to listeners along the event route by setting KeyEventArgs.Handled=true
for not allowed keys. For allowed keys listeners will be invoked.
protected override void OnKeyDown(KeyEventArgs e)
{
switch ( e.Key )
{
case Key.E:
case Key.OemPlus:
case Key.OemMinus:
case Key.OemPeriod:
case Key.Subtract:
case Key.Add:
case Key.Decimal:
case Key.D0:
case Key.D1:
case Key.D2:
case Key.D3:
case Key.D4:
case Key.D5:
case Key.D6:
case Key.D7:
case Key.D8:
case Key.D9:
case Key.NumPad0:
case Key.NumPad1:
case Key.NumPad2:
case Key.NumPad3:
case Key.NumPad4:
case Key.NumPad5:
case Key.NumPad6:
case Key.NumPad7:
case Key.NumPad8:
case Key.NumPad9:
case Key.Back:
break;
default:
e.Handled = true;
break;
}
}
Sn. 1: OnKey method - file NumericalBox.cs, project NumericalBoxCtrl
OnKey will prevent meaningless keys but would not prevent malformed numbers such as +..1e.e.1
, etc.
Regular expression for numbers validation
It looks like a good idea to check a number's syntax during the text change - i.e. during the typing in. Now the first regular expression comes to the fore. It is:
[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?
Meaning:
[-+]? - optional sign section: matches either -
, or +
, or nothing
\d* - integer section: matches either 0, or more decimal digits
(\.?) - decimal point section: group that matches either 0, or 1 decimal point
(\d+) - fractional section: group that matches either 1, or more decimal digits
([eE][-+]?\d+)? - exponent section: group that matches either nothing, or exponent base (e or E) followed by optional sign, followed by either 1, or more decimal digits.
The regular expression is provided as a variable of type Regex; parameter value RegexOptions.ECMAScript narrows possible set of digits representation to the English one only.
Regex _rgxChangedValid = new Regex( @"^[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?$" );
Sn. 2: Regex variable - file NumericalBox.cs, project NumericalBoxCtrl
Anchors ^ and $ say that the match starts at the beginning of the string and must occur at the end of the string.
OnTextChanged override - first attempt - project NumericalBoxCtrlJobHalfDone
Let us implement all above mentioned in OnTextChanged:
protected override void OnTextChanged( TextChangedEventArgs e )
{
base.OnTextChanged( e );
if ( !IsInputTextValid( Text ) )
{
Text = _lastVerifiedText;
}
_lastVerifiedText = Text;
CaretIndex = Text.Length;
}
private bool IsInputTextValid( string text )
{
return !string.IsNullOrEmpty( text == null ? null : text.Trim( ) )
? ( _rgxChangedValid.IsMatch( text ) ? true : false )
: false;
}
Sn. 3: OnChangeText override and IsInputTextValid method - file NumericalBox.cs, project NumericalBoxCtrlJobHalfDone
Private variable _lastVerifiedText
stores the last typed string that passed regular expression check and is used for restoring the last good version. More convenient way of this restoration will be shown in the final project NumericalTextBox.
Now let us try the project NumericalTextBoxCtrlJobHalfDone - start the Demo NumericalTextBoxCtrlJobHalfDone.exe and begin to type in number 123.45 in the text box.
Fig. 1: Control does not allow to type in 123. or 123e
The attempt fails! We are not able to type in the decimal point.
Cause of failure
The thing is that our _rgxChangedValid
describes a completed well-formed number; for example, it has to contain at least one digit after the decimal point. However, during sequential typing the user is not able to type in the decimal point and the first digit after it simultaneously - decimal point always comes first and is rejected.
_rgxChangedValid
expects the completed number and at the moment we have only a job half-done. "Don't show a fool a job half-done!"
OnTextChanged override - second attempt - project NumericalBoxCtrl
So we should use a different regular expression for approvig incomplete numbers in the OnTextChanged override. This is it:
Regex _rgxChangedValid = new Regex( @"^[-+]?\d*(\.?)(\d*)([eE][-+]?\d*)?$", RegexOptions.ECMAScript );
Sn. 4: Regex variable for approve incomplete numbers - file NumericalBox.cs, project NumericalBoxCtrl
There are only two differences between NumericalBoxCtrlJobHalfDone _rgxChangeValid
ang NumericalBoxCtrl _rgxChangedValid
. The second regular expression has \d*
on the second and on the third digit positions. The quantifier *
means "matching the previous element zero or more times". That means "zero or more digits" in our case, and in turn that means that strings "123." and "123e" are allowed. Now this is the code of the new version of OnTextChanged:
protected override void OnTextChanged( TextChangedEventArgs e )
{
base.OnTextChanged( e );
string longestValidText;
if ( !IsTextValid( _rgxChangedValid, _rgxSaveChanged, Text, out longestValidText ) )
{
this.Text = longestValidText;
}
CaretIndex = Text.Length;
}
Sn. 4: OnTextChanged - file NumericalBox.cs, project NumericalBoxCtrl
The private method IsTextValid (it will be explained below) checks the NumericalBox content with the help of _rgxChangedValid
and tries to extract the longest valid substring from the invalid content.
OnLostFocus override
Now, the completed number should be checked also - when it is completed, of course. It is the LostFocus
event, that is the criterion of completeness: a number should be considered completed when the NumericalBox loses the focus. Microsoft did not accidentally choose Mode=LostFocus
for bindings from the TextBox!
Our first "regular expression for numbers validation" should be used here. We will call it _rgxLostFocusValid
now:
Regex _rgxLostFocusValid = new Regex( @"^[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?$", RegexOptions.ECMAScript );
And here OnLostFocus is:
protected override void OnLostFocus( RoutedEventArgs e )
{
base.OnLostFocus( e );
string longestValidText;
if(!IsTextValid( _rgxLostFocusValid, _rgxSaveLost, Text, out longestValidText ))
{
this.Text = longestValidText;
}
}
Sn. 5: OnLostFocus - file NumericalBox.cs, project NumericalBoxCtrl
It looks almost like OnTextChanged except it uses _rgxLostFocusValid
regular expression and does not place the cursor at the end of the string because of the cursor is cancelled by loss of focus.
By the way, about the loss of focus. It is performed by mouse click outside the box and I apologize that it is implemented in code behind. MVVM implementation of LostFocus
event looks a bit ponderous for such a short project.
IsTextValid method and extraction of valid substring
IsTextValid method extracts the longest valid substring. It uses rgxSave=_rgxSaveChanged
Regex _rgxSaveChanged = new Regex( @"[-+]?\d*(\.?)(\d*)([eE][-+]?\d*)?", RegexOptions.ECMAScript );
regular expression in case of incomplete number and rgxSave=_rgxSaveLost
Regex _rgxSaveLost = new Regex( @"[-+]?\d*(\.?)(\d+)([eE][-+]?\d+)?", RegexOptions.ECMAScript );
regular expression in case of completed number:
longestValidSubstr = rgxSave.Match( text ).Value;
Sn. 6: excerpt from IsTextValid method - file NumericalBox.cs, project NumericalBoxCtrl
Both _rgxSaveChanged
and _rgxLostChanged
have no ^ and $ anchors for extracting a valid substring from the middle of text.
IsTextValid method also validates text by means of Regex.Match
method, where rgxValid
equals to _rgxChangedValid
and _rgxLostValid
in the "changed" validation and in the "lost focus" validation respectively:
return !string.IsNullOrEmpty( text == null ? null : text.Trim( ) ) ?
rgxValid.IsMatch( text ) :
false;
Sn. 7: excerpt from IsTextValid method - file NumericalBox.cs, project NumericalBoxCtrl
Using the code
Try the Demo NumericalBoxCtrl.exe.
Now NumericalBox allows well-formed numbers:
Fig. 2: Well-formed number
and tries to extract valid part of malformed number after LostFocus event:
Fig. 3: Malformed number
Fig. 4: Malformed number is corrected
NumericalBox.cs can be used wherever it is necessary, instead of TextBox.
History
2015-09-09 bad references to pictures were repaired
2015-09-13 grammar and typo were corrected; some bugs in the code were fixed