Introduction

This is an oft-asked question in the VB.Net Forums as there are many uses for a specialized TextBox control which will only accept numeric, or otherwise constrained, input.  While the most common request is probably for a TextBox which only accepts numbers as input, there could also be times when you only want upper-case letters or some other series of predefined characters.

There are a variety of possible ways to handle requirements like these, and the exact method chosen will likely depend on the specific kind of input that you wish to constrain.  However, there are a few common techniques which could apply to most any requirement and one caveat which must always be considered.  This article will explore these techniques and explain the caveat through an example of a NumericTextBox control which only accepts numeric input; that is, it requires a decimal number or currency value.

Custom TextBox Control

While it would be possible to implement most of the following techniques through the event handlers of a standard TextBox, it is often more useful to create a new custom control which inherits from TextBox and encapsulates all of the input constraining logic.  For this example we will create a custom class called NumericTextBox:

Option Strict On
Imports System.Globalization
 
Public Class NumericTextBox
    Inherits TextBox
 
End Class

For more information on creating custom controls, see Developing Custom Windows Forms Controls with the .Net Framework.

Input Validation

The most basic solution to ensuring that a TextBox contains a specific value is to handle the Validating event and test the current Text property value.  With this one simple mechanism you can ensure that the user will be forced to supply a valid value in the TextBox because the Form will not allow any other input until the control passes validation.  In the case of numeric input, all that is necessary is ensuring that the TextBox.Text can be parsed into a decimal:

Option Strict On
Imports System.Globalization
 
Public Class NumericTextBox
    Inherits TextBox
 
    Protected Overrides Sub OnValidating(e As System.ComponentModel.CancelEventArgs)
        If TextLength > 0 AndAlso Not Decimal.TryParse(Text, NumberStyles.Any, Nothing, Nothing) Then
            SelectAll()
            e.Cancel = True
        End If
        MyBase.OnValidating(e)
    End Sub
 
End Class

In this example the validation simply ensures that some text has been entered and that it can be parsed into a decimal number.  There are a variety of ways to implement the test and you may wish to specify a more strict set of NumberStyles or perform some other test more suited to the requirements.  The main point to understand is that a simple validation routine is all that is really necessary to ensure valid control input.  For more information in control validation, see the Validating Event documentation.

But while validation ultimately ensures valid user input, it does not actually constrain the characters entered by the user.  The user could still type invalid characters and they would appear in the TextBox; the user would simply be forbidden from leaving the TextBox until the input was corrected.  To actually constrain the input, we will need to handle an input-related event.

Constraining Input

There are multiple opportunities to intercept, modify, or otherwise constrain the user input to a control.  The most fundamental mechanism would involve intercepting the Windows Messages representing various user input during the control's WndProc method execution, or could include a modification of character processing via handling of IsInputChar (or other preprocessing).  While this technique may offer the most granular control over input constraints, it could also become complex to implement and could introduce bugs if implemented incorrectly.

Since we are primarily interested in keyboard input we can probably utilize one or more of the control's KeyDown, KeyPress, and KeyUp events.  Of these three events, KeyPress is probably the most appropriate for our needs.  Within the event handler (or rather, the override of the method which raises the event) it is easy to access the Char instance associated with the key press and to suppress the character when necessary by setting the Handled property on the EventArgs instance.  In pseudo code, this looks something like:

Public Class NumericTextBox
    Inherits TextBox
 
    Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
        'If e.KeyChar [is valid] Then
        '   e.Handled = False
        'ElseIf e.KeyChar [is invalid] Then
        '   e.Handled = True
        'End If
        MyBase.OnKeyPress(e)
    End Sub
 
End Class

Quite simply, the code which handles a key press needs only to determine if the associated KeyChar is considered valid input and then set e.Handled to False when it is not.  So how do we determine what input is valid?

Determining Valid Input

This is the primary portion of the solution which will vary based on the kind of input being constrained.  Since we are working with instances of Char structures, one obvious technique would be to hold a list of valid characters and then ensure that each input character was contained in the list.  While this works, and may even be necessary in some situations, there is another technique which can often be useful when the objective is to categorize characters.

By calling the CharUnicodeInfo.GetUnicodeCategory method we can get a UnicodeCategory value which may help us quickly determine that a particular character is valid input.  In the case of our NumericTextBox, we could check to ensure that the character is either a decimal digit or a control character such as backspace or enter:

Public Class NumericTextBox
    Inherits TextBox
 
    Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
        'assume that input character is invalid
        e.Handled = True
        Dim category As UnicodeCategory = CharUnicodeInfo.GetUnicodeCategory(e.KeyChar)
        'if the category is a control char (backspace, enter, etc) or a decimal digit...
        If category = UnicodeCategory.Control OrElse category = UnicodeCategory.DecimalDigitNumber Then
            'specify that the input is valid
            e.Handled = False
        End If
        MyBase.OnKeyPress(e)
    End Sub
 
End Class

While this works nicely for any number character, it doesn't allow for a decimal point, thousands separator, or negative sign.  And what if we want to allow a currency symbol?  To address these concerns we can include a collection of predefined characters to allow.  So while we may not have gotten away from the more obvious solution of building a list of valid characters, utilizing the unicode category technique helps to limit the number of characters we have to take into consideration.

Localizing Validation

When we build our list of additional allowed characters, we should attempt to take the user's local culture settings into account.  We can do this by accessing the NumberFormatInfo of the CurrentCulture to determine what characters to use for things like the decimal point, group separator, and negative sign.

Within our custom class we can define a list of characters and pre-populate it with the appropriate localized characters.  Then we can modify the OnKeyPress code to also check the list for valid input:

Public Class NumericTextBox
    Inherits TextBox
 
    Private allowedCharacters As New List(Of Char)({CChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NegativeSign)})
 
    Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
        e.Handled = True
        If allowedCharacters.Contains(e.KeyChar) Then
            e.Handled = False
        Else
            Dim category As UnicodeCategory = CharUnicodeInfo.GetUnicodeCategory(e.KeyChar)
            If category = UnicodeCategory.Control OrElse category = UnicodeCategory.DecimalDigitNumber Then
                e.Handled = False
            End If
        End If
        MyBase.OnKeyPress(e)
    End Sub
 
End Class

With this code we now have a TextBox which will only accept characters used to construct decimal numbers; any other key input will be ignored.  But what happens if the user pastes text into the TextBox?

The Caveat:  Pasting

A user pasting text from the clipboard is the important caveat mentioned in the beginning of this article.  Regardless of which input constraint techniques we implement, we must always be mindful of copy/paste functionality.  As our NumericTextBox control stands right now, the user can still paste invalid characters into our control.

While there may be more than one way to deal with invalid text being pasted into the control, the best solution may be to simply prevent the paste from occurring in the first place.  We could simply suppress the CTRL+C input combination during KeyDown, but that would eliminate all paste functionality.  It would be better to allow pasting so long as the value on the clipboard is a number.  

To accomplish this we will need to reach into Windows message processing and override WndProc, however, the logic that we need to execute is really quite simple once we've determined the appropriate message to process:

Public Class NumericTextBox
    Inherits TextBox
 
    Protected Overrides Sub WndProc(ByRef m As Message)
        'if the message is WM_PASTE (0x0302)...
        If m.Msg = &H302 Then
            'if the clipboard contains text and that text is not numeric
            If Clipboard.ContainsText AndAlso Not IsNumeric(Clipboard.GetText) Then
                'update the message identifier to "no operation"
                m.Msg = 0
            End If
        End If
        'allow base processing of the final message
        MyBase.WndProc(m)
    End Sub
End Class

We can use the API documentation to determine that WM_PASTE has a value of 0x0302 and this gives us the message ID that our code needs to look for.  When this message occurs we can test the contents of the clipboard to ensure that it contains text which can be represented a numeric input.  If that is not the case, then we simply change the message ID to zero (the "no operation" message ID) which causes the control to ignore the paste message.  In this way the paste operation never actually occurs on the control.

Allowing Currency

If we wanted our NumericTextBox control to optionally allow the user to include a currency symbol we could provide a property whose setter adjusts the internal list of valid characters:

Public Class NumericTextBox
    Inherits TextBox
 
    Private allowedCharacters As New List(Of Char)({CChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NegativeSign)})
 
    Private _AllowCurrency As Boolean = False
    Public Property AllowCurrency As Boolean
        Get
            Return _AllowCurrency
        End Get
        Set(value As Boolean)
            If Not _AllowCurrency = value Then
                If _AllowCurrency Then
                    allowedCharacters.Remove(CChar(CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol))
                Else
                    allowedCharacters.Add(CChar(CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol))
                End If
                _AllowCurrency = value
            End If
        End Set
    End Property
 
End Class

With this property in place, whenever the property value changes, the associated currency symbol character is added to or removed from the allowed characters list as appropriate.  We could add a similar property to support the percentage symbol, or really, any other characters that we might want to optionally allow.

Tying it all Together

To create a fully robust NumericTextBox control, we will need to combine all of the above techniques into a single object.  We really want both the KeyPress and Validating code so that not only are the individual characters in the TextBox considered numeric, but the overall text value is also considered a valid number.  Without the Validating code, the user could enter text like "123.45,12-1" because each individual character is valid even though the overall string is not.  However, we don't have to require the validation functionality and can add a simple boolean property to let the developer decide whether or not they want that functionality in a particular instance of the NumericTextBox control.

In our KeyPress character analysis we can use techniques like examining the Unicode category of the character, or comparing the character to a known list to determine valid input.  And in our Validating code we can ensure that the entire text value is considered a valid decimal number, according to whatever formatting is appropriate.

Finally we need to capture and analyze any attempt to paste text into the control and suppress the action if the clipboard does not contain numeric text.  The complete code for our NumericTextBox class then becomes:

Option Strict On
Imports System.Globalization
 
Public Class NumericTextBox
    Inherits TextBox
 
    Private allowedCharacters As New List(Of Char)({CChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NumberGroupSeparator),
                              CChar(CultureInfo.CurrentCulture.NumberFormat.NegativeSign)})
 
    Private _AllowCurrency As Boolean = False
    Public Property AllowCurrency As Boolean
        Get
            Return _AllowCurrency
        End Get
        Set(value As Boolean)
            If Not _AllowCurrency = value Then
                If _AllowCurrency Then
                    allowedCharacters.Remove(CChar(CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol))
                Else
                    allowedCharacters.Add(CChar(CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol))
                End If
                _AllowCurrency = value
            End If
        End Set
    End Property
 
    Public Property EnforceDecimal As Boolean = True
 
    Protected Overrides Sub OnKeyPress(e As KeyPressEventArgs)
        e.Handled = True
        If allowedCharacters.Contains(e.KeyChar) Then
            e.Handled = False
        Else
            Dim category As UnicodeCategory = CharUnicodeInfo.GetUnicodeCategory(e.KeyChar)
            If category = UnicodeCategory.Control OrElse category = UnicodeCategory.DecimalDigitNumber Then
                e.Handled = False
            End If
        End If
        MyBase.OnKeyPress(e)
    End Sub
 
    Protected Overrides Sub OnValidating(e As System.ComponentModel.CancelEventArgs)
        If EnforceDecimal Then
            If TextLength > 0 AndAlso Not Decimal.TryParse(Text, NumberStyles.Any, Nothing, Nothing) Then
                SelectAll()
                e.Cancel = True
            End If
        End If
        MyBase.OnValidating(e)
    End Sub
 
    Protected Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = &H302 Then
            If Clipboard.ContainsText AndAlso Not IsNumeric(Clipboard.GetText) Then
                m.Msg = 0
            End If
        End If
        MyBase.WndProc(m)
    End Sub
End Class

Summary

While there are any number of ways to constrain the input of a TextBox control, creating a custom TextBox with both KeyPress and Validating method handlers provides a robust and efficient solution.  If at all possible, it is best to avoid any processing in the TextChanged event and to enforce all constraints at the time an action occurs (such as a key press or a paste operation).

The example control in this article is designed specifically for numeric input but the same techniques of analyzing the Unicode category or comparing to a list of known characters, and filtering clipboard contents during a paste operation, could be used to to support any kind of input constraints.