VB.Net Restricting TextBox to Positive Long or Double

11283
29
08-28-2012 03:24 PM
RichardFairhurst
MVP Honored Contributor
I have been searching the internet for ideas on how to restrict a VB.Net System.Windows.Forms.TextBox control to only accept positive Long or positive Double numeric values.  I think I have come up with a good control set up, primarily based on this post on msdn.  The code within the KeyPress and Leave events below is generic and can be pasted into any Textbox KeyPress and Leave events to produce the behaviors described below.  I am posting this in case it helps others and also to see if anyone has a more elegant way of doing this.

For this example, the behavior criteria of my Long Textbox are as follows:

1.  When the user finishes editing in the Textbox the value of the Textbox must be a Long interger that is positive and greater than or equal to 1 (in this example the Textbox represents an annual average daily trip count which cannot be negative and which is used as a divisor in an equation, so at least 1 annual average daily trip must occur on the road for a valid analysis).

2.  From the keyboard the user must only be able to type numbers.

3.  The user may paste text into the Textbox.  This introduces the possibility for insertion of negative numbers, floating values, and invalid entries which must be resolved in the following ways:
a.  If a negative number is pasted into the Textbox, it must be reset to its absolute value.
b.  If a floating number is pasted into the Textbox, it must be rounded to a whole interger.
c.  If the user pastes invalid text into the Textbox and does not correct an invalid entry prior to leaving the Textbox, the user will be permitted to exit the textbox, but the invalid value will be replaced by the minimum default value (in this case 1).

4.  If a user attempts to leave the Textbox with a blank entry or an entry of 0, the user is permitted to leave but the textbox will be completed using the minimum default value (in this case 1).

5.  Numbers must always be corrected to a conventional format when the user leaves the Textbox.  In other words, a user may enter a value of 023 into the Textbox, but when he exits the Textbox it must appear as 23.

