NumericalBox - WPF TextBox's Subclass That Accepts Well-Formed Decimal Numbers Only

In this article you will learn WPF TextBox's subclass that accepts well-formed decimal numbers only in NumericalBox. TextBox accepts numbers of form [[-+]integer-part][.fractional-part][{eE}[-+]exponent] only.

Introduction

There exists a lot of TextBox's subclass implementations that allows you 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.

OnKey override
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.

CODE
            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

CODE

Let us implement all above mentioned in OnTextChanged:

Sn. 3: OnTextChanged 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.

123
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

NumericalBoxCtrl
So we should use a different regular expression for approvig incomplete numbers in the OnTextChanged override. This is it:

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:

CODE
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!

CODE
Our first "regular expression for numbers validation" should be used here. We will call it _rgxLostFocusValid now:

And here OnLostFocus is:

OnLostFocus
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
IsTextValid method extracts the longest valid substring. It uses rgxSave=_rgxSaveChanged

_rgxSaveLost
regular expression in case of incomplete number and rgxSave=_rgxSaveLost

regular expression in case of completed number:

number
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
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:

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:

Well-formed number
Fig. 2: Well-formed number

and tries to extract valid part of malformed number after LostFocus event:

Malformed number
Fig. 3: Malformed number

Malformed number
Fig. 4: Malformed number is corrected

NumericalBox.cs can be used wherever it is necessary, instead of TextBox.
 
Read more articles on WPF: