You should never trust your users. We don't mean you can't trust them to pay (a separate issue that we won't go into here). We mean you can't trust the data that your users enter. They may not give you all the data that you need, or they may not give you data in the correct format. List boxes, radio buttons , and all the other controls that give users choices do so to make sure that they provide data in the correct format. However, sometimes you need to validate free-form data entry, such as what a user types into a text box. For that, you handle a control's Validating event: Sub applicantNameTextBox_Validating(object As Sender, _ e As CancelEventArgs) _ Handles applicantNameTextBox.Validating ' Check for existence of application name If applicantNameTextBox.Text.Length = 0 Then MessageBox.Show("Please enter a name", "Error") e.Cancel = True End If End Sub The Validating event is called when the focus is moved from one control on the form that has the CausesValidation property set to true to another control that has the CausesValidation property set to true ”for example, from a TextBox control to the OK button. The Validating event gives the handler the chance to cancel the moving of the focus by setting the CancelEventArgs.Cancel property to true. In this example, if the user doesn't enter a name into the text box, then the Validating event handler notifies the user of the transgression and cancels the event, keeping the focus on the text box containing invalid data. If the Validating event is not canceled , the form will be notified via the Validated event: Sub applicantNameTextBox_Validated(sender As Object, e As EventArgs) _ Handles applicantNameTextBox.Validated MessageBox.Show("Nice name, " + applicantNameTextBox.Text, "Thanks!") End Sub Each control has CausesValidation set to true by default. To allow the user to cancel the form without entering valid data, you must set the CausesValidation property to false for your Cancel or Close button: Sub InitializeComponent() ... Me.cancelButton.CausesValidation = False ... End Sub Regular Expressions and ValidationOne handy tool for data validation that's not specific to WinForms but is provided by .NET is the Regex class from the System.Text.RegularExpressions namespace. The Regex class provides a regular expression interpreter. A regular expression is a general-purpose way to describe the format of string data so that, among other things, a string can be checked to make sure that it fits a required format. The regular expression language is beyond the scope of this book, [5] but let's look at an example. A string to check that the format of a phone number fits the U.S. format, including area code, parentheses, spaces, and dashes, would look like the following:
^\(\d{3}\) \d{3}-\d{4}$ This regular expression breaks down as follows :
This regular expression can be used in a Validating event handler to check for a U.S. phone number: Sub applicantPhoneNoTextBox_Validating(sender As Object, _ e As CancelEventArgs) Dim re As Regex = New Regex ("^\(\d{3}\) \d{3}-\d{4}$") If Not(re.IsMatch(applicantPhoneNoTextBox.Text) Then MessageBox.Show( _ "Please enter a US phone number: (xxx) xxx-xxxx", _ "Error") e.Cancel = True End If End Sub If the string entered into the phone number text box does not match the regular expression in its entirety, the IsMatch method of the Regex class will return false, letting the handler indicate to the user that the data is not in the correct format. Taken together, regular expressions and validation provide a powerful tool to check a wide range of input strings provided by the user. Data Format NotificationAs much as we lean on the message box in our test development, we prefer not to use it for actual applications. For one thing, users are likely to forget the acceptable format for a phone number after the message box goes away. One alternative is to use a status bar, but status bars tend to be ignored because they're at the bottom of the screen, far away from what the user is looking at. [6] A better way is to use the ErrorProvider component, which shows the user that there's a problem and provides a tooltip with extra information, as shown in Figure 3.5.
Figure 3.5. Sample Use of the ErrorProvider Component
When the user attempts to change focus from the empty text box, we use an instance of the ErrorProvider component to set an error associated with that text box, causing the icon to be displayed to the right of the text box and making the tooltip available for more information. To implement this behavior, you drag an ErrorProvider component onto the form and handle the Validating event: Sub applicantNameTextBox_Validating(sender As Object, _ e As CancelEventArgs) Handles applicantNameTextBox.Validating Dim sError As String = "" If applicantNameTextBox.Text.Length = 0 Then sError = "Please enter a name" e.Cancel = True End If ErrorProvider1.SetError(CType(sender, Control), sError) End Sub Notice the call to ErrorProvider.SetError. This call passes the control the error is associated with (which we get from the sender argument to the Validating event), along with the error string, which will be used as the tooltip. If there is no problem, the error will be empty, and that will cause the error indicator on the dialog to go away. Thorough ValidationAs useful as the Validating event is, especially when combined with the ErrorProvider component, there is one validation issue you'll have to deal with separately. Because the Validating event is triggered only when focus is moved from one control to another, if a control has invalid data but never receives focus, it will never be validated. For example, the form in Figure 3.5 has three text boxes. Even if you were to handle the Validating event for all three text boxes, the user could still enter valid data into the first one ( assuming it gets focus first) and press the OK button, causing the form to close and return DialogResult.OK. The problem is that the other two text boxes will never get focus, will never receive the Validating event, and therefore will never get a chance to cancel the acceptance of the form. One way to deal with this problem is to make sure that all controls start with valid data, requiring the user to set focus to invalidate the data and triggering the validation events when focus is lost. However, there are lots of cases when you can't fill in valid initial data. What's a good default for a phone number? Or e-mail address? Or mother's maiden name? For these cases, you'll need to write the code manually to validate the form in the OK button's Click event handler: Sub okButton_Click(sender As Object, e As EventArgs) _ Handles okButton.Click ' Validate each control manually Dim control As Control For Each control In Me.Controls control.Focus() If Not Me.Validate() Then Me.DialogResult = DialogResult.None Exit Sub End If Next End Sub This code walks the list of controls in the form's control collection, setting each one in turn to have the focus. By itself, this will not trigger the Validating event, unfortunately , so we also must trigger that event by calling the form's Validate method. The Form.Validate method does not validate all the controls in the form. Instead, it validates only the control that just lost focus. If validation fails on any one control, we change the form's DialogResult property (preset to DialogResult.OK by the OK button's DialogResult property), and that stops the form from closing. If you've got controls that contain other controls, such as controls contained by group boxes, you must be a little more thorough about checking each control for child controls to make sure that they're validated, too: ' Retrieve all controls and all child controls etc. ' Make sure to send controls back at lowest depth first ' so that most child controls are checked for things before ' container controls, e.g., a TextBox is checked before a ' GroupBox control Function GetAllControls() As Control() Dim allControls As ArrayList = New ArrayList() Dim myqueue As Queue = New Queue() myqueue.Enqueue(Me.Controls) Dim mycontrols As ControlCollection Dim current As Object Do While myqueue.Count > 0 current = queue.Dequeue() If TypeOf current Is ControlCollection Then mycontrols = CType(current, ControlCollection) If mycontrols.Count > 0 Then Dim mycontrol As Control For Each mycontrol In mycontrols allControls.Add(mycontrol) queue.Enqueue(mycontrol.Controls) Next End If End If Loop End Function Sub okButton_Click(sender As Object, e As EventArgs) ' Me.DialogResult = DialogResult.OK ' Me.Close() ' Validate each control manually Dim mycontrol As Control For Each mycontrol In GetAllControls() ' Validate this control mycontrol.Focus() If Not(Me.Validate()) Then Me.DialogResult = DialogResult.None Exit For End If Next End Sub The GetAllControls method gets all of a form's controls, as well as all the controls' child controls, all those controls' children, and so on. This function is handy whenever you need to dig through the complete set of controls on a form, not just when you're validating. You'll see it used a few more times in this chapter. The OK or Apply button's Click event handler is also a good place to do any other kind of validation that can't be done using the validation events, such as checking multiple related fields at the same time. If for any reason you don't want the form to close, set the form's DialogResult to None, and it won't. |