Extending Windows Forms with a Custom Validation Component Library

 

Michael Weinhardt
www.mikedub.net

March 16, 2004

Summary: Data validation is a key step in ensuring clean data capture and subsequent processing and reporting. This installment explores the programmatic validation infrastructure native to Windows Forms and builds upon that foundation to develop a custom library of validation components designed to provide a more productive validation experience akin to using ASP.NET's validation controls. (24 printed pages)

Download the winforms03162004_sample.msi sample file.

Contents

Introduction
The Highlights of Windows Forms Validation
Programmatic Validation vs. Declarative Validation
Establishing Design-Time Support
Imitation Is the Sincerest Form of Flattery
Introducing the Required Field Validator
BaseValidator: Divide and Conquer
In for a Penny, In for a Pound
The Completed Custom Validation Infrastructure
Where Are We?
An Alternative Custom Validation Solution
C# vs. Visual Basic .NET
Genghis
Acknowledgements
References

Introduction

Call me weird, but one of my favorite films is Amazon Women on the Moon, a 1950s B-Grade science fiction movie parody interspersed with several short comedy sketches in the same style as The Kentucky Fried Movie. One of the sketches, "Two IDs," features Karen and Jerry (played by Rosanna Arquette and Steve Guttenberg, respectively) on the night of their first date. The story begins with Jerry arriving at Karen's apartment. After a few moments of friendly banter, Karen rather unusually asks Jerry for two forms of identification—a major credit card and valid driver's license. Karen uses the identification to run a dating check on Jerry and it's this disturbing turn of events that forms the backbone of the comedy. Unfortunately for Jerry, his background check fails and Karen declines the date. More than just being amusing, this story is a modern-day fable from which Windows Forms developers can learn a clear lesson—always validate data to avoid dodgy situations on par with dating Steve Guttenberg. Conversely, successfully validated data provides an equivalent experience to dating Rosanna Arquette, which for this author would be quite nice.

The Highlights of Windows Forms Validation

Simply stated, validation is the process of ensuring that data is complete and accurate before subsequent processing or storage. While validation can be implemented in both data and business-rule application tiers, it should be included in the presentation tier that forms the front-line validation defense. UIs typically provide the scope for developers to construct more human, responsive and informative validation experiences for end users, while avoiding issues like unnecessary network round-trips across n-tier applications. Validation can include type, range, and business-rule checks and the form in Figure 1 could benefit from all of these types.

Figure 1. Add New Employee form requiring validation

This form needs to validate the following:

  • Name, Date of Birth and Phone Number are required
  • Date of Birth must be a valid date/time value
  • Phone Number must conform to Australian format: (xx) xxxx xxxx
  • A new employee must be at least 18 years old

An appropriate infrastructure is needed to implement this validation and Windows Forms unsurprisingly provides it, built directly into each control. A control's intention to support validation is indicated by setting its CausesValidation property to True, the default value for all controls. When a control's CausesValidation property is set to True, its Validating event is fired if focus shifts to another control that also has a CausesValidation value of True. Consequently, you handle Validating to implement your validation logic, such as ensuring Name is provided:

void txtName_Validating(object sender, CancelEventArgs e) {
  // Name is required
  if(txtName.Text.Trim() == "" ) {
    e.Cancel = true;
    return;
  } 
}

Validating supplies a CancelEventArgs parameter that lets you signal whether the field was valid by setting its Cancel property. If Cancel is True (invalid field), focus remains on the invalid control. If the default of False (valid field), the Validated event is fired and focus shifts to the new control. Figure 2 illustrates this process.

Figure 2. Windows Forms validation process

The onus is on you to visually inform the user of valid or invalid data, and instinct might suggest the status bar as an appropriate mechanism for doing so. Visually speaking, this technique has two problems:

  1. Only one error can be displayed on the status bar while a form may have several invalid controls.
  2. It may be difficult to ascertain exactly which control an error message refers to given the status bar is typically located at the bottom of the form.

The ErrorProvider component turns out to be a better option as a mechanism for error notification for one or more controls, using a combination of icon and tool tip to notify the user of an error and display an appropriate message in close proximity to the related control, as shown in Figure 3.

Figure 3. ErrorProvider in action

Enabling this component is as simple as dragging an ErrorProvider component onto a form and configuring its icon, blink rate, and blink style, after which the ErrorProvider can be incorporated into validation code:

void txtName_Validating(object sender, CancelEventArgs e) {
  // Name is required
  if(txtName.Text.Trim() == "" ) {
    errorProvider.SetError(txtName "Name is required");
    e.Cancel = true;
    return;
  }    
  // Name is Valid
  errorProvider.SetError(txtName, "");
}

CausesValidation, Validating, and the ErrorProvider provide the basic infrastructure for implementing per-control validation in a pattern that we can reuse for other controls, such as txtDateOfBirth and txtPhoneNumber:

void txtDateOfBirth_Validating(object sender, CancelEventArgs e) {    
  DateTime dob;
  // DOB is required and must be a valid date
  if( !DateTime.TryParse(txtDateOfBirth.Text, out dob) ) {
    errorProvider.SetError(txtDateOfBirth, "Must be a date/time value");
    e.Cancel = true;
    return;
  }
  // Must be 18 or older
  if( dob > DateTime.Now.AddYears(-18) ) {
    errorProvider.SetError(txtDateOfBirth, "Must be 18 or older");
    e.Cancel = true;
    return;
  }
  // DOB is valid
  errorProvider.SetError(txtDateOfBirth, "");
}  
void txtPhoneNumber_Validating(object sender, CancelEventArgs e) {      
  // Phone number is required and must be in Aussie format
  Regex re = new Regex(@"^\(\d{2}\) \d{4} \d{4}$");
  if( !re.IsMatch(txtPhoneNumber.Text) ) {
    errorProvider.SetError(txtPhoneNumber, "Must be (xx) xxxx xxxx");
    e.Cancel = true;
    return;
  }
  // Phone Number is valid
  errorProvider.SetError(txtPhoneNumber, ""); 
}

Form-Wide Validation

The combination of Validating event and ErrorProvider component provides a great solution that dynamically validates each control at the point of impact, that is, as users enter data. Unfortunately, the reliance on Validating events prevents this solution from automatically scaling up to support form-wide validation that's needed when users click the OK button to complete data entry. It is possible that one or more controls have not had the focus before OK is clicked and, consequently, have not fired their Validating events. Form-wide validation is implemented by manually calling the validation logic tied up in each Validating event, achieved by enumerating all controls on a form, setting focus to each, and calling the form's Validate method, like so:

void btnOK_Click(object sender, System.EventArgs e) {
  foreach( Control control in Controls ) {
    // Set focus on control
    control.Focus();
    // Validate causes the control's Validating event to be fired,
    // if CausesValidation is True
    if( !Validate() ) {
      DialogResult = DialogResult.None;
      return;
    }
  }
}

The Cancel button, however, does not need to implement form-wide validation because its job is to simply close the form. Alas, users may not be able to click Cancel if the control they're currently on is invalid because focus is retained since the Cancel button's CausesValidation is also set to True by default. This situation is easily avoided by setting the Cancel button's CausesValidation property to False, thereby preventing Validating from being fired on any controls that focus shifts from, shown in Figure 4.

Figure 4. Preventing validation

With approximately 60 lines of code, our Add New Employee form supports basic validation.

Programmatic Validation vs. Declarative Validation

From a productivity point of view, there is a fundamental problem with this solution, which is that non-trivial applications tend to have more than one form, often with more controls per form than our trivial sample, and consequently requiring more validation code. Writing more and more code in the face of increasing UI complexity is not a scalable technique and obviously best avoided. One solution could be to identify and package common validation logic into reusable types. This would help reduce client code commitment to creating and configuring validation object instances as needed. However, although a step in the right direction, this solution still requires programmatic commitment. But, this solution suggests a pattern common to Windows Forms UIs—types that do most of the work on behalf of a Windows Form, with a majority of the client code written to configure them, turn out to be ideal candidates for Windows Forms components or controls. Packaged this way, developer effort is converted to dragging a component or control from the Toolbox onto a form, configuring it from design-time features like the Property Browser, and letting the Windows Forms designer do the hard work of translating our design-time intentions into code that is persisted in InitializeComponent. Thus, a programmatic experience is transformed into a declarative experience where declarative is synonymous with productive. This turns out to be an ideal situation for Windows Forms validation.

Establishing Design-Time Support

The first step is to establish design-time support, which requires that your implementation derives from one of three types of design-time component: System.ComponentModel.Component, System.Windows.Forms.Control, or System.Windows.Forms.UserControl. Component is the right choice if your logic doesn't require a UI, or either Control or UserControl if it does. The difference between Control and UserControl is how the UI is rendered. The former is rendered from custom paint code written by you, while the latter is rendered from other controls and components. Our existing validation code doesn't paint itself because it hands that task over to an ErrorProvider. Consequently, Component turns out to be the most suitable choice for packaging our validation classes.

Imitation Is the Sincerest Form of Flattery

The next step is to work out what sort of validators we'll need. A good place to start is to see what validators ASP.NET implements. Why? I'm a big consistency fan and also have "don't reinvent the wheel" tattooed across my forehead (backwards, of course, so I can see it in the mirror when I brush my teeth). As such, I think the main goal should be to create Windows Forms validators that are consistent with their ASP.NET counterparts in terms of exposed types and members (where the solution makes sense in Windows Forms). More than technical homage to the ASP.NET team, this choice creates familiarity for developers across development paradigms. ASP.NET currently offers the validators shown in the table below.

Validation Control Description
RequiredFieldValidator Ensures a field contains a value
RegularExpressionValidator Uses a regular expression to validate a field's format
CompareValidator Performs an equality test on the targeted field and another field or value
RangeValidator Tests that a field is of a particular type and/or within a specified range
CustomValidator Creates a server-side event handler for custom validation logic more complex than the other validators can handle

I also think extensibility should be another goal so developers can incorporate their own custom validators into the infrastructure with minimal effort when the provided validators do not suffice. Finally, to save effort, the implementation should leverage the existing Windows Forms validation infrastructure we explored earlier.

Introducing the Required Field Validator

With these goals in mind, it's time to get into the nuts and bolts. Let's start off by building the simplest possible validator we can, the RequiredFieldValidator.

The Visual Studio .NET Solution

We'll need a Visual Studio® .NET solution in which to implement the RequiredFieldValidator. Whenever I build a component or control library, I prefer a "two project" approach that involves splitting the solution into a component or control project and a corresponding test harness project, by:

  1. Creating a solution (winforms03162004_CustomValidation)
  2. Adding a Windows Forms Application project to the solution (CustomValidationSample)
  3. Adding a Windows Forms Control project to the solution (CustomValidation)
  4. Removing the default UserControl1 from the CustomValidation project
  5. Adding a new class called RequiredFieldValidator and deriving it from System.ComponentModel.Component

Note I'm using Visual Studio .NET 2003 and the .NET Framework 1.1 as these validation components are not restricted to Whidbey.

The Interface

Being consistent entails implementing the same interface where it makes sense in Windows Forms. This means implementing the same members exposed by the ASP.NET RequiredFieldValidator:

class RequiredFieldValidator : Component {
  string ControlToValidate {get; set;}
  string ErrorMessage {get; set;}
  string InitialValue {get; set;}
  bool IsValid {get; set;}
  void Validate();
}

The following table describes each member in detail.

Member Description
ControlToValidate Specifies which control to validate.
ErrorMessage Message to display if ControlToValidate is invalid. Default is "".
InitialValue A mandatory value does not necessarily mean a value other than "". In some cases, a default control value might be used as a prompt eg using "[Choose a value]" in a ListBox control. In this case, the required value must be different than the initial value of "[Choose a value]". InitialValue supports this requirement. The default is "".
IsValid Reports whether ControlToValidate's data is valid or not after Validate is called. IsValid can be set from client code when you need to override Validate, as with the ASP.NET implementation. True by default.
Validate Validates ControlToValidate's value and stores the result in the IsValid.

In ASP.NET, ControlToValidate is a string used to index ViewState for server-side control instances. This indirection is needed to handle the request-based connectionless nature of a Web application. Since we don't need to make the same consideration in Windows Forms, we can refer directly to the control. Also, as we'll be using an ErrorProvider internally, we'll add an Icon property for developers to configure. With that in mind, we need to adjust the interface like so:

class RequiredFieldValidator : Component {
  ...
  Control ControlToValidate {get; set;}
  Icon Icon {get; set;}
  ...
}

The Implementation

The resulting implementation looks like this:

class RequiredFieldValidator : Component {
  // Private fields
  ...
  Control ControlToValidate 
    get { return _controlToValidate; }
    set {  
      _controlToValidate = value;
      // Hook up ControlToValidate's Validating event
      // at run-time ie not from VS.NET
      if( (_controlToValidate != null) && (!DesignMode) ) {
        _controlToValidate.Validating += 
        new CancelEventHandler(ControlToValidate_Validating);
      }
    }
  }
  string ErrorMessage {
    get { return _errorMessage; }
    set { _errorMessage = value; }
  }
  Icon Icon {
    get { return _icon; }
    set { _icon = value; }
  }
  string InitialValue {
    get { return _initialValue; }
    set { _initialValue = value; }
  }
  bool IsValid {
    get { return _isValid; }
    set { _isValid = value; }
  }  
  void Validate() {
    // Is valid if different than initial value,
    // which is not necessarily an empty string
    string controlValue = ControlToValidate.Text.Trim();
    string initialValue;
    if( _initialValue == null ) initialValue = "";
    else initialValue = _initialValue.Trim();
    _isValid = (controlValue != initialValue);
    // Display an error if ControlToValidate is invalid
    string errorMessage = "";
    if( !_isValid ) {
      errorMessage = _errorMessage;
      _errorProvider.Icon = _icon;
    }
    _errorProvider.SetError(_controlToValidate, errorMessage);
  }
  private void ControlToValidate_Validating(
    object sender, 
    CancelEventArgs e) {
    // We don't cancel if invalid since we don't want to force
    // the focus to remain on ControlToValidate if invalid
    Validate();
  }
}

The crux of this implementation is how it hooks into ControlToValidate's Validating event:

