Export (0) Print
Expand All
11 out of 15 rated this helpful - Rate this topic

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

 

Michael Weinhardt
www.mikedub.net

June 18, 2004

Summary: This time round, we build on the control-scoped developed in Part 1 and the form-scoped validation developed in Part 2 to build control-scoped validation support. We also create an extensible validation summary framework with two separate implementations. (21 printed pages)


Download the winforms05182004_sample.msi sample file.

Where Were We?

In Part 1 of this series, we leveraged native Windows Forms validation to develop a custom library of reusable, control-scoped validation components. In Part 2, we consolidated on our efforts to construct automatic, code-free, form-scoped validation. In this final installment, we'll explore a third validation scope that lies between the other two—per-container validation—before finishing off by constructing an extensible validation summary framework into which we plug not one, but two implementations.

Too Much of One and Not Enough of the Other

Part 1 and Part 2 were developed around the form shown in Figure 1.

Figure 1. Add New Employee form with control-scoped and form-scoped validation

Add New Employee is a simple form containing a few Label, TextBox, and Button controls are all contained in the form itself. In the real world, however, forms can often be more complex. Such complexity often involves grouping or container controls, like Tab or GroupBox controls, to organize other controls into more visually appealing and functional layouts. The Add Employee Wizard form is an example of such form, with two wizard steps. Figure 2 shows the first step, gathering personal details, and Figure 3 shows the second step of gathering preferences.

Figure 2. Add Employee Wizard step 1: gathering Personal Details

Figure 3. Add Employee Wizard step 2: gathering employee preferences

Wizards are often used to guide users through complex or infrequent tasks by breaking them down into a series of small, easily palatable steps. Depending on the type of information being gathered by a wizard, validation might make sense on a step-by-step basis. This could be the case when a later wizard step depends on data in an earlier wizard step. Alternatively, if a complex wizard contains many steps it might be more appealing to validate as you go rather than validate the entire wizard at the end. While wizards could be built in many different ways, for this sample I used one Panel control for each step. That is one for gathering employee's Personal Details, personalDetailsStep, and one for an employee's Preferences, preferencesStep. Each step uses the appropriate validation components to provide validation. I would also like to validate each step before users can move to another step or complete the wizard. When considering our two existing validation options, we find that control-scoped validation does not provide enough support, while form-scoped validation provides too much.

Enter the ContainerValidator

What we need is a third option that knows how to validate container controls such as Panel, and that's where ContainerValidator comes in. And, just like our validation components, our first step is to identify exactly which controls it supports. Natively, System.Windows.Forms.Control provides containment support with its Controls property, but because it's the parent of the entire control and form inheritance hierarchy in System.Windows.Forms, using just this option would be overkill given that it would include a vast number of non-containment controls. We have to be a little more discerning and Figure 4 shows the six direct and indirect derivations of Control I designated for support by ContainerValidator.

Figure 4. The six types targeted for container validation

Form is included because it shares similar grouping and containment characteristics to the other five controls. Consequently, there turns out to be a large degree of overlap between how the FormValidator and ContainerValidator will operate, particularly with regard to the use of Validate and IsValid. In direct juxtaposition, there turns out to be a small amount of implementation-specific code. A situation like this suggests the construction of a base class to capture and expose the overlap, from which FormValidator and ContainerValidator can mutually derive and enhance for their specific purposes. The next step, then, involves the deconstruction of FormValidator into a new base class called BaseContainerValidator, shown here:

public abstract class BaseContainerValidator : Component {
  ...
  public Form HostingForm {...}
  ...
  public bool IsValid {
    get {
      foreach(BaseValidator validator in GetValidators() ) {
        if( !validator.IsValid ) {
          return false;
        }
      }
      return true;
    }
  }

  public void Validate() {   
    // Validate
    Control firstInTabOrder = null;
    foreach(BaseValidator validator in GetValidators() ) {
      // Validate control
      validator.Validate();  
      // Record tab order if before current recorded tab order
      if( !validator.IsValid ) {
        if( (firstInTabOrder == null) || 
            (firstInTabOrder.TabIndex > validator.TabIndex) ) {
          firstInTabOrder = validator.ControlToValidate;
        }
      }
    }
    // Select first invalid control in tab order, if any
    if( firstInTabOrder != null ) {
      firstInTabOrder.Focus();
    }
  }
  public abstract ValidatorCollection GetValidators();
}

This implementation is obviously familiar to the FormValidator, although it excludes FormValidator-specific features such as the ValidateOnAccept property. The key difference, however, is ContainerValidator's abstract GetValidators member. FormValidator's Validate and IsValid members both enumerate a ValidatorCollection that contains all BaseValidators hosted within the jurisdiction of the FormValidator, that is the whole Form. On the other hand, ContainerValidator's jurisdiction is somewhat smaller and, consequently, so is the set of BaseValidators it needs to enumerate. GetValidators allows each BaseContainerValidator to return a set of BaseValidators within their own specific jurisdiction. BaseContainerValidator itself uses a generic refactoring of FormValidator's enumeration logic to cope with any ValidatorCollection. Note that we also leave the task of determining which controls a BaseContainerValidator derivation can support up to them, as they can and do differ from derivation to derivation.

Updated FormValidator

Given the new base container validation type, we need to update FormValidator to suit, specifically by overriding GetValidators and adding in the ValidateOnAccept logic we developed in the last installment:

[ToolboxBitmap(typeof(FormValidator), "FormValidator.ico")]
public class FormValidator : BaseContainerValidator, ISupportInitialize {
  ...
  #region ISupportInitialize
  public void BeginInit() {}
  public void EndInit() {
    // Handle AcceptButton click if requested
    if( (HostingForm != null) && _validateOnAccept ) {
      Button acceptButton = (Button)HostingForm.AcceptButton;
      if( acceptButton != null ) {
        acceptButton.Click += new EventHandler(AcceptButton_Click);
      }
    }
  }
  #endregion
  ...
  public bool ValidateOnAccept {...}  
  public override ValidatorCollection GetValidators() {
    return ValidatorManager.GetValidators(HostingForm);
  }
  private void AcceptButton_Click(object sender, System.EventArgs e) {
    // If DialogResult is OK, that means we need to return None
    if( HostingForm.DialogResult == DialogResult.OK ) {
      Validate();
      if( !IsValid ) {
        HostingForm.DialogResult = DialogResult.None;
      }
    }
  }
}

The usage of the new and improved FormValidator is exactly the same as the previous installment, so we won't dwell.

ContainerValidator

Instead, let's move on to the new ContainerValidator, which is what this installment is about after all. Like the updated FormValidator, this simply involves deriving from BaseContainerValidator, overriding GetValidators and adding container-specific functionality, such as specifying a container to validate and a validation depth:

[ToolboxBitmap(typeof(ContainerValidator), "ContainerValidator.ico")]
public class ContainerValidator : BaseContainerValidator {
  ...
  [TypeConverter(typeof(ContainerControlConverter))]
  public Control ContainerToValidate {...}
  ...
  public ValidationDepth ValidationDepth {...}
  ...
  public override ValidatorCollection GetValidators() {
    return ValidatorManager.GetValidators(
      HostingForm, 
      _containerToValidate);
  }
}

Well, that was easy now, wasn't it? ContainerToValidate allows a developer to choose which container control that ContainerValidator will supervise, as specified by the ContainerControlConverter. This is similar to how BaseValidator uses the ControlToValidate property. ValidationDepth is included to specify whether ContainerValidator will validate immediate child controls, or all controls. Where this distinction becomes important is when one container control contains one or more non-container controls, as well as one or more container controls. Validation depth is specified by the aptly named ValidationDepth enumeration:

public enum ValidationDepth {
  ContainerOnly,
  All
}

You may have noticed that GetValidators has been overridden with a call to a new ValidatorManager.GetValidators override that returns all validators for a specific container to a specific depth:

public class ValidatorManager { 
  ...
  public static ValidatorCollection GetValidators(
    Form hostingForm, 
    Control container,
    ValidationDepth validationDepth) {
    ValidatorCollection validators = 
      ValidatorManager.GetValidators(hostingForm);
    ValidatorCollection contained = new ValidatorCollection();
    foreach(BaseValidator validator in validators ) {
      // Only validate BaseValidators hosted by the container I reference
      if( IsParent(container, 
                   validator.ControlToValidate, 
                   validationDepth) ) { 
        contained.Add(validator);
      }
    }
    return contained;
  }
  ...
}

With that in place, we can add our newly built ContainerValidator to the Add Employee Wizard and configure it, as shown in Figure 5.

Figure 5. Configuring a ContainerValidator

While the FormValidator can automatically validate when an AcceptButton is clicked, we have no such luxury with ContainerValidators. Even though we could pick a button to automatically validate on, there is no simple way to return the validation results to the button for further processing. Unfortunately, this means we have to write code to validate the container:

public class AddEmployeeWizardForm : System.Windows.Forms.Form {
 ...
  private void nextButton_Click(object sender, System.EventArgs e) {
    if( personalDetailsPage.Visible ) {
      this.detsContainerValidator.Validate();
      if( this.detsContainerValidator.IsValid ) {
        // Configure form
        ...
      }
      else {
     MessageBox.Show("Personal details invalid.");
      }
    }
  }
  ...
}

When the Next button is clicked, this code executes as illustrated in Figure 6.

Figure 6. ContainerValidator in action

Tab Index Issues

While both FormValidator and ContainerValidator are now validating successfully, they will run into problems when confronted with a form like Figure 7.

Figure 7. Add Employee in tabbed layout with FormValidator

This variation on the Add Employee Wizard allows users to enter both employee details and preferences before clicking OK and activating validation, in this case using the FormValidator. If you remember back to the last installment, you'll recall that we spent some time ensuring controls are validated in a visually logical tab order. While that logic still works in a variety of situations, it fails when confronted with validation of controls over multiple tab pages. As it turns out, through the design time, all the controls on the Employee Details tab page are in tab order before the controls on the Preferences tab page. Physically, however, both Name and Alias text boxes share the same physical tab index. The visual result is that the controls are validated left to right and top to bottom across tab pages, rather than the desired top to bottom and left to right across tab pages. Figure 8 illustrates the dueling tab indices.

Figure 8. Designer tab order vs. actual TabIndex

To counteract this, we need to devise a unique value to compare against. I've done exactly this by updating BaseValidator with a read-only FlattenedTabIndex property that returns a unique decimal value calculated by concatenating the tab index of each control from the host form down to ControlToValidate. The property is shown here:

public abstract class BaseValidator : Component, ISupportInitialize {
  ...
  public decimal FlattenedTabIndex {
    get {
      // Generate unique tab index and store it if 
      // not already generated
      if( _flattenedTabIndex == null ) {
        StringBuilder sb = new StringBuilder();
        Control current = _controlToValidate;
        while( current != null ) {
          string tabIndex = current.TabIndex.ToString();
          sb.Insert(0, tabIndex);
          current = current.Parent;
        }
        sb.Insert(0, "0.");
        _flattenedTabIndex = sb.ToString();
      }
      // Return unique tab index
      return decimal.Parse(_flattenedTabIndex);
    }
  }
  ...
}

This code produces a tab order of 0.000n for all validated controls on the Employee Details tab page and 0.001n for all validated controls on Configuration tab page. For example, Figure 9 shows the flattened tab index value for txtName.

Figure 9. Flattened tab index value for txtName

The following shows how BaseContainerValidator has been updated to use FlattenedTabIndex:

public abstract class BaseContainerValidator : Component {
  ...
  public void Validate() {            
    // Validate
    BaseValidator firstInTabOrder = null;
    ValidatorCollection invalid = new ValidatorCollection();
    foreach(BaseValidator validator in GetValidators() ) {
      // Validate control
      validator.Validate();          
      // Set focus on the control it its invalid and the earliest invalid
      // control in the tab order
      if( !validator.IsValid ) {
        if( (firstInTabOrder == null) || 
            (firstInTabOrder.FlattenedTabIndex > 
             validator.FlattenedTabIndex) ) {
          firstInTabOrder = validator;
        }
        invalid.Add(validator);
      }
    }
    // Select first invalid control in tab order, if any
    if( firstInTabOrder != null ) {
      firstInTabOrder.ControlToValidate.Focus();
    }
  }
  ...
}

