现在的位置: 首页 > 综合 > 正文

【MSDN文摘】使用自定义验证组件库扩展windows窗体: Container Scope

2012年11月25日 ⁄ 综合 ⁄ 共 24710字 ⁄ 字号 评论关闭
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.

抱歉!评论已关闭.