Control ControlToValidate {
  get {...}
  set {
    ...
    // Hook up ControlToValidate's Validating event
    // at run-time ie not at VS.NET design-time
    if( (_controlToValidate != null) && (!DesignMode) ) {
      _controlToValidate.Validating += 
        new CancelEventHandler(ControlToValidate_Validating);
  }
}

This allows required field validation to be dynamically executed as data is entered like standard Windows Forms implementation. We also gain an additional benefit that isn't so obvious. In the initial implementation, focus is retained in a control until it's valid, that is, until its Validating event's CancelEventArgs.Cancel property is set to False, essentially trapping the user in the control. Data entry UIs should not trap users anywhere and this idea is reflected in ControlToValidate_Validating, which handles the event as a trigger for its own validation rather than as a mechanism for integration with the underlying validation infrastructure. Figure 5 shows the updated process.

Figure 5. Updated validation processing

Design-Time Integration

The implementation's functional aspect is complete, leaving the design-time aspect. All good Visual Studio .NET design-time citizens use a variety of features to specify their design-time behavior. For example, the RequiredFieldValidator uses the ToolboxBitmapAttribute to specify a custom icon displayed in the Toolbox and the Component Tray, as well as CategoryAttribute and DescriptionAttribute to make the RequiredFieldValidator's public properties more descriptive in the Property Browser:

[ToolboxBitmap(
  typeof(RequiredFieldValidator), 
  "RequiredFieldValidator.ico")]
class RequiredFieldValidator : Component {
  ...
  [Category("Behaviour")]
  [Description("Gets or sets the control to validate.")]
  Control ControlToValidate {...}
  [Category("Appearance")]
  [Description("Gets or sets the text for the error message.")]
  string ErrorMessage {...}
  [Category("Appearance")]
  [Description("Gets or sets the Icon to display ErrorMessage.")]
  Icon Icon {...}
  [Category("Behaviour")]
  [Description("Gets or sets the default value to validate against.")]  
  string InitialValue  {...}
}

The complete implementation uses several other design-time features beyond the scope of this discussion, including:

  • Specifying which controls can be selected for ControlToValidate from the Property Browser. This implementation allows TextBox, ListBox, ComboBox and UserControl controls
  • Hiding IsValid from the Property Browser as it is a run-time-only property (all public properties are automatically displayed in the Property Browser at design-time).

If you'd like a more detailed exploration of the design-time, please take a look at the following articles:

Using the RequiredFieldValidator

To use the RequiredFieldValidator, we need to add it to the Toolbox before it can be dragged onto a form and configured. To do so, we:

  1. Rebuild the solution.
  2. Open the test form in Visual Studio .NET.
  3. Right-click on the Toolbox and select Add Tab.
  4. Create a new tab called Custom Validation and select it.
  5. Right-click on the Toolbox and select Add/Remove Items.
  6. Browse to the newly built component assembly (CustomValidation.dll) and select it.

These steps allow you to use the component in the Windows Forms designer, as shown in Figure 6.

Figure 6. RequiredFieldValidator with design-time attributes in action

I applied the RequiredFieldValidator to all fields on the Add New Employee form, with prompts for the type of values a user can enter in each field. Figure 7 shows how it all works at run time.

Figure 7. Fun and happiness at runtime

BaseValidator: Divide and Conquer

With RequiredFieldValidator in the bag, we can easily implement the remaining validators. Well, perhaps not that easily. The RequiredFieldValidator tightly couples specific "required" validation logic with the remaining majority of logic that each validator needs to implement such as specifying a control to validate. In situations like this, an appropriate solution is to gently pry RequiredFieldValidator apart into two new types, the BaseValidator and a much slimmer RequiredFieldValidator that derives from it. Strictly speaking from a consistency point of view, we should also create the IValidator interface and have BaseValidator implement in the same manner as ASP.NET. However, the degree of extensibility provided by this approach is more than we need right now. BaseValidator implements the reusable portion of the validation logic reminiscent of the original RequiredFieldValidator:

abstract class BaseValidator : Component, IValidator {
  Control ControlToValidate {...}
  string ErrorMessage {...}
  Icon Icon {...}
  bool IsValid {...}    
  void Validate() {...}
  private void ControlToValidate_Validating(...) {...}
}

While IsValid and Validate are common to all validators, they become facades in the new architecture, leaving specific validation up to each derived validator. Consequently, BaseValidator needs a mechanism by which it can "ask" a derived validator whether it is valid or not. This is achieved with an additional abstract method, EvaluateIsValid. Both EvaluateIsValid and BaseValidator are abstract to ensure that each derived validator knows they need to be able to answer this question. The new additions require a bit of refactoring, shown here:

abstract class BaseValidator : Component {
  ...
  void Validate() {
    ...
    _isValid = EvaluateIsValid();
    ...
  }
  protected abstract bool EvaluateIsValid();
}

The effect is that BaseValidator can only be used by derivation, and EvaluateIsValid must be implemented. The Validate method has been updated to call down to the descendent's EvaluateIsValid to interrogate its validity. This technique is also applied to the ASP.NET controls, and certainly adds the desired consistency.

RequiredFieldValidator: Redux

With the BaseValidator done, we now need to refactor RequiredFieldValidator to keep up its end of the bargain by deriving from BaseValidator and implementing the specified required field validation bits in both EvaluateIsValid and InitialValue. Proving that less is more, the new, slimmer RequiredFieldValidator looks like this:

[ToolboxBitmap(
  typeof(RequiredFieldValidator), 
  "RequiredFieldValidator.ico")]
class RequiredFieldValidator : BaseValidator {
  string InitialValue  {...}
  protected override bool EvaluateIsValid() {
    string controlValue = ControlToValidate.Text.Trim();
    string initialValue;
    if( _initialValue == null ) initialValue = "";
    else initialValue = _initialValue.Trim();
    return (controlValue != initialValue);
  }
}

In for a Penny, In for a Pound

The nice separation of generic and specific validation functionality across a base class and derivations allows us to now focus on specific validation only. This worked great for the RequiredFieldValidator, but works even better for the several remaining validators we should implement to be consistent with ASP.NET, including:

  • RegularExpressionValidator
  • CustomValidator
  • CompareValidator
  • RangeValidator

Let's take a look at each validator and the logic used to implement them.

RegularExpressionValidator

Regular expressions are a powerful textual pattern-matching technology built around a rich set of tokens. When fields require values of a certain shape, regular expressions are a great alternative to string manipulation, especially in the face of non-trivial text patterns. For example, ensuring that a phone number is in Australian format—(xx) xxxx xxxx—can be reduced to a single regular expression:

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

This power is why ASP.NET implements a RegularExpressionValidator, and why we will too:

using System.Text.RegularExpressions;
...
[ToolboxBitmap(
  typeof(RegularExpressionValidator),
  "RegularExpressionValidator.ico")]
class RegularExpressionValidator : BaseValidator {
  ...
  string ValidationExpression {...}
  protected override bool EvaluateIsValid() {
    // Don't validate if empty
    if( ControlToValidate.Text.Trim() == "" ) return true;
    // Successful if match matches the entire text of ControlToValidate
    string field = ControlToValidate.Text.Trim();
    return Regex.IsMatch(field, _validationExpression.Trim());  
  }
}

At design-time, developers can provide the validation expression through the property browser, shown in Figure 8.

Figure 8. Specifying a regular expression for validation

CustomValidator

There will be times when specific validators are not capable of solving involved validation problems, especially where complex business rules are database access is required. In these cases, custom code is the answer and the CustomValidator allows us to write that custom code and ensure that it remains integrated with our custom validation infrastructure, which becomes important for form-wide validation later on. The CustomValidator supports this requirement with a custom Validating event and ValidatingCancelEventArgs:

[ToolboxBitmap(typeof(CustomValidator), "CustomValidator.ico")]
class CustomValidator : BaseValidator {
  class ValidatingCancelEventArgs {...}
  delegate void ValidatingEventHandler(object sender, ValidatingCancelEventArgs e);
  event ValidatingEventHandler Validating;
  void OnValidating(ValidatingCancelEventArgs e) {
    if( Validating != null ) Validating(this, e);
  }

  protected override bool EvaluateIsValid() {
    // Pass validation processing to event handler and wait for response
    ValidatingCancelEventArgs args = 
      new ValidatingCancelEventArgs(false,ControlToValidate);
    OnValidating(args);
    return args.Valid;
  }
}

You handle the CustomValidator's Validating event by double-clicking it from the Property Browser, shown in Figure 9.

Figure 9. Creating a Validating handler for custom, complex validation

Then, simply add the appropriate validation logic, which in this case ensures a new employee is 18 years or older:

class AddNewEmployeeForm : Form
  void cstmDOB_Validating(object sender, ValidatingCancelEventArgs e) {
  try {
    // Must be 18 or older
    DateTime dob = DateTime.Parse(((Control)e.ControlToValidate).Text);
    DateTime  legal = DateTime.Now.AddYears(-18);
    e.Valid = ( dob < legal );
  }
  catch( Exception ex ) { e.Valid = false; }
}

Note that the ASP.NET CustomValidator names its equivalent event, ServerValidate, in reference to the server-side processing nature of ASP.NET. We've used Validating because we don't need to make that distinction and because the BaseValidator already uses Validate from which CustomValidator derives.

BaseCompareValidator

The validators thus far are capable of processing only a single field. In some cases, however, validation may involve several fields and/or values, whether to ensure that a field's value is within a minimum/maximum value range (RangeValidator), or to compare one field against another (CompareValidator). In either case, extra work is needed to perform type checking, conversion, and comparison, including the type against which the values must validate. This functionality should be packaged into a new type, the BaseCompareValidator from which RangeValidator and CompareValidator can derive to save themselves some extra work. BaseCompareValidator itself derives from BaseValidator to obtain basic validation support and implements four new members:

abstract class BaseCompareValidator : BaseValidator { 
  ...  
  ValidationDataType Type {...}
  protected TypeConverter TypeConverter {...}
  protected bool CanConvert(string value) {...}
  protected string Format(string value) {...}
}

ValidationDataType is a custom enumeration that specifies what types are supported for compare and range validation:

enum ValidationDataType {
  Currency,
  Date,
  Double,
  Integer,
  String
}

RangeValidator

For those occasions where you need to make sure a field value is within a specified range, a RangeValidator makes sense. It lets the developer enter a minimum and maximum value and a type for all involved values. This time, RangeValidator derives from BaseCompareValidator:

[ToolboxBitmap(typeof(RangeValidator), "RangeValidator.ico")]
class RangeValidator : BaseCompareValidator {
  ...
  string MinimumValue {...}
  string MaximumValue {...}
  protected override bool EvaluateIsValid() {
      // Don't validate if empty, unless required
      // Validate and convert Minimum
      // Validate and convert Maximum
      // Check minimum <= maximum
      // Check and convert ControlToValue
      // Is minimum <= value <= maximum
    }
  }

The Property Browser is used to configure range and type properties, illustrated in Figure 10.

Figure 10. Configuring the RangeValidator

CompareValidator

Last, but by no means least, comes CompareValidator, which is used to perform an equality test on ControlToValidate and either another control or value. These are specified by ControlToCompare or ValueToCompare respectively:

[ToolboxBitmap(typeof(CompareValidator), "CompareValidator.ico")]
class CompareValidator : BaseCompareValidator {
  ...
  Control ControlToCompare {...}
  ValidationCompareOperator Operator {...}
  string ValueToCompare {...}   
  protected override bool EvaluateIsValid() {
    // Don't validate if empty, unless required
    // Check ControlToCompare provided
    // Check ValueToCompare provided
    // Validate and convert CompareFrom
    // Validate and convert CompareTo
    // Perform comparison eg ==, >, >=, <, <=, !=
  }
}

Operator defines the equality test performed on ControlToValidate and ControlToCompare or ValueToCompare. Operator is specified by ValidationCompareOperator:

enum ValidationCompareOperator {
  DataTypeCheck,
  Equal,
  GreaterThan,
  GreaterThanEqual,
  LessThan,
  LessThanEqual,
  NotEqual
}

As you can tell, you can also use CompareValidator to validate that ControlToValidate is of a particular data type, specified by the DataTypeCheck value. This is the only comparison that does not require either ControlToCompare or ValueToCompare. Figure 11 shows how you might configure the CompareValidator.

Figure 11. Configuring the CompareValidator

The Completed Custom Validation Infrastructure

All ASP.NET equivalent validator components are now present and accounted for. Each implementation directly or indirectly derives from the BaseValidator to inherit basic and reusable validation functionality. Such inheritance creates an implicitly extensible infrastructure that other developers can use as easily as we did. Figure 12 shows the custom validation solution and where developers might leverage it. Situations where this may be necessary include:

  • Creating a new generic validator
  • Creating a business rule specific validator
  • Adding additional related logic to one of the existing validators

The design's extensible support for these situations is illustrated in Figure 12.

Figure 12. Custom validation's extensible support

Where Are We?

We first explored the native validation framework built into the .NET Framework for Windows Forms. We then repackaged our validation logic into several design-time components to transform an initially programmatic validation experience into a more declarative and productive one. In the process, we created an extensible validation infrastructure built on BaseValidator to implement RequiredFieldValidator, RegularExpressionValidator, CustomValidator, CompareValidator, and RangeValidator. This design allows other developers to easily create new validators by also deriving from BaseValidator to inherit generic functionality and reduce the implementation problem to only the specific validation. This article focuses on per-control validation and only hinted at form-wide validation. The next installment builds on custom validation to incorporate form-wide validation, looks at combining existing validators and adds another staple of the ASP.NET validation control suite, the ValidationSummary control. Between now and then, I recommend you watch Amazon Women on the Moon, if only for a few laughs.

An Alternative Custom Validation Solution

I first created the validation components for Genghis way back in late 2002. During research for the referenced design-time MSDN Magazine articles, I found Billy Hollis' validation piece at his (and Rocky's) Adventures in Visual Basic .NET column on MSDN Online. I totally recommend that you read it for a cool Windows Forms validation alternative to the one I built, plus a great discussion on implementing extender properties for the design-time.

C# vs. Visual Basic .NET

Some comments on my previous two installments highlight that those pieces are written for C# only, while plenty of you guys reading the column are Visual Basic® .NET developers. My personal preference is evidently for C#, but I'm quite happy to write for Visual Basic.NET. While I would love to provide both Visual Basic .NET and C# versions of my code samples, I simply don't have the time to put them together and incorporate both into each column installment. But I don't want to exclude anyone either so, as a middle ground, I'd like to swap between Visual Basic .NET and C# as I write each installment. This would make the first Visual Basic .NET-targeted piece appear in two installments (the next Validation piece will continue the C# work started here). I'd love to keep both communities happy within the time I can spend on this work. Does this solution suffice? Please e-mail me and tell me what you think.

Genghis

A more complete and slightly different variant of this solution is currently available from Genghis), a shared source set of extensions for .NET Windows Forms developers a la Microsoft Foundation Classes (MFC). Interestingly, while writing this piece, I needed to make several improvements that will be reflected into Genghis in the next version. However, feel free to download it now if you can't wait and, if you have time, please send me any bugs or enhancements you would like to see addressed.

Acknowledgements

This piece was inspired by Ron Green, Chris Sells, and Amazon Women on the Moon.

References

  • Validator Controls for Windows Forms by Billy Hollis

For a detailed discussion of the intricacies of developing components and controls that target the design-time, check out:

If you are unfamiliar with regular expressions, take a look at the following:

Michael Weinhardt is currently working full-time on various .NET writing commitments that include co-authoring Windows Forms Programming in C#, 2nd Edition (Addison Wesley) with Chris Sells and writing this column. Michael loves .NET in general, Windows Forms specifically, and watches 80s television shows when he can. Visit www.mikedub.net for further information.