Ensuring tab order validation both within and across container controls completes the essential work on ContainerValidator and leaves us with an extensible suite of control-scoped, container-scoped, and form-scoped validations to support a variety of UI scenarios.

Validation Summary

Capturing invalid data is one thing, but relaying that information to the user turns out to be another. While the individual validation components provide information using ErrorProviders, the best our FormValidator can do is display a custom message using a MessageBox. This allows developers to display simple and complex messages, of course, although they have to code it by hand. The ideal situation would be to drag a component onto a form, configure it, and have it display a message for you at the appropriate moment. In the ASP.NET validation infrastructure, this task is performed by ValidationSummary web control, which presents a complete validation summary of the entire Web form as simple text, list, or a bulleted list. This concept turns out to be useful for container- and form-scoped validation in Windows Forms, and one that we'll spend the rest of this installment building.

BaseValidationSummary Component

The first consideration that we need to make is that of extensibility. I wanted to create an extensible validation summary design because there are many more shapes and sizes of validation summary than I can provide out of the box. So, out of the development box, I'm going to use a familiar design pattern for injecting a point of extensibility into our design by creating a validation summary base class called BaseValidationSummary. Much like BaseValidator, BaseValidationSummary would be abstract to ensure it is derived from before being used, and would implement the backbone of functionality common to all validation summary derivations.

One other important consideration to make is which components need a validation summary and how are they hooked up. It doesn't make much sense to provide a summary for a single validation component, while container- and form-scoped validators can contain multiple validation components in which case summarization makes sense. And, while some forms may only use a FormValidator, others may have multiple ContainerValidators, in which case it would be easier for developers to use a single validation summary to service one or more ContainerValidators. A common technique for providing functionality to one or more other controls is the extender property provider model, like that used by the ErrorProvider and ToolTip components. Extender providers add properties to other controls and, subsequently, providing functionality to those controls. This is what the ValidationSummary component should do. The final consideration is how to provide derivation-specific execution. The following code shows how I've approached the issues of extensibility, extender provider properties and derivation-specific execution:

[ProvideProperty("ShowSummary", typeof(BaseContainerValidator))]
[ProvideProperty("ErrorMessage", typeof(BaseContainerValidator))]
[ProvideProperty("ErrorCaption", typeof(BaseContainerValidator))]
public abstract class BaseValidationSummary : 
  Component, IExtenderProvider {
  
  private Hashtable _showSummaries = new Hashtable(); 
  private Hashtable _errorMessages = new Hashtable();
  private Hashtable _errorCaptions = new Hashtable();
    
  #region IExtenderProvider
  bool IExtenderProvider.CanExtend(object extendee) {
    // We extend to BaseContainerValidators only
    return true;
  }
  #endregion
  
  // ShowSummary property
  public bool GetShowSummary(BaseContainerValidator extendee) {...}
  public void SetShowSummary(BaseContainerValidator extendee, 
    bool value) {...}
  
  // ErrorMessage Property
  public string GetErrorMessage(BaseContainerValidator extendee) {...}
  public void SetErrorMessage(BaseContainerValidator extendee, 
    string value) {...}

  // ErrorCaption property
  public string GetErrorCaption(BaseContainerValidator extendee) {...}
  public void SetErrorCaption(BaseContainerValidator extendee, 
    string value) {...}
}

