Data Validation


Even though you may have a nice modal or modeless form, it doesn't mean you can trust your users. I don't mean you can't trust them to pay (a separate issue that I won't go into here); I mean you can't trust the data they enter. They may not give you all the data 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 ensure that they provide data in the correct format. However, free-form data entry controls such as text boxes sometimes require you to validate them, because users can provide any sort of data. For that, you handle a control's Validating event:

void applicantNameTextBox_Validating(object sender, CancelEventArgs e) {   // Check that applicant name exists   if( ((Control)sender).Text.Trim().Length == 0 ) {     MessageBox.Show("Please enter a name", "Error");     e.Cancel = true;   } }


The Validating event is fired when the focus is moved from one control on the form whose CausesValidation property is set to true to another control whose CausesValidation property is set to truein this case, from the Applicant Name text box control to the OK button. The Validating event lets the handler 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, the Validating event handler notifies the user of the transgression, cancels the event, and retains focus on the invalid text box.

If the Validating event is not canceled, the form is notified via the Validated event:

void applicantNameTextBox_Validated(object sender, EventArgs e) {   MessageBox.Show(     "Nice name, " + this.applicantNameTextBox.Text, "Thanks!"); }


Each control has CausesValidation set to true by default. To allow the user to click the Cancel button to close the form without entering valid data, you must set the CausesValidation property to false for your Cancel or Close button. Doing so from the Properties window generates the following code:

// LoanApplicationDialog.Designer.cs partial class LoanApplicationDialog {   ...   void InitializeComponent() {     ...     this.cancelButton.CausesValidation = false;     ...   } }


Regular Expressions and Validation

One handy tool for data validation that's not specific to Windows Forms but is provided by .NET is the regular expression interpreter encapsulated as the Regex class in the System.Text.RegularExpressions namespace. A regular expression is a general-purpose way to describe the format of string data so that, among other things, you can check a string to make sure that it fits a required format.

Although the regular expression language is beyond the scope of this book, let's take a look at a small example.[7]. A string to check that the format of a phone number fits the Australian format, (02) 9999-1234, including area code, parentheses, spaces, and hyphens, would look like this:

[7] For an overview of regular expressions in .NET, read "Regular Expressions in .NET," by Michael Weinhardt and Chris Sells, Windows Developer, November 2002, http://www.wd-mag.com/documents/s=7547/ win0212d/ (http://tinysells.com/10)

^\(\d{2}\) \d{4}-\d{4}$


This regular expression breaks down as follows:

  • The leading "^" means to start checking the string from the beginning. Without this, any leading characters that don't match the regular expression are ignored, something that could lead to improperly formatted phone numbers.

  • The "\(" means to match against a literal "(" character. The "\" prefix escapes the "(," which otherwise would be treated specially by the Regex class.

  • The "\d{2}" means to match two digits.

  • The "\)" means to match a ")" character followed by a space character.

  • The "\d{4}-\d{4}" means to match four more digits, followed by a "-" character, followed by four more digits.

  • The trailing "$" means to match the string all the way to the end so that no other characters can come after the phone number.

This regular expression can be used in a Validating event handler to check for an Australian phone number:

Regex rgxOzPhone = new Regex(@"^\(\d{2}\) \d{4}-\d{4}$"); ... void applicantPhoneNoTextBox_Validating(   object sender, CancelEventArgs e) {   // Check that a valid Australian application phone exists   if( !rgxOzPhone.IsMatch(((Control)sender).Text) ) {      MessageBox.Show(        "Please enter an Australian phone number: (xx) xxxx-xxxx",        "Error");      e.Cancel = true;   } }


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 returns 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.[8]

[8] An implementation of ASP.NET-like validation controls for Windows Forms is available from Genghis at http://www.genghisgroup.com (http://tinysells.com/8).

Masked Text Entry and Validation

One downside of using regular expressions is that the required data format is not visually apparent to the user because the control itself is empty. This is where a MaskedTextBox comes into its own. MaskedTextBox allows you to specify a visual mask that helps users understand the type and range of the required data. A mask is composed of a sequence of mask characters, each of which specifies an expected data type and range and shows how it should be displayed in the text box as a placeholder character. Masks are stored in the MaskedTextBox control's Mask property and, for the Australian phone number example, would look like this:

(00) 0000-0000


The MaskedTextBox uses "0" to specify a required number, and "(" ")" and "-" are treated as string literal placeholders and turn out to be useful visual guides. Wherever a mask character accepts data, a prompt character (specified by the PromptChar property and defaulting to "_") is displayed when MaskedTextBox is active, as shown in Figure 3.6.

Figure 3.6. An Active MaskedTextBox Control with Prompt Characters


By default, the prompts disappear when MaskedTextBox loses focus, although you can set the HidePromptOnLeave property to true to retain them in this situation.

MaskedTextBox has many useful features and a rich set of mask characters to choose from.

Using MaskedTextBox controls gives you two advantages. First, it gives users visual cues to assist with data entry. Second, many data formats are simple, and using mask characters is likely to be simpler and more self-explanatory than using regular expressions. On the other hand, the richness of regular expressions supports more complex data-formatting requirements, particularly when data can be entered in multiple formats. In these cases, regular expressions may be more maintainable than the equivalent code, and it's worth the investment to learn how to use them.

Data Format Notification

If users enter invalid data, they need to be notified in some fashion. As much as I lean on the message box in my test development, I prefer not to use it for actual applications; if a form has several controls that are invalid, it quickly becomes difficult for the user to remember which controls are invalid in what way, because message boxes don't stick around. This problem is exacerbated by increasing levels of data-formatting complexity.

One alternative is to use a status strip, but status strips tend to be ignored because they're at the bottom of the screen, far away from what the user is looking at.[9] A better way is to use the ErrorProvider component, which provides a visual indicator from which a tool tip can be displayed, as shown in Figure 3.7.

[9] According to legend, Microsoft did a usability study awarding people $50 if they would look under their chairs, putting the notification for this award in the status strip. The $50 went unclaimed during the testing.

Figure 3.7. Sample Use of the ErrorProvider Component


When users attempt 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 tool tip available. To implement this behavior, you drag an ErrorProvider component onto the form and handle the Validating event for the desired control:

void applicantNameTextBox_Validating(object sender, CancelEventArgs e) {   // Check that applicant name exists   string error = null;   if( ((Control)sender).Text.Trim().Length == 0 ) {     error = "Please enter a name";     e.Cancel = true;   }   this.errorProvider.SetError((Control)sender, error); }


Notice the call to ErrorProvider.SetError. The first argument is the control that the error is associated with, which we get from the Validating event's sender argument. The second argument is the error string, which turns into the tool tip. If the control's data is valid, you hide the error indicator by setting the error string to null.

Combined Validation

Validation is not always on a per-control basis. In some cases, several controls capture a set of data that needs to be validated in its entirety, rather than each individual control. For example, consider the update to the Loan Application dialog shown in Figure 3.8. Now it accepts a repayment percentage split across two payments, where the combined percentage must, of course, add up to 100.

Figure 3.8. Combined Controls Requiring Single Validation


In this example, it doesn't make sense to validate each numeric up/down control, because it's the combined value that needs to be validated. It's much nicer to group those controls within a container control, such as a group box, and validate collectively:

void applicantLoanAmountRepaymentGroupBox_Validating(   object sender, CancelEventArgs e) {   // Check that 1st and 2nd percentages sum up to 100   string error = null;   if( (this.firstNumericUpDown.Value +        this.secondNumericUpDown.Value != 100) ) {      error = "First and second repayments must add up to 100%";      e.Cancel = true;   }   this.errorProvider.SetError((Control)sender, error); }


Here, we handle the group box's Validating event to validate the combined values of the contained controls. If the data is invalid, we pass the group box to the ErrorProvider.SetError invocation to display the error provider icon for the group box as a whole, rather than any individual controls, as shown in Figure 3.9.

Figure 3.9. Combined Controls Being Validated as One


As with validating a single control at a time, we hide the error by setting the error string passed to ErrorProvider.SetError to null.

Thorough Validation

As useful as the Validating event is, especially when combined with ErrorProvider, there is one validation issue you must deal with separately: If a control has invalid data but never receives focus, it is never validated, because the Validating event is triggered only when focus is moved from one control to another. For example, the form in Figure 3.9 has three text boxes and two numeric up/down controls. Even if you were to handle the Validating event for all five controls, the user could still enter valid data into the first one (assuming it gets focus first) and press OK, causing the form to close and return DialogResult.OK. The problem is that the other two text boxes never get focus, never receive the Validating event, and, therefore, never get a chance to cancel the acceptance of the form.

One solution is to make sure that all controls start with valid data, although there are lots of cases when you can't provide reasonable and valid initial data. What's a good default for a phone number? Or an e-mail address? For these cases, you need to manually write the code to validate the form via the OK button's Click event handler:

void okButton_Click(object sender, EventArgs e) {   if( !this.Validate() ) {     this.DialogResult = DialogResult.None;   } }


The Validate method causes the form to enumerate the controls on the form. For each control found, the Validating event is fired, thereby executing your validation logic. Validate is also implemented by container controls such as SplitContainer, ToolStripContainer, and UserControl. If you need to validate subsets of controls on a form, such as those in container controls like group boxes, you can call ValidateChildren, which accepts a ValidationConstraints enumeration value that specifies the control subset of interest:

namespace System.Windows.Forms {   enum ValidationConstraints {     None = 0, // No child controls     Selectable = 1, // All selectable controls     Enabled = 2, // All enabled controls on the form     Visible = 4, // All visible controls     TabStop = 8, // All tabbable controls     ImmediateChildren = 16, // Controls whose parent is this form   } }


For example, validating the selectable controls on a form would look like this:

void okButton_Click(object sender, EventArgs e) {   if( !this.ValidateChildren(ValidationConstraints.Selectable) ) {     this.DialogResult = DialogResult.None;   } }


As a shortcut for validating selectable child controls, you can invoke ValidateChildren without passing a ValidationConstraints value:

void okButton_Click(object sender, EventArgs e) {   if( !this.ValidateChildren() ) {     this.DialogResult = DialogResult.None;   } }


You can even use more than one ValidationConstraints value if necessary. For example, the following code validates only immediate child controls of the validating container that can be tabbed to:

void okButton_Click(object sender, EventArgs e) {   if( !this.ValidateChildren(ValidationConstraints.ImmediateChildren |     ValidationConstraints.TabStop) ) {     this.DialogResult = DialogResult.None;   } }


Although Validate validates all controls on a form by default, it may never be called because of an interesting validation quirk: If the control that has focus is invalid, focus is prevented from leaving that control and moving to any other control with CausesValidation set to true, including other data entry controls and the OK button. This also means that the Esc key can't be pressed to close the form (even though you can press the form's close button or select Close from its system menu). Retaining focus on a control is inconsistent with general Windows user experience practice, which is to typically allow unfettered navigation across a form's landscape. Consequently, retaining focus in this way can potentially confuse your users. Presumably, this behavior is the default in order to be consistent with previous versions of Windows Forms.

You can avoid this situation by preventing both Validating and Validated events from firing until either the Validate or the ValidateChildren method is called. Use the form's AutoValidate property:

// LoanApplicationDialog.Designer.cs using System.Windows.Forms; ... partial class LoanApplicationDialog {   ...   void InitializeComponent() {     ...     // Allow focus to shift away from invalid controls     this.AutoValidate = AutoValidate.EnableAllowFocusChange;     ...   } }


You can set AutoValidate to one of four AutoValidate enumeration values:

namespace System.Windows.Forms {   enum AutoValidate {     Inherit = -1 // Use parent control's AutoValidate value     Disable = 0, // Don't validate any control     EnablePreventFocusChange = 1, // Keep focus if invalid (default)     EnableAllowFocusChange = 2, // Don't keep focus if invalid   } }


Because controls have their CausesValidation property set to true by default, their Validating and Validated events fire automatically. To prevent this behavior for all controls on a form or a user control, you set AutoValidate to AutoValidate.Disable. This is easier than setting each and every control's CausesValidation property to false, and it easily allows you to turn validation back on by setting AutoValidate to EnableAllowFocusChange, EnablePreventFocusChange, or Inherit. Inherit is for controls that implement AutoValidate and wish to use the same AutoValidate behavior of their containers.

With AutoValidate set to EnableAllowFocusChange and with Validate being called when the OK button is pressed, you can validate an entire form in one fell swoop, resulting in the dialog shown in Figure 3.10.

Figure 3.10. Single-Click Formwide Validation Using Validate and AutoValidate.EnableAllowFocusChange


In summary, you should consider several principles when including form validation: First, always include validation. Even a little can go a long way to helping users understand what is required for individual fields and entire forms. Second, to avoid an unpleasant user experience, don't retain focus in any control. Third, implement dialog-wide validation via the OK button's Click event handler; client code on the form that created the dialog processes your data, and the more accurate it is, the less likely it is that an exception will occur or bad data will be allowed through. Finally, because validation is usually specific to controls and container controls, you should deploy your validation logic to their Validating event handlers, especially because formwide validation ensures that this logic is called.[10] These considerations are summarized in the following code:

[10] User controls, discussed in Chapter 10, can simplify the deployment of multiple controls and validations into a single, reusable control, thereby saving you a lot of trouble.

// LoanApplicationDialog.cs partial class LoanApplicationDialog : Form {   public LoanApplicationDialog() {     InitializeComponent();     // Configure AcceptButton and CancelButton     this.AcceptButton = this.okButton;     this.CancelButton = this.cancelButton;     this.okButton.DialogResult = DialogResult.OK;     this.cancelButton.DialogResult = DialogResult.Cancel;     // Allow focus to shift away from invalid controls     this.AutoValidate = AutoValidate.EnableAllowFocusChange;   }   ...   void applicantNameTextBox_Validating(     object sender, CancelEventArgs e) {     // Check that applicant name exists     string error = null;     if( ((Control)sender).Text.Trim().Length == 0 ) {        error = "Please enter a name";       e.Cancel = true;     }     this.errorProvider.SetError((Control)sender, error);   }   // Other Validating event handlers   ...   void okButton_Click(object sender, EventArgs e) {     if( !this.Validate() ) {       this.DialogResult = DialogResult.None;     }   } }


The built-in Windows Forms validation infrastructure provides a fine framework for applying these principles.




Windows Forms 2.0 Programming
Windows Forms 2.0 Programming (Microsoft .NET Development Series)
ISBN: 0321267966
EAN: 2147483647
Year: 2006
Pages: 216

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net