Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

NumericalBox - WPF TextBox's subclass that accepts well-formed decimal numbers only

0.00/5 (No votes)
9 Sep 2015 1  
TextBox that accepts numbers of form [[-+]integer-part][.fractional-part][{eE}[-+]exponent] only

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

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here