Here is the code that implements this behavior.  The Textbox in this specific example is named txtAADT; however, the code written within the KeyPress and Leave Subs is generic and can be pasted into these two events for any other Textbox to implement the same behavior.
Imports System.Math

  Private Sub txtAADT_KeyPress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtAADT.KeyPress
    # If the keystroke is not a numeric digit, the keystroke will be disregarded.  Code works for any Textbox KeyPress event.
    If Not (Char.IsDigit(e.KeyChar) Or Char.IsControl(e.KeyChar)) Then
      e.Handled = True
    End If
  End Sub

  Private Sub txtAADT_Leave(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtAADT.Leave
    # User has completed their entry and is leaving the Textbox.

    # Create a Textbox variable to make the code below generic so that it can be pasted into any Textbox Leave event
    Dim tbx As System.Windows.Forms.TextBox = sender

    Dim value As Double
    If Not Double.TryParse(tbx.Text, value) Then
      # The user did not correct an invalid entry so replace it with 1
      tbx.Text = 1
    ElseIf value < -1 Then
      # The user pasted a negative value, so make sure it is rounded and take its absolute value
      tbx.Text = Abs(CLng(value))
    ElseIf value < 1 Then
      # The user has entered 0 or pasted in a value between -1 and 1, so replace with minimum value of 1
      tbx.Text = 1
    ElseIf value = vbNull Then
      # The user has blanked out the Textbox, so fill in the Textbox with the minimum value of 1
      tbx.Text = 1
    Else
      # The user has entered a valid postive number, so make sure it is rounded and appears as a conventional number
      tbx.Text = CLng(value)
    End If
  End Sub


The positive Double Textbox has similar requirements to the positive Long Textbox, but it allows an optional single decimal point to be inserted to create non-integer values.  (The code is not designed at this time to respond to local Cultural settings for the decimal character, so that would be a nice enhancement if anyone cares to suggest the code required to do that.)  The following additional or different behaviors are implement with the positive Double Textbox:

1.  When the user finishes editing in the Textbox the value of the Textbox must be a Double value that is positive and greater than or equal to 0 (in this example the Textbox represents a Distance of offset from an intersection which cannot be negative, but which may be 0 where no offset is desired).

2.  From the keyboard the user must only be able to type numbers and at most one decimal character.

3.  The user may paste text into the Textbox.  This introduces the possibility for insertion of negative numbers and invalid entries which must be resolved in the following ways:
a.  If a negative number is pasted into the Textbox, it must be reset to its absolute value.
b.  If the user pastes invalid text into the Textbox and does not correct an invalid entry prior to leaving the Textbox, the user will be permitted to exit the textbox, but the invalid value will be replaced by the minimum default value (in this case 0).

4.  If a user attempts to leave the Textbox with a blank entry, the user is permitted to leave but the textbox will be completed using the minimum default value (in this case 0).

5  A maximum of 4 fractional digits are permitted.  Any fractional digits beyond that must be rounded.  (This is due to the maximum resolution of the data)

6.  Numbers must always be corrected to a conventional format when the user leaves the Textbox.  In other words, a user may enter a value of 023.123456 into the Textbox, but when he exits the Textbox it must appear as 23.1235.

Here is the code for the positive Double Textbox.  The Textbox in this specific example is named txtDistance1; however, the code written within the KeyPress and Leave Subs is generic and can be pasted into these two events for any other Textbox to implement the same behavior.
Imports System.Math

  Private Sub txtDistance1_KeyPress(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtDistance1.KeyPress
    # Create a Textbox variable so that the code within this KeyPress event is generic for all positive Double Textboxes.
    Dim tbx As System.Windows.Forms.TextBox = sender
    If Not (Char.IsDigit(e.KeyChar) Or Char.IsControl(e.KeyChar) Or (e.KeyChar = "." And tbx.Text.IndexOf(".") < 0) Or (e.KeyChar = "." And tbx.Text.IndexOf(".") >= 0 And tbx.SelectionLength > 0 And tbx.SelectionStart <= tbx.Text.IndexOf(".") And tbx.SelectionStart + tbx.SelectionLength > tbx.Text.IndexOf("."))) Then
    # Explanation: Prevent the keystroke if it is not a numeric digit, a non-printable character such as a Backspace, or a decimal point where no decimal point exists in the Textbox or where the existing decimal point is contained in a text selection that will be overwritten by the new decimal point.
      e.Handled = True
    End If
  End Sub

  Private Sub txtDistance1_Leave(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtDistance1.Leave
    # User has completed their entry and is leaving the Textbox

    # Create a Textbox variable so that the code below is generic and can be pasted into any Textbox Leave event
    Dim tbx As System.Windows.Forms.TextBox = sender
    Dim value As Double
    If Not Double.TryParse(tbx.Text, value) Then
      # Textbox value is invalid, so replace it with the minimum default value (in this case 0)
      tbx.Text = 0
    ElseIf value = vbNull Then
      # The user has blanked out the Textbox, so fill in the Textbox with the minimum value of 0
      tbx.Text = 0
    ElseIf value >= 0 Then
      # value is positive so round to 4 decimal places and correct to conventional format
      tbx.Text = Round(value, 4)
    Else
      # value is negative so round to 4 decimal places, apply the absolute value and correct to conventional format
      tbx.Text = Abs(Round(value, 4))
    End If
  End Sub


Again, I hope this helps.
0 Kudos
29 Replies
LeoDonahue
Occasional Contributor III
What if the user pastes in a hex or unicode escape sequence for a number?  Something like 0x0039 or 9 (hilarious, the unicode I pasted in here was converted to the numeric value of 9, which is what I tried pasting in as unicode...)
http://msdn.microsoft.com/en-us/library/aa664669(v=vs.71).aspx

Also, rather than trap for correct input and settle for defaults, you can test the values of the text boxes for numeric and prompt the user for correct input. 

Your code is only checking for "leave" events, which you hope the user enters a value and leaves that text box.  What if the user enters junk into a text box and doesn't do anything to trigger the leave event before they submit?

Did you find this example?  http://msdn.microsoft.com/en-us/library/ms229644(v=vs.80).aspx#Y0
0 Kudos
RichardFairhurst
MVP Honored Contributor
What if the user pastes in a hex or unicode escape sequence for a number?  Something like 0x0039 or 9
http://msdn.microsoft.com/en-us/library/aa664669(v=vs.71).aspx

Also, rather than trap for correct input and settle for defaults, you can test the values of the text boxes for numeric and prompt the user for correct input. 

Your code is only checking for "leave" events, which you hope the user enters a value and leaves that text box.  What if the user enters junk into a text box and doesn't do anything to trigger the leave event before they submit?

Did you find this example?  http://msdn.microsoft.com/en-us/library/ms229644(v=vs.80).aspx#Y0


I hate the approach of locking a user into a field.  I won't do it for myself or them.  I don't ever want a prompt.  So that is not the way I will design my interface.  The pasting example may be a worry to programmers that work in the public shpere, but I only work for in house Engineers, who don't even know that hex codes or escape codes exist.  So far I have been unable to submit anything without leaving the dialog except by closing the dialog without posting, which I want to allow.  Anyway, I have my preferences and if they don't work for others, that is fine.  This is working the way I described and the way I want.

Anyway, I did not see the help you pointed to and it probably does help to make improvements over my code.  I searched for 3 hours on msdn and never hit on the search terms that brought that back.  Search systems are a failure more and more everywhere (especially microsoft's).

Edit:

Something seems to be missing from the example.  I got it to compile and it allows the keystrokes it says it will.  However, I cannot use an InputPanel in an Add-In form and there is no obvious indentification of where to implement the IntValue or DecimalValue to correct the user's input.  It lets the user put in multiple decimal points and negative signs, which I have prevented.  With the keyboard a user cannot enter junk in my Textbox or an invalid base 10 number, but a user can in this example.  Only pasting creates junk in my Textbox, which is a rare occurance for my particular users.  Based on the entries I can type in I would expect errors to arise from the IntValue and DecimalValue properties if I did access them.  Again, I understand that for programmers in the public sphere more possibilities need to be handled, but they simply are not worth an hour of my time for my users.

Edit 2:

I see the potential of the leave event not being fired.  Triggering that code in the Submit method is one of the options.  If you know of a better event that must fire before a user can execute a command button I would like to know about it.  It seems you are suggesting to validate it in the command button itself, which I can do to clean up junk.  There is no right way to do this (which is the key reason I love the Python script tool interface.  This stuff is built in with one click and it just works without me coding anything.  Unfortunately the script tool is slow to load and execute when too much code gets added, which is why I am fighting my frustrations and building it in VB.Net.)
0 Kudos
LeoDonahue
Occasional Contributor III
I wasn't thinking about locking a user into a text box before they can move on, rather, a label next to the text box indicating the value entered is invalid.

What happens if your user enters more than one decimal point?  Such as:  .........

Yes, searches only help if you have the right keywords.  I googled:  msdn restrict textbox to numbers.  First link.
0 Kudos
RichardFairhurst
MVP Honored Contributor
I wasn't thinking about locking a user into a text box before they can move on, rather, a label next to the text box indicating the value entered is invalid.

What happens if your user enters more than one decimal point?  Such as:  .........

Yes, searches only help if you have the right keywords.  I googled:  msdn restrict textbox to numbers.  First link.


Run my code.  Entering more than 1 decimal point in the Double Textbox is impossible with the keyboard.  The only way to cause this problem is with pasting (which is not the data entry method users of my form will normally use).

If the user insisted on pasting in such a value the code would replace it with the default value if a user insists on exiting the textbox before correcting that entry.  A more likely problem would be that they meant to paste over a decimal and instead pasted before or after it and thereby entered two decimal points without realizing it.  When they left the Textbox such an entry would be replaced by the default value.  I see the point of using a label to notify them about such replacements, but I generally would not let faulty input go uncorrected hoping the user would go back and correct it before pressing the execute button.

The alternative would be to disable the execute button until all entries were valid and use labels to highlight errors.  I assume that is the approach you are recommending.  I thought I had seen some kind of Validator class that could add error symbols and messages directly to controls (rather than having to generate my own labels all over the form), but I may be misremembering and mixing languages and interfaces.  I will try searching for it (wish me luck).

Speaking of funky searches, I had that problem with the Python website too.  I was trying to find a Python based method for closing a Windows program.  Nowhere was the word "close" associated with that action and I never got any hits in my search.  Later another user pointed out that the Python term you need to use for closing a Window was "Kill".  But if you search "python kill" you get a lot of articles about deaths in the Amazon and not much on the python language.  Sigh.
0 Kudos
KenBuja
MVP Esteemed Contributor
Here's a subroutine I found that I usually put in my text box KeyPress functions when I want to restrict what's being typed in. This doesn't account for hex values, but I doubt that the users would even know what they are.

   Friend Function ValidateKey(ByVal Key As Integer, ByVal KeyMask As String) As Boolean

        'I wrote this simple general-purpose key-validating function. You might find
        'it useful. just call it like this in your keypress event:

        '    If Not ValidateKey(Asc(e.KeyChar), "mask") Then e.Handled = True

        '"mask" contains pairs of characters. The function checks that your keypress
        'falls between the ascii range of each pair. For instance if you want to
        'allow only lowercase letters just use "az". If you want only numbers and
        'decimal points use "09.." Include as many pairs as you like.
        'PURPOSE: check that a keystroke was allowed by key mask. return true/false

        'declare variable

        Dim intCharCheck As Integer = 0

        Try
            'assume invalid
            ValidateKey = False

            Do Until intCharCheck = KeyMask.Length Or ValidateKey = True

                'check if keypress falls between pair of characters in KeyMask
                If Key >= Asc(KeyMask.Chars(intCharCheck)) And Key <= Asc(KeyMask.Chars(intCharCheck + 1)) Then ValidateKey = True

                'advance to next pair in KeyMask
                intCharCheck += 2

            Loop

            'allow backspace and Enter
            If Key = System.Windows.Forms.Keys.Back Then ValidateKey = True
        Catch ex As Exception
            System.Windows.Forms.MessageBox.Show(ex.ToString, "Utilities: ValidateKey")
        End Try

    End Function


When using this with numbers, I have additional checks to make sure there's not more than one decimal place or negative sign, in addition. Here's an example for a group of textboxes that can be decimal degree or degree minutes seconds or just numbers.

    Private Sub Numeric_KeyPress(sender As Object, e As System.Windows.Forms.KeyPressEventArgs) Handles txtLatD.KeyPress, txtLatM.KeyPress, txtLatS.KeyPress, txtLonD.KeyPress, txtLonM.KeyPress, txtLonS.KeyPress, txtLatDD.KeyPress, txtLonDD.KeyPress, txtDistance.KeyPress

        Dim Mask As String

        Select Case sender.name
            Case "txtLatD", "txtLonD"
                Mask = "09--"
            Case "txtLatDD", "txtLonDD"
                Mask = "09--.."
            Case Else
                Mask = "09.."
        End Select

        If Not ValidateKey(Asc(e.KeyChar), Mask) Then e.Handled = True
        If Asc(e.KeyChar) = 46 Then 'test for existing decimal point
            If InStr(sender.text, ".") > 0 Then e.Handled = True
        End If
        If Asc(e.KeyChar) = 45 Then 'test for exisiting negative sign
            If InStr(sender.text, "-") > 0 Then e.Handled = True
            If sender.selectionstart > 0 Then e.Handled = True 'Can't have a negative sign other than in the first position
        End If

    End Sub
0 Kudos
LeoDonahue
Occasional Contributor III
The alternative would be to disable the execute button until all entries were valid and use labels to highlight errors.  I assume that is the approach you are recommending.

I was thinking of something more straightforward, as in when the user clicks the execute button, a boolean method would first check the validity of the input as a group and set caption messages next to the invalid input. Once the boolean method returns true, continue on with the execute logic. Execute would be three methods.  The boolean validity check of the execute click action, the validity check itself, and the exectue.  Then you wouldn't have all of those keypress and leave events firing around.  Imagine if you had to debug this code and had to switch between the form and the code to trace your way through those events.

I will try searching for it (wish me luck)
lol.
0 Kudos
LeoDonahue
Occasional Contributor III
The validity check method could be as simple as the way this text changed method works.

http://msdn.microsoft.com/en-us/library/system.windows.forms.control.textchanged.aspx


Did you also look at the Masked Text Box?
http://msdn.microsoft.com/en-us/library/system.windows.forms.maskedtextbox.mask.aspx
0 Kudos
RichardFairhurst
MVP Honored Contributor
Imagine if you had to debug this code and had to switch between the form and the code to trace your way through those events.

lol.


The code I wrote does have such a problem in that the code is fully self contained to operate within a single method that has an obvious connection to the behavior of the Textbox.  And even if it did cause a problem it is not that much code or difficult to interprete with the commenting I have added.

Tracing your suggested code involving multiple boolean variables accessed across the entire form is much more likely to miss a code path, because I have to set and trap these flags all over the form, keeping track between methods that fire in orders that I have no control over or clear understanding of.  It is easy to set the flag to a condition indicating one or more conditions fails to pass the test for execution, but nearly impossible to set the flag to indicate that widely dispersed operations all combine to allow final form execution without a centeralize check method that queries every relavant part of the form.  Attempting to write such a centralize check is much more likely to result in  impossible code traces of branching if then else statements and in debug nightmares that isolating each part of the form to play its part in ensuring valid input.

Were I to use your suggested set of boolean variables I would be sure that they affected the enabled status of the execute button, so that I could eliminate the user's ability to proceed on that path until the variables are set correctly for execution and visually see on the form what the boolean variable state was for any given set of user actions to assist me in debugging the ever expanding code paths.  But I have gone down that road before and have always regretted it when I want to make additions or modifications to the forms behaviors.  Every new option affecting the boolean variables seems to result in an exponential growth of code paths and unexpected combinations of conditions that I have to spend a lot of time tracing out.

I still am not convinced that using default values to replace obvious garbage that can only be generated through rare user actions is a bad idea.

Have you actually run my code to see for yourself how it works?  I have been running the code you have referenced.
0 Kudos
KenBuja
MVP Esteemed Contributor
The problem with putting it into the TextChanged subroutine is that the user can put in invalid characters into the text box, whereas the KeyPress validation won't put those invalid characters there to begin with. Using the code from the MS example, you can still see something like this

[ATTACH=CONFIG]17327[/ATTACH]

It looks like the Masked Textbox requires more user interaction with decimal numbers, requiring them to position the cursor at the correct location to put in a number that has fewer numbers than the maximum number.
0 Kudos