VB.Net Restricting TextBox to Positive Long or Double

11252
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
  Is there a way to detect that a hot key was used?

You would assign the hot key by setting the Text property of your button, so you would be in control of that.  An end user can not create their own hot key, that I am aware of.
0 Kudos
RichardFairhurst
MVP Honored Contributor
You would assign the hot key by setting the Text property of your button, so you would be in control of that.  An end user can not create their own hot key, that I am aware of.


I wasn't refering to whether or not a hot key had been set up.  I was asking if I can detect that the hot key event was activated by the user as distinguished from the button press event?  I suppose that I could just run through the full control collection to see which object had focus and force the Leave event to fire on any object other than the button that was active, rather than having to validate all of the objects that had previously been validated through their Leave events.
0 Kudos
LeoDonahue
Occasional Contributor III
I wasn't refering to whether or not a hot key had been set up.  I was asking if I can detect that the hot key event was activated by the user as distinguished from the button press event?

Sorry, I misunderstood you.

I suppose you can track key press combination events, such as alt + s (the exampe I gave earlier with &Submit).

I re-read your original post and it seems like there is an easier way.  I saw where you are creating some generic code that can be pasted into every textbox's leave event?  That seems like a lot of duplicate code.  If you wanted to change that code, you'd have to update every leave event?
# Create a Textbox variable to make the code below generic so that it can be pasted into any Textbox Leave event
0 Kudos
LeoDonahue
Occasional Contributor III
You could modify this sample to use the code you have in your original post. Then you would have a method that validates your text input, rather than relying on keypress and leave events to restrict data that is entered into the text field. 

Unless your users know in advance, how would they know your text box is expecting a integer vs a double?  I think it would be somewhat confusing for a user to enter something, and have your code change the value to something else without any feedback to the user as to why.

    private void submit(){
        if(isReady()){
            JOptionPane.showMessageDialog(this, "Form Submitted");
        }
    }
    
    private boolean isReady(){
        boolean ready1 = false;
        boolean ready2 = false;
        boolean ready3 = false;
        
        if(isDouble(textField.getText())){
            ready1 = true;
            label.setText("");
        } else {
            label.setText("invalid entry");
        }
        
        if(isDouble(textField_1.getText())){
            ready2 = true;
            label_1.setText("");
        } else {
            label_1.setText("invalid entry");
        }
        
        if(isDouble(textField_2.getText())){
            ready3 = true;
            label_2.setText("");
        } else {
            label_2.setText("invalid entry");
        }
        return (ready1 && ready2 && ready3);
    }
    
    private boolean isDouble(String x){
        try{
            Double.valueOf(x);
            return true;
        } catch (NumberFormatException e){
            return false;
        }
    }
0 Kudos
RichardFairhurst
MVP Honored Contributor
Leo:


You and I fundamentally differ on our approaches.  Neither is wrong, but they do not have the same results.  Your method delays everything to the submit button.  That is unacceptable to me.  All validation must take place as soon as the user commits any particular piece of information into the form.  A final check is acceptable as a failsafe, but not as a replacement for up front checking.  I am willing to accept the difficulties such an approach results in, since I am unwilling to wait until a user submits the form to warn them of data input errors.  Unless you accept that is where I am coming from and deal with validation at the soonest possible moment in the code you suggest, we will have to agree to disagree on this subject.

Your recommendations are noted, and others reading this thread are free to consider your arguments for best practices and follow your advice.  But nothing you have said has had any effect on this aspect of what I want.  This for me is a non-negotiable program requirement.  If a client of yours told you that they won't pay you for an interface that delays validation to a final button press, and no urgings on your part about how acheiving this behavior will complicate the code that you will have to write and maintain will change their mind, would you refuse to comply and force them to hire someone else to get what they want?

However, you have convinced me that I do not need to paste the same complete generic code into each Leave event.  And the fact that I have made the code generic to any textbox suggest a different approach.  However, centralizing this kind of validation to each form's Submit button is not a sufficient approach either.  There seems to me to be little doubt at this point that this subject warrants a more comprehensive approach than has been suggested so far.

The need for this kind of validation is universal to all forms, and Microsoft clearly has provided no real help to users to easily extend their forms with decent Textbox input validation functionality.  So it seems one or more validation helper classes should be written for all such validations that takes an approach that meets and centralizes well designed code for a variety of user validation needs.  I have done this before whenever I realize I am repeated using the same code over and over and clearly this is going to happen with form Textbox validations.

No matter what form a person is designing, or when the form needs to handle validation, Long and Double values (and for that matter any other variable type managed through a textbox) all have similar validation property options in a program, such as whether to be validated against a minimum or maximum value range, handling translations of input and output formats, invalid input messaging, default value suggestions or replacements, valid character filtering/masking, association with a specific unit of measure and conversions to alternative units of measure (including hex and escape codes if desired), precision (obvious for Doubles, but even Longs can be rounded to the nearest 10, 100, or 1,000 for many applications, and dates may at times be resticted to the first day or last day of a month or first or last date of the calendar or fiscal year), etc.  Additionally, once one of these options is set, it tends to be applicable and have implications across all of the other functions.  I find that a helper Class is the best way to deal with this level of interrelationships and collect, organize, extend and maintain the best validation code that is repeatedly used across programs to handle the mechanics of such operations and options.  So I think that will be the direction I will be taking next.  (I love a discussion or thought process that assures me that a universal approach to a problem is warrented, since those are the problems really worth solving and that boost my productivity the most and leave me the most satisfied when I finish).
0 Kudos
LeoDonahue
Occasional Contributor III
Richard:

I agree that we disagree.

Our methods do produce the same results, the result being that the input is validated.  You probably meant to say "Neither is wrong, but they do not have the same effects".  My effect being to let submit handle validation, your effect being to let the control handle validation.

My replies are merely suggestions.  You can use them or ignore them.

I am posting this in case it helps others and also to see if anyone has a more elegant way of doing this.


Perhaps you need to deal with the fact that if you were posting this thread to see if anyone has a more elegant way of handling user input validation, it might be worthwhile to be polite to those who take the time to respond.
0 Kudos
RichardFairhurst
MVP Honored Contributor
Unless your users know in advance, how would they know your text box is expecting a integer vs a double?


Try my code please before you keep giving invalid comments.  If you tried it you would see that a user cannot type a decimal character into the field that handles Long values, but they can type one decimal character (and only one decimal character) into the other.  Pasting is the only way the user could avoid observing this restriction.  Try typing any non-number and you can't, so obviously it is not accepting those inputs.  Pasting is the only way to avoid observing that restriction.  Pasting will be at the users peril.

If I knew how to capture the paste event, validate the input for compliance, and prevent any invalid input from ever appearing in the textbox I would take that approach in a heartbeat.  I would rather warn them why they cannot put in invalid input in the first place than warn them that they succeeded in screwing up the data input to the form at the last minute.  This would be the form of elegance I would desire and materially different results and effects.

I will be doing warnings as I have indicated in later posts and telling the user what they are screwing up, but much earlier than you are.
0 Kudos
LeoDonahue
Occasional Contributor III
I will suggest one last helpful link, since you have no luck with Searching MSDN:  http://msdn.microsoft.com/en-us/library/ms172106%28v=vs.80%29.aspx
0 Kudos
RichardFairhurst
MVP Honored Contributor
Perhaps you need to deal with the fact that if you were posting this thread to see if anyone has a more elegant way of handling user input validation, it might be worthwhile to be polite to those who take the time to respond.


I do not feel I have been impolite, but I have been persistently maintaining my position of what coding objectives I have just as you have.  I grant your approach is more elegant if I was willing to delay validation to the final check.  You just have not convinced me that your approach is a complete solution that supplants or invalidates my approach.  However, I do actually plan on using most of your suggestions in addition to my approach (by adding validation warning messages, maximum value validation, a final failsafe check, perhaps providing correction options to the user to return to the textbox to make the correction or accept a range of default value suggestions, rather than simply imposing a default value, while still ensuring that if they leave the textbox it will contain a valid value, etc).

The discussion has been fruitful and I consider your inputs to be worthwhile.  Many of the challenges you have posed to my thinking will result in changes to the original code I posted.  You have posted coding options I have taken the time to try for myself, which I feel shows I was not wasting your time.  So in any event, I appreciate that you have responded and I have learned a great deal from experimenting with the suggestions you have made.
0 Kudos
RichardFairhurst
MVP Honored Contributor
For those interested in preventing a user from inserting any invalid characters into a textbox, I have demonstrated and others have demonstrated that there is a way to prevent that from occurring through the keyboard.  However, so far no one has prevented invalid characters from being pasted into the textbox.

The code below is my first attempt at preventing any invalid characters from being entered by both typing or pasting.  The code below also restricts the total number of characters in the textbox to a set number of characters during both keyboard entry and text paste events into the Textbox.

For this example my Textbox will hold a date made up of decimal digits and a date separator.  The code removes any character that was typed or pasted that was not a number or a date separator.  Also, no more than 10 characters can be typed or pasted into the textbox and all attempts to enter characters that would exceed that maximum never appear in the Textbox.  The code works through the TextChanged event, which is triggered by any typing, pasting or programatic change to the Textbox's text value. 

Here is the commented code:

' These are the minimum reference imports required for this code.
Imports System.Windows.Forms
Imports System.Globalization
Imports System.Text.RegularExpressions

  ' At this time the Code is within a Form class
  Private Sub txtToDate_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtToDate.TextChanged
    If TypeOf sender Is TextBox Then
      ' Just a failsafe to be sure that only a textbox is the sender object
      Dim tbx As TextBox = sender
      ' call the Date Character validation Sub
      TextBoxDateText(tbx)
    End If
  End Sub

  ' The code that follows could be moved out of the Form class into a helper class dll.
  Private Sub TextBoxDateText(ByVal tbx As TextBox)
    If Len(tbx.Text) > 0 Then
      ' Only testing for valid characters when at least 1 character has been entered into the Textbox.

      ' Variables for the cursor position in the textbox, the character length of the text, and the actual text value
      Dim tbxPos As Long = tbx.SelectionStart
      Dim tbxLen As Long = Len(tbx.Text)
      Dim tbxText As String = tbx.Text

      ' Get the user's local Date Separator character(s).  Escapes need to be added in front of . or \ characters,
      ' but that is not needed in the U.S.
      Dim dateSep As String = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.DateSeparator

      ' Create a regular expression string of characters I want to allow.  In this case it is the Date Separator character
      ' and any decimal digit (\d is a special group that matches decimal digits for the user's local culture.
      Dim validChars As String = dateSep & "\d"

      ' Test the text in the Textbox for any character that is not part of the validChar string.  See Function below.
      If Not IsValidChars(tbxText, validChars) Then

        ' One or more invalid characters was found so get the text with only valid characters.  See Function below.
        tbxText = KeepValidChars(tbxText, validChars)

        ' Set where the cursor would go and the new text character length after stripping out invalid characters
        tbxPos = tbxPos - (tbxLen - Len(tbxText))
        tbxLen = Len(tbxText)
      End If

      ' Test if the maximum character count is exceed.  Here it is hard coded, but in future posts 
      ' I will change it to a parameter variable.
      If tbxLen > 10 Then
        ' The text is still longer than the maximum character count so remove excess characters to the left of the cursor

        ' This code assumes the Textbox flag is set for left to right entry.  Change to the right of the cursor if you use the opposite.
        ' Explanation of Substring expression below:  Start at beginning of string.  From the cursor position going left remove 
        ' any characters that would exceed the maximum allowed.  Concatenate whatever remains with 
        ' everything to the right of the cursor.
        tbxText = tbxText.Substring(0, tbxPos - (tbxLen - 10)) & tbxText.Substring(tbxPos, tbxLen - tbxPos)

        ' Set the new cursor position and the new text character length that applies after characters maximum is respected
        tbxPos = tbxPos - (tbxLen - 10)
        tbxLen = Len(tbxText)
      End If

      ' Test if any of the above operations would change the text of the Textbox.
      If tbx.Text <> tbxText Then

        ' Text changed so replace what is in the Textbox with the character validated text
        ' and set the new cursor position to be immediately to the right of the last retained inserted character.
        tbx.Text = tbxText
        tbx.SelectionStart = tbxPos
      End If

      ' end of operations on Textbox text with one or more characters entered.
    End If
  End Sub

  Private Shared Function IsValidChars(ByVal txtDate As String, ByVal validChars As String) As Boolean

    ' Declare a regular expression which is the negation in any position of the provided valid characters
    Dim objNotDatePattern As New Regex("[^" & validChars & "]+")

    ' A match occurs if invalid characters are present, but return the match's negation to 
    ' report true when only valid characters are present.
    Return (Not objNotDatePattern.IsMatch(txtDate))
  End Function

  Function KeepValidChars(ByVal input As String, ByVal validChars As String) As String

    ' Making a change that could trigger an error so place in try block.
    Try

      ' Replace any invalid character with no character, so that only valid characters in the validChars list will remain.
      Return Regex.Replace(input, _
             "[^" & validChars & "]", _
             "", RegexOptions.None)
    Catch e As Exception
      Return input
    End Try
  End Function


The code does not behave completely like disabling a keystroke through the KeyPress or KeyDown events.  This is because when a user enters invalid text (only through pasting if the KeyPress methods discussed previously have been applied) if they had selected any text that text will be overwritten, even if the new overwriting text was all invalid.  Nonetheless, with this code there is no way for a user to ever enter any character into the textbox that is invalid and have those invalid characters appear in the textbox.

While in many ways this approach is similar to the behavior of a MaskedTextBox, it has some differences.  Unlike a MaskedTextBox there are no predefined positions that would require a user to fill in a complete 10 digit date to fill in a mask with all positions required.  Also there would be no strange looking mask holes showing up like "_1/_2/__12" when the MaskedTextBox uses a mask with options that allow short month, day and year values to be recognized as valid.  However, like a MaskedTextBox, additional tests still must occur to validate that the entry is an actual date (I would handle those through the Leave event and in a failsafe Submit event).

The disadvantage relative to the MaskedTextBox at this point is that the user must currrently type the separator, where they do not have to do that with the MaskedTextBox.  I plan to enhance the behavior of the TextChanged event with automatic separator placement as the user types whenever a user has clearly entered a full month value or day value (for the moment assuming a U.S M/d/yy format, but I will try to globalize it according to local cultural configurations if I can).  The logic required to do that obviously will be tricky, but I think it can be done.

Note that already many of the functions can be used directly to validate other types of Textbox values.  The IsValidChars and KeepValidChars work just as well for Long and Double values as they do for Date values.  They just need to have the correct regular expression character set string passed to them to prevent invalid characters from appearing in those textboxes, even during a paste event.  All of the logic in the TextBoxDateText Sub also applies, with the only modifications being to use a different validChars string.  There would also need to be an additional check in the case of Double fields to deal maximum characters on each side of a globalized decimal separator.  However, Dates have a variation of that type of validation involving the Date Separator, and Longs and Doubles may deal with Thousands separators.  Perhaps as I proceed a generic validation function will come to me that would apply to all such maximum character set parsings on either side of a separator as a user types.

Anyway, I hope this helps.
0 Kudos