This design uses a standard approach to extender property provider implementation. While beyond the scope of this installment, you can get more information from these articles, Building Windows Forms Controls and Components with Rich Design-Time Features, Part 1 and Building Windows Forms Controls and Components with Rich Design-Time Features, Part 2, that Chris Sells and I wrote, or Billy Hollis' cool validation article, Validator Controls for Windows Forms). First, BaseValidationSummary extends its properties to all BaseContainerValidator components, as specified in the ProvidePropertyAttributes adorning the class. The properties that BaseValidationSummary extended to BaseContainerValidators include ShowSummary, ErrorMessage and ErrorCaption. The latter two allow customization of the validation summary presentation while ShowSummary lets a BaseContainerValidator opt in or out of using the validation summary. This offers flexibility in situations where multiple ContainerValidators may exist on a form although not all of them need to show a summary. The implementation so far collects summary configuration information. Next, we need to provide a mechanism for displaying a summary at the appropriate moment and constrained by the aforementioned configuration information. Because we are extending FormValidator and ContainerValidator, neither of those types has explicit ValidationSummary knowledge, which is desired to avoid tight coupling. However, BaseValidationSummary needs to know when a BaseContainerValidator requires the summary to be displayed. The most appropriate technique is to implement an event on BaseContainerValidator that BaseValidationSummary can handle with derivation-specific validation logic. I've implemented this as the Summarize event, shown here:

public class SummarizeEventArgs {
  public ValidatorCollection Validators;
  public Form HostingForm;
  public SummarizeEventArgs(
    ValidatorCollection validators, 
    Form hostingForm) {
    Validators = validators;
    HostingForm = hostingForm;
  }
}
public delegate void SummarizeEventHandler(
  object sender, 
  SummarizeEventArgs e);
public abstract class BaseContainerValidator : Component {
  ...
  public event SummarizeEventHandler Summarize;
  protected void OnSummarize(SummarizeEventArgs e) {
    if( Summarize != null ) {
      Summarize(this, e);
    }
  }
  // Support validation in flattened tab index order
  public ValidationSummaryDisplayMode GetDisplayMode(
    BaseContainerValidator extendee) {...}
}

Next, BaseValidationSummary needs to register and deregister with that event at some point. Because ShowSummary ultimately states whether the BaseValidationSummary is used or not, the best location is when ShowSummary is set or, rather, when a BaseContainerValidator opts in for validation summary. BaseValidationSummary also needs a handler to subscribe with, which is where the Summarize event comes in. The updated SetShowSummary is shown here:

public abstract class BaseValidationSummary : 
  Component, IExtenderProvider {
  ...
  public void SetShowSummary(BaseContainerValidator extendee, 
                             bool value) {
    if( value == true ) {
      _showSummaries[extendee] = value;
      extendee.Summarize += new SummarizeEventHandler(Summarize);
    }
    else {
      _showSummaries.Remove(extendee);
    }
  }
  ...
  protected abstract void Summarize(object sender, SummarizeEventArgs e);
  ...
}

Notice that Summarize is abstract, and so is the class definition implicitly. This achieves two goals. First, derivations must be created. Second, each derivation is responsible for implementing summarization in whichever way it sees fit. This supports the desired extensibility and reduces custom derivation to the point of implementing only the bits specific to a particular type of summarization.

ValidationSummary Component

With BaseValidationSummary in place, we can begin the derivation carnival. Our equivalent to ASP.NET's ValidationSummary is the first derivation to take place. First, we create a new ValidationSummary class and derive it from BaseValidationSummary. Second, we start adding specific ValidationSummary functionality by implementing the DisplayMode property to allow developers to choose the validation summary format. The possible set of display modes is captured by the ValidationSummaryDisplayMode enumeration:

public enum ValidationSummaryDisplayMode {
  List, // Simple list
  BulletList, // Bulleted list
  SingleParagraph, // No line-breaks
  Simple, // Plain MessageBox
}

The other implementation-specific task is to override the base class' Summarize event handler with the code that does the summarization work. The result is shown here:

[ToolboxBitmap(typeof(ValidationSummary), "ValidationSummary.ico")]
[ProvideProperty("DisplayMode", typeof(BaseContainerValidator))]
public class ValidationSummary : BaseValidationSummary {
  ...
  private Hashtable _displayModes = new Hashtable();
  ...
  // DisplayMode property
  public ValidationSummaryDisplayMode 
    GetDisplayMode(BaseContainerValidator extendee) {...}
  public void SetDisplayMode(BaseContainerValidator extendee, 
    ValidationSummaryDisplayMode value) {...}
  ...
  protected override void Summarize(object sender, SummarizeEventArgs e) {      
    // Don't validate if no validators were passed
    ...
    // Make sure there are validators
    ...
    // Get error text, if provided
    ...   
    // Get error caption, if provided
    ...
    // Build summary message body
    string errors = "";
    if( displayMode == ValidationSummaryDisplayMode.Simple ) {
      // Build Simple message
      errors = errorMessage;
    }
    else {
      // Build List, BulletList or SingleParagraph
      foreach(object validator in base.Sort(validators)) {
        BaseValidator current = (BaseValidator)validator;
        if( !current.IsValid ) {
          switch( displayMode ) {
            case ValidationSummaryDisplayMode.List:
              errors += string.Format("{0}\n", current.ErrorMessage);
              break;
            case ValidationSummaryDisplayMode.BulletList:
              errors += string.Format("- {0}\n", current.ErrorMessage);
              break;
            case ValidationSummaryDisplayMode.SingleParagraph:
              errors += string.Format("{0}. ", current.ErrorMessage);
              break;
          }
        }
      }
      // Prepend error message, if provided
      if( (errors != "") && (errorMessage != "") ) {
        errors = string.Format("{0}\n\n{1}", errorMessage.Trim(), errors);
      }
    }
    // Display summary message
    MessageBox.Show(errors, 
                    errorCaption, 
                    MessageBoxButtons.OK, 
                    MessageBoxIcon.Warning);
  }
}

We can now rebuild the solution, add ValidationSummary to the ToolBox, drag into onto a form with either a FormValidator or ContainerValidator from which we can set the ShowSummary, DisplayMode, ErrorMessage, and ErrorCaption extender provided properties, shown in Figure 10.

Figure 10. Setting ValidationSummary's extended properties at design-time

At runtime, these settings produce the message box shown in Figure 11.

Figure 11. ValidationSummary display a bulleted list validation summary

ListValidationSummary Component

ValidationSummary shows a static validation summary that is useful in a variety of situations, especially if the list of validation errors is small and easy to remember. However, more complex forms require more validation that results in a bigger summary. Big summaries make it harder for users to remember what was actually summarized when they go back to editing the form. What we need in these situations is a dynamic validation summary that can stay open while users edit forms. To that end, and to ensure that the validation summary infrastructure was extensible, I built the ListValidationSummary component, which dynamically manages and displays validation errors using an internally managed sizeable tool window with a list box to reflect the current state of validation on the form, as shown in Figure 12.

Figure 12. Dynamic ListValidationSummary component in action

Unfortunately, I don't have enough room to discuss the implementation so I urge you to explore it yourself, especially if you need to build your own custom validation summary implementations. However, here is a brief list of ListValidationSummary's features:

  • When OK is clicked, it appears showing all invalid controls.
  • You can double-click summary list entries to set focus in the corresponding control being validated.
  • As controls become valid, corresponding list entries disappear from the summary list.
  • As controls become invalid, corresponding list entries appear in the summary list.
  • All entries appear in the list in flattened tab index order.
  • The summary form can be closed by the user, or when the host container or form is disposed.

Where Are We?

With ContainerValidator and extensible validation summary support, complete with two implementations, this installment brings the validation series to a close. In part 1, we built a bunch of control-scoped validation components on top of native Windows Forms validation infrastructure. Part 2 leveraged these components to provide a 100 percent declarative form-scoped validation solution. We've finished off by building the ContainerValidator to handle container-scoped validation. We also created an extensible validation summary framework, and built two validation summary components. The first is reminiscent of the ValidationSummary control found in ASP.NET 2.0. The second is a more dynamic version that you may find useful.

Genghis

The solution you can download with this installment now encompasses and extends the version found at Genghis. I intend to rotate this code into the next Genghis drop but, for now, you should prefer this version. And, as always, feel free to e-mail me if you have any bug reports or ideas for enhancements, so I can include them in the next Genghis drop if suitable.

Acknowledgements

Thanks to Mike Harsh and his team for their excellent and ongoing technical reviews and feedback, and for keeping the zing in Windows Forms on its way to version 2.0. Also thanks to Marc Wilson who, as my MSDN editor/reviewer, has shown a lot of understanding and flexibility during recent writing blues. There aren't enough Tim Tams in Australia to (a) feed his new addiction and (b) say thanks.

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.