Export (0) Print
Expand All
6 out of 13 rated this helpful - Rate this topic

Extending Windows Forms with a Custom Validation Component Library, Part 2

 

Michael Weinhardt
www.mikedub.net

April 20, 2004

Summary: Michael Weinhardt continues his series on custom validation and examines form-wide validation using the FormValidator component. (19 printed pages)


Download the winforms04202004_sample.msi sample file.

Where Were We?

Last month, we implemented a suite of validation components that leveraged the native Windows Forms validation infrastructure to provide reusable, declarative validation from within the Visual Studio® .NET Windows Forms Designer. The result provided per-control validation. That is, validation that occurs as a user navigates from one control to the next. Unfortunately, when a user completes data entry, it is impossible to guarantee that they have navigated to, and subsequently, validated all controls on a form. In these situations, a form-wide validation solution is needed as a safeguard against dodgy data. In this installment, we examine how form-wide validation is programmatically supported by our custom validation component library already, before proceeding to convert it into a purely declarative alternative.

Programmatic Form-Wide Validation

One technique for implementing form-wide validation is to check the validity of all relevant controls simultaneously when a Windows Form's OK button is clicked. Let's use the Add New Employee sample form from last month's installment, shown in Figure 1, to show how.

Figure 1. Add New Employee form and associated validation components

Each validator exposes a Validate method and IsValid property, inherited from BaseValidator. These members can be utilized to determine a form's validity, like so:

private void btnOK_Click(object sender, System.EventArgs e) {
  // Validate all controls, including those whose Validating
  // events may not have had the opportunity to fire
  reqName.Validate();
  reqDOB.Validate();
  cstmDOB.Validate();
  reqPhoneNumber.Validate();
  rgxPhoneNumber.Validate();
  reqTypingSpeed.Validate();
  rngTypingSpeed.Validate();
  reqCommences.Validate();
  cmpCommences.Validate();
  
  // Check whether the form is valid
  if( (reqName.IsValid) &&
      (reqDOB.IsValid) &&
      (cstmDOB.IsValid) &&
      (reqPhoneNumber.IsValid) &&
      (rgxPhoneNumber.IsValid) &&
      (reqTypingSpeed.IsValid) &&
      (rngTypingSpeed.IsValid) &&
      (reqCommences.IsValid) &&
      (cmpCommences.IsValid) ) DialogResult = DialogResult.OK;
  else MessageBox.Show("Form not valid.");
}

There are a few interesting observations we can make about this code. First, my Mum could write better code than the above. Second, it's not scalable as the technique requires us to write more code as more validators are added to the form.

ValidatorCollection

The most important observation, however, is the repeated pattern of calling Validate and IsValid on each and every validator. A pattern like this suggests an enumeration-style refactoring, which would implicitly address the two issues previously highlighted by allowing us to write better code than my Mum and, more importantly, offer a scalable code alternative. Unfortunately, while System.Windows.Forms.Form does implement an enumerable control collection with the Controls property, it doesn't offer a component equivalent. Interestingly, the Windows Forms Designer does inject a designer-generated collection of components into Windows Forms, aptly called components:

public class AddNewEmployeeForm : System.Windows.Forms.Form {
  ...
  /// <summary>
  /// Required designer variable.
  /// </summary>
  private System.ComponentModel.Container components = null;
  ...
}

components manages a list of components that utilize unmanaged resources and need to dispose of them when the host form disposes. One example is the System.Windows.Forms.Timer, which relies on unmanaged Win32® system timers (further discussion is beyond the scope of this piece, but can be found Chapter 9 of Chris Sells' book, Windows Forms Programming in C#). As the components collection is designer-managed, and because our custom validation components don't rely on unmanaged resources, we can't use components for the desired enumeration. Instead, we must create our own guaranteed and strongly-typed collection of BaseValidators. Manually creating such collections, particularly strongly-typed ones, can be a time consuming endeavor. In these situations, I recommend using CollectionGen (http://sellsbrothers.com/tools/#collectiongen), a custom tool for Visual Studio .NET created by Chris Sells' et al to do the heavy lifting for you. CollectionGen produced the following code for the required BaseValidator collection, called ValidatorCollection:

[Serializable]
public class ValidatorCollection : 
  ICollection, IList, IEnumerable, ICloneable {
  // CollectionGen implementation
  ...
}

Perhaps the resulting implementation is more complete than we need to solve our specific problem, but the few hours of code-time it saves frees me up to catch up on some old Knight Rider (http://www.imdb.com/title/tt0083437/) episodes.

ValidatorManager

Unfortunately, neither Michael Knight nor K.I.T.T. are going to incorporate ValidatorCollection into the validation library for us, so we'll need to turn off the TV and do it ourselves. At this point, we do have a ValidatorCollection we can enumerate, but to guarantee it contains a list of all running validators, we need to implement a mechanism to add and remove said validators to and from ValidatorCollection at runtime. The ValidationManager was created to fulfill this requirement:

public class ValidatorManager {

  private static Hashtable _validators = new Hashtable();

  public static void Register(BaseValidator validator, Form hostingForm) {
      
    // Create form bucket if it doesn't exist
    if( _validators[hostingForm] == null ) {
      _validators[hostingForm] = new ValidatorCollection();
    }
      
    // Add this validator to the list of registered validators
    ValidatorCollection validators = 
      (ValidatorCollection)_validators[hostingForm];
    validators.Add(validator);
  }
    
  public static ValidatorCollection GetValidators(Form hostingForm) {
    return (ValidatorCollection)_validators[hostingForm];
  }
    
  public static void DeRegister(BaseValidator validator, 
    Form hostingForm) {
    
    // Remove this validator from the list of registered validators
    ValidatorCollection validators = 
      (ValidatorCollection)_validators[hostingForm];
    validators.Remove(validator);
      
    // Remove form bucket if all validators on the form are de-registered
    if( validators.Count == 0 ) _validators.Remove(hostingForm);
  }
}

ValidatorManager fundamentally uses the _validators hash table to manage a list of one or more ValidatorCollection instances, each representing a set of validators hosted on a specific form. Each ValidatorCollection is associated with a specific form and contains one or more references to BaseValidators hosted by that form. The association is made as a BaseValidator registers and de-registers with the ValidationManager because both Register and DeRegister methods require a reference to the BaseValidator and the form it's hosted on. The ValidatorCollection for a specific form can be retrieved by passing a form reference to GetValidators. The entire implementation is static (shared) to guarantee in-memory access and simplify client code and ValidatorManager instance management.

The Other Side of the Coin: Updating the BaseValidator

Register and DeRegister need to be called from somewhere to make it all work, and that somewhere is nowhere other than the BaseValidator because this logic is common to all validators. Because a BaseValidator lives and dies in concert with its host form, calls to Register and DeRegister need to be synchronized with the hosting form's lifetime, specifically by handling the hosting form's Load and Closed events:

public abstract class BaseValidator : Component {
  ...
  private void Form_Load(object sender, EventArgs e) {
    // Register with ValidatorManager
    ValidatorManager.Register(this, (Form)sender);
  }  
  private void Form_Closed(object sender, EventArgs e) {
    // DeRegister from ValidatorCollection
    ValidatorManager.DeRegister(this, (Form)sender);
  }
  ...
}

The next step is to hook up these event handler's to the Load and Closed events. The form we need is the BaseValidator's ControlToValidate's host form and, because ControlToValidate is of type Control, we can call its FindForm method to retrieve the host form. Unfortunately, we can't call FindForm from the BaseValidator's constructor because its ControlToValidate may not have been assigned a form at that moment. This is a result of how the Windows Form Designer uses InitializeComponent to store the code that constructs a form and assigns controls to parent containers:

private void InitializeComponent() {
  ...
  // Create control instance
  this.txtDOB = new System.Windows.Forms.TextBox();
  ...
  // Initialize control
  // 
  // txtDOB
  // 
  this.txtDOB.Location = new System.Drawing.Point(101, 37);
  this.txtDOB.Name = "txtDOB";
  this.txtDOB.Size = new System.Drawing.Size(167, 20);
  this.txtDOB.TabIndex = 3;
  this.txtDOB.Text = "";
  ...
  // 
  // cstmDOB
  // 
  this.cstmDOB.ControlToValidate = this.txtDOB;
  this.cstmDOB.ErrorMessage = "Employee must be 18 years old";
  this.cstmDOB.Icon = 
    ((System.Drawing.Icon)(resources.GetObject("cstmDOB.Icon")));
  this.cstmDOB.Validating += 
    new CustomValidator.ValidatingEventHandler(this.cstmDOB_Validating);
  // 
  // reqDOB
  // 
  this.reqDOB.ControlToValidate = this.txtDOB;
  this.reqDOB.ErrorMessage = "Date of Birth is required";
  this.reqDOB.Icon = 
    ((System.Drawing.Icon)(resources.GetObject("reqDOB.Icon")));
  this.reqDOB.InitialValue = "";
  ...
  // 
  // AddNewEmployeeForm
  // 
  ...
  // Add control to form and set control's Parent to this form
  this.Controls.Add(this.txtDOB);
  ...
}

As you can see, the control instance is created well before it is assigned to a form, which is after the associated validators, which renders calls to FindForm useless. In this situation, you can turn to System.ComponentModel.ISupportInitialize, which exists to solve initialization dependency problems just like this through two methods it defines—BeginInit and EndInit. The Windows Forms Designer uses reflection to determine whether a component implements ISupportInitialize and, if so, injects calls to both BeginInit and EndInit into InitializeComponent, before and after form initialization respectively. Because EndInit is guaranteed to be called after the BaseValidator's ControlToValidate has been assigned a parent and, consequently, can return a form from FindForm, that's where we should register with the Load and Closed events. The following code shows how:

public abstract class BaseValidator : Component, ISupportInitialize {
  ...
  #region ISupportInitialize
  public void BeginInit() {}
  public void EndInit() {
    // Hook up ControlToValidate's parent form's Load and Closed events 
    // ...
    Form host = _controlToValidate.FindForm();
    if( (_controlToValidate != null) && (!DesignMode) && 
        (host != null) ) {
      host.Load += new EventHandler(Form_Load);
      host.Closed += new EventHandler(Form_Closed);
    }
  }
  #endregion
  ...
}

The updated InitializeComponent looks like this:

private void InitializeComponent() {
  ...
  // Call BaseValidator implementation's BeginInit implementation
  ((System.ComponentModel.ISupportInitialize)(this.reqDOB)).BeginInit();
  ...
  // Control, component and form initialization
  ...
  // Call BaseValidator implementation's EndInit implementation
  ((System.ComponentModel.ISupportInitialize)(this.reqDOB)).EndInit();
}

You might be wondering why I didn't deploy the registration and deregistration calls to ISupportInitialize.EndInit and Dispose respectively. Because the ValidatorManager manages one or more ValidatorCollections hashed by parent form, I wanted to ensure that each ValidatorCollection was removed from the ValidatorManager when its associated form closed, rather than waiting for garbage collection.

Enumerating ValidatorCollection

Creating ValidatorCollection, ValidatorManager and updating BaseValidator completes the registration mechanism we need to enable the desired BaseValidator enumeration. Figure 2 shows an internal representation of how these pieces fit together.

Figure 2. Internal representation of ValidatorManager, ValidatorCollection and BaseValidators

To take advantage of the updated design, all we need is a simple update to the OK button's Click event handler:

private void btnOK_Click(object sender, System.EventArgs e) {
  // Better form wide validation
  ValidatorCollection validators = ValidatorManager.GetValidators(this);
  // Make sure all validate so UI visually reflects all validation issues
  foreach( BaseValidator validator in validators ) {
    validator.Validate();
  }
  foreach( BaseValidator validator in validators ) {
    if( validator.IsValid == false ) {
      MessageBox.Show("Form is invalid");
      return;
    }
  }
  DialogResult = DialogResult.OK; 
}

The resulting code is far more elegant than our first attempt and supports scalability by relieving us from the chore of writing more and more code as further validators are added to a form. Oh, and in your face, Mum!

Declarative Form-Wide Validation: The FormValidator

If the purpose is to write as little code as possible, then we can further reduce this solution by refactoring into a more reusable model. ASP.NET does so natively using System.Web.UI.Page, the type from which all ASP.NET code-behind pages derive. Specifically, Page implements the following validation-oriented members:

public class Page : TemplateControl, IHttpHandler {
  ...
  public virtual void Validate();
  public bool IsValid { get; }
  public ValidatorCollection Validators { get; }
  ...
}

We already have a ValidatorCollection (so named for consistency), and the usage of Validate and IsValid turns out to be the equivalent of the form-wide enumeration-based validation logic we just implemented. Unfortunately, although System.Windows.Forms.Form implements Validate, it is tied to Windows Forms native validation that we are leveraging from our custom library, not integrating with. As such, it makes sense to continue with one of the key themes of this series, which is to redeploy appropriate logic into reusable components that developers can drag and drop onto their forms as needed. A component to validate a form can only be called the FormValidator, and is shown here implementing Validate and IsValid:

[ToolboxBitmap(typeof(FormValidator), "FormValidator.ico")]
public class FormValidator : Component {

  private Form _hostingForm = null;
  ... 
  public Form HostingForm {...}
  public bool IsValid {
    get {
      // Get validators for this form, if any
      ValidatorCollection validators = 
        ValidatorManager.GetValidators(_hostingForm);
      if( validators == null ) return true;
      // Check validity
      foreach(BaseValidator validator in validators) {
        if( validator.IsValid == false ) return false;
      }
      return true;
    }
  }
    
  public void Validate() {            
    // Get validators for this form, if any
    ValidatorCollection validators = 
      ValidatorManager.GetValidators(_hostingForm);
    if( validators == null ) return;
    // Validate
    Control firstInTabOrder = null;      
    foreach(BaseValidator validator in validators) {
      validator.Validate();
    }  
  }
}

Besides Validate and IsValid, FormValidator implements a HostingForm property. Because components have no way of natively ascertaining their hosting form like controls can from either their Parent property or FindForm method, we need to perform a little design-time trickery to achieve the same goals, manifested as the HostingForm property. A magician never reveals his tricks, but I'm no magician and this isn't my trick, so feel free explore this technique in details, also in Chapter 9 of Chris Sells' book. After rebuilding the CustomValidation project and adding the FormValidator component to the Toolbox, we simply drag one onto our form to use it, shown in Figure 3.

Figure 3. Using the FormValidator component

Relying on the FormValidator, the OK button's Click event handler is reduced to three lines of code:

private void btnOK_Click(object sender, System.EventArgs e) {
  formValidator.Validate();
  if( formValidator.IsValid ) DialogResult = DialogResult.OK;
  else MessageBox.Show("Form not valid.");
}

Figure 4 shows the result at runtime.

Figure 4. FormValidator in action

While reducing the client code investment to three lines is great, zero lines of code would be better, especially to achieve fully declarative form-wide validation. To achieve this, the FormValidator needs to implement its own version of the three lines of code and execute it on our behalf at the appropriate moment, which is when a form's AcceptButton is clicked. A form's AcceptButton and CancelButton can both be set from the Property Browser at design-time, shown in Figure 5.

Figure 5. Specifying a form's AcceptButton and CancelButton

This specifies that the designated AcceptButton is clicked when a user presses the Enter key on a form, while the designated CancelButton is clicked when a user presses the ESC key. FormValidator needs to determine its host form's AcceptButton and then handle that button's Click event, which is dependent on AcceptButton being set from within InitializeComponent. Consequently, we must again implement ISupportInitialize, shown here:

public class FormValidator : Component, ISupportInitialize {

  #region ISupportInitialize
  public void BeginInit() {}
  public void EndInit() {
    if( (_hostingForm != null) ) {
      Button acceptButton = (Button)_hostingForm.AcceptButton;
      if( acceptButton != null ) {
        acceptButton.Click += new EventHandler(AcceptButton_Click);
      }
    }
  }
  #endregion

  private Form _hostingForm = null;
  [Browsable(false)]
  [DefaultValue(null)]
  public Form HostingForm {...}
  ...
  private void AcceptButton_Click(object sender, System.EventArgs e) {
    Validate();
    if( IsValid ) _hostingForm.DialogResult = DialogResult.OK;
    else MessageBox.Show("Form not valid.");
  }
  ...
}

What Happens When You Assume

This solution works because I'm using the FormValidator on a form with a specific configuration geared towards modal dialog behavior, which includes setting the form's FormBorderStyle property to FixedDialog, setting the AcceptButton and CancelButton discussed earlier. It also means setting the DialogResult for both AcceptButton and CancelButton to null and Cancel respectively. The result is the dialog automatically closes on Cancel, but requires us to handle the AcceptButton's Click event, which is what the FormValidator does on our behalf so we don't need to write any code. However, this assumes your owner-owned form model involves validating the form and processing the data gathered by the dialog after returning to the parent form. Unfortunately, a goodly number of you may use an alternative approach, such as validating the form and processing the gathered data before returning to the parent form. The problem with the latter technique is that automatic validation results in having two Click event handlers registered with the AcceptButton, one created by the developer and one created by the FormValidator. While I prefer the former technique, everybody's different and we can celebrate that difference by simply creating a property, ValidateOnAccept to specify whether automatic validation is required and update FormValidator accordingly:

public class FormValidator : Component, ISupportInitialize {

  #region ISupportInitialize
  public void BeginInit() {}
  public void EndInit() {
    if( (_hostingForm != null) && _validateOnAccept ) {
      Button acceptButton = (Button)_hostingForm.AcceptButton;
      if( acceptButton != null ) {
        acceptButton.Click += new EventHandler(AcceptButton_Click);
      }
    }
  }
  #endregion

  private Form _hostingForm = null;
  private bool _validateOnAccept = true;
  private string _errorMessage = "Form is not valid.";

  [Browsable(false)]
  [DefaultValue(null)]
  public Form HostingForm {...}

  [Category("Behavior")]
  [Description("If the host form's Accept property is set...")]
  [DefaultValue(true)]
  public bool ValidateOnAccept {
    get { return _validateOnAccept; }
    set { _validateOnAccept = value; }
  }
    
  [Category("Behavior")]
  [Description("Specifies the error message displayed...")]
  [DefaultValue("Form is not valid.")]
  public string ErrorMessage {
    get { return _errorMessage; }
    set { _errorMessage = value; }
  }

  private void AcceptButton_Click(object sender, System.EventArgs e) {
    Validate();
    if( IsValid ) _hostingForm.DialogResult = DialogResult.OK;
    else {
      string caption = string.Format("{0} Invalid", _hostingForm.Text);
      MessageBox.Show(_errorMessage, caption);
    }
  }
  ...
}

ValidateOnAccept is True by default. You'll notice I've also included an ErrorMessage property to allow some degree of customization around displaying a useful error message to the user.

Validating in Tab Order

Also of use to the user is the order in which validation is visually processed. Currently, FormValidator selects the first invalid control rather than the first control in visual order, as specified by tab order. Figure 6 shows the correct tab order for the Add New Employee form.

Figure 6. Specifying tab order

Validating in tab order allows the user to work down the form to fix invalid fields, which is a little more intuitive than a seemingly random approach. To make sure validation occurs in tab order, FormValidator must be updated like so:

[ToolboxBitmap(typeof(FormValidator), "FormValidator.ico")]
public class FormValidator : Component {
  ... 
  public Form HostingForm {...}
  public bool IsValid {...}
  public void Validate() {            
    // Validate all validators on this form, ensuring first invalid
    // control (in tab order) is selected
    Control firstInTabOrder = null;
    ValidatorCollection validators = 
      ValidatorManager.GetValidators(_hostingForm);
    foreach(BaseValidator validator in validators) {
      // Validate control
      validator.Validate();
      // Record tab order if before current recorded tab order
      if( !validator.IsValid ) {
        if( (firstInTabOrder == null) || 
            (firstInTabOrder.TabIndex > 
               validator.ControlToValidate.TabIndex) ) {
          firstInTabOrder = validator.ControlToValidate;
        }
      }
    }
    // Select first invalid control in tab order, if any
    if( firstInTabOrder != null ) firstInTabOrder.Focus();
  }  
}   

Figure 7 shows the result by setting focus on the first invalid control in tab order.

Figure 7. Setting focus on the first invalid control in Tab Order, which is Date of Birth.

Where Are We?

This time round the mulberry bush, we continued the journey by building on per-control validation established in the first installment to provide form-vide validation with the FormValidator. Depending on how you use modal dialogs, the FormValidator can support a completely declarative form-wide validation experience. As it turns out, though, we have built the two extremes of validation scope—per-control and form-wide. However, Windows Forms may contain a tab control with several tabs, each loosely related or completely unrelated, and each needing its own validation. The Windows® desktop properties dialog and its use of the Apply button on each property tab is one example. In these scenarios, container-specific validation makes more sense. In the next and final installment in the validation series, that's exactly the problem we tackle. We'll also extend the validation component library with the ability to display validation error summaries through a base implementation and an extensible design to allow further custom summary solutions.

Acknowledgements

Many thanks this time. First, thanks again to Ron Green and Chris Sells for working the problem. To Shawn A. Van Ness and Chris Sells et al for CollectionGen, Stephen Goodwin for his observation on article introductions, Ian Griffiths and Chris Sells for the HostingForm design-time trickery, and the readers who responded to my last installment for their comments on the solution I presented. I figure my pieces are interesting enough only when you guys are inspired enough to comment, and especially when you disagree :). So, feel free to e-mail (mikedub@optusnet.com.au) me your thoughts, especially if I can apply relevant observations back into this column.

Last but nowhere near least, happy Mother's Day, Mum!

Visual Basic .NET vs. C#

At this point, nobody seemed too concerned with whether I write for Visual Basic® .NET or C#, so I think I'll stick with C#. I'll endeavor to keep it as readable as possible—avoid using the Ternary operator, as requested by one reader. I'd like to provide both C# and Visual Basic .NET code samples, but it takes a lot of time to prepare, especially given that this month's sample is around 1300 lines of code. However, if someone can suggest a *good* C# to Visual Basic .NET converter that drastically reduces the conversion effort, I'll seriously consider providing samples in both languages.

References

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.

Did you find this helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft. All rights reserved.