本页内容
![]() | 回顾 |
![]() | 程序性窗体范围验证 |
![]() | ValidatorCollection |
![]() | ValidatorManager |
![]() | 问题的另一面:更新 BaseValidator |
![]() | 枚举 ValidatorCollection |
![]() | 声明性窗体范围验证:FormValidator |
![]() | 按照 Tab 键顺序验证 |
![]() | 我们所处的位置 |
![]() | 致谢 |
![]() | Visual Basic .NET 与 C# |
![]() | 参考资料 |
回顾
上一篇,我们实现了一套验证组件,这些组件借助于固有的 Windows 窗体验证基础结构,从 Visual Studio .NET Windows 窗体设计器内部提供可重用的、声明性的验证。结果提供了针对每个控件的验证,即当用户在控件之间导航时发生的验证。遗憾的是,当用户完成数据输入时,无法保证他们已经导航到并随后验证了窗体中的所有控件。在上述情形下,需要使用窗体范围的验证解决方案来防止输入不可靠的数据。在这一期中,我们将探讨已有的自定义验证组件库如何以编程方式支持窗体范围验证,然后再将其转换为纯声明性的替代验证。
程序性窗体范围验证
实现窗体范围验证的一种技术是在 Windows 窗体的 OK 按钮被单击时,同时检查所有相关控件的有效性。让我们使用上一期中的 Add New Employee 示例窗体(如图 1 所示)来说明这一方法。

图 1 Add New Employee 窗体及相关的验证组件
每个验证程序都公开了 Validate 方法和 IsValid 属性,它们都继承自 BaseValidator。可以利用这些成员来确定窗体的有效性,如下所示:
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."); }
关于上述代码,我们可以发表一些有意思的意见。首先,我的母亲可以写出比这更好的代码。其次,它不是可伸缩的,因为随着更多验证程序被添加到窗体中,该技术要求我们编写更多的代码。
ValidatorCollection
然而,最重要的意见是对每个验证程序都反复调用 Validate 和 IsValid。类似这样的模式使人联想到进行枚举样式的重构,它使我们可以编写比我的母亲更好的代码,并且更为重要的是,可以提供可伸缩的替代代码,这无疑会解决前面强调的两个问题。遗憾的是,尽管 System.Windows.Forms.Form 确实通过 Controls 属性实现了一个枚举控件集合,但它并没有提供具有类似功能的组件。但有趣的是,Windows Forms Designer 的确将一个由设计器生成的组件集合插入到 Windows 窗体中,该集合被恰当地称为 components:
public class AddNewEmployeeForm : System.Windows.Forms.Form { ... /// /// Required designer variable. /// private System.ComponentModel.Container components = null; ... }
components 管理一个组件列表,该列表中的组件利用非托管资源,并且需要在宿主窗体被处置后处置这些资源。System.Windows.Forms.Timer 就是这样的组件,它依赖于非托管 Win32 系统计时器(详细讨论超出了本文范围,但可以在 Chris Sells 的著作 Windows Forms Programming in C# 的 Chapter 9 中找到)。因为 components 集合是由设计器管理的,并且因为我们的自定义验证组件不依赖于非托管资源,所以我们不能使用这些组件来进行需要的枚举。相反,我们必须创建自己的有保证的、强类型的 BaseValidators 集合。手动创建此类集合,尤其是强类型集合,可能是一件费时费力的苦差事。在上述情形下,我建议使用 CollectionGen (http://sellsbrothers.com/tools/#collectiongen),这是由 Chris Sells' et al 创建的 Visual Studio .NET 自定义工具,可为您完成这一繁重的工作。CollectionGen 为所需的 BaseValidator 集合(称为 ValidatorCollection)产生以下代码:
[Serializable] public class ValidatorCollection : ICollection, IList, IEnumerable, ICloneable { // CollectionGen implementation ... }
或许得到的实现比我们解决特定问题所需的实现更为完整,但它所节省的数小时编码时间使我能够有空欣赏一些旧的 Knight Rider (http://www.imdb.com/title/tt0083437/) 情节。
ValidatorManager
遗憾的是,无论 Michael Knight 还是 K.I.T.T. 都不能为我们将 ValidatorCollection 合并到验证库中,因此我们需要关闭电视并自己动手完成。此时,我们确实有了可以枚举的 ValidatorCollection,但要保证它含有所有正在运行的验证程序的列表,我们需要实现一种机制,以便在运行时在 ValidatorCollection 中添加和删除所谓的验证程序。我们创建了 ValidationManager 以满足这一要求:
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 使用 _validators 哈希表来管理由一个或多个 ValidatorCollection 实例组成的列表,其中每个实例表示特定窗体上承载的一组验证程序。每个 ValidatorCollection 都与特定窗体相关联,并且包含一个或多个对该窗体所承载的 BaseValidators 的引用。关联是在 BaseValidator 向 ValidationManager 注册和注销自己时建立的,因为 Register 方法和 DeRegister 方法都需要对 BaseValidator 以及承载它的窗体的引用。特定窗体的 ValidatorCollection 可以通过向 GetValidators 传递一个窗体引用而检索到。整个实现都是静态的(共享的),以保证内存中的访问并且简化客户端代码和 ValidatorManager 实例管理。
问题的另一面:更新 BaseValidator
Register 和 DeRegister 需要在某个地方进行调用以使其全部有效,而这个地方就是 BaseValidator,因为此逻辑是所有验证程序所共有的。由于 BaseValidator 与其宿主窗体同生共死,因此需要将对 Register 和 DeRegister 的调用与宿主窗体的生存期进行同步,具体说来,这是通过处理宿主窗体的 Load 和 Closed 事件实现的:
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); } ... }
下一步是将这些事件处理程序挂钩到 Load 和 Closed 事件。我们需要的窗体是 BaseValidator 的 ControlToValidate 的宿主窗体,并且因为 ControlToValidate 的类型是 Control,我们可以调用它的 FindForm 方法来检索宿主窗体。遗憾的是,我们不能从 BaseValidator 的构造函数中调用 FindForm,因为它的 ControlToValidate 在那时可能尚未被分配一个窗体。这是 Windows Form Designer 使用 InitializeComponent 来存储那些构建窗体并向父容器分配控件的代码的结果:
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); ... }
正如您所看到的,控件实例的创建时间远远早于它被分配给窗体的时间,后者又在关联的验证程序之后,这使得对 FindForm 的调用毫无用处。在此情况下,您可以求助于 System.ComponentModel.ISupportInitialize,它通过所定义的两个方法(BeginInit 和 EndInit)来解决与此类似的初始化依赖问题。Windows Forms Designer 使用反射来确定组件是否实现了 ISupportInitialize,如果是,则将对 BeginInit 和 EndInit 的调用都插入到 InitializeComponent 中,分别在窗体初始化之前和之后。因为能够保证 EndInit 在已经为 BaseValidator 的 ControlToValidate 分配一个父控件之后调用,并且由此能够从 FindForm 返回一个窗体,所以这就是我们应该向 Load 和 Closed 事件进行注册的地方。下面的代码说明了实现方法:
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 ... }
更新后的 InitializeComponent 如下所示:
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(); }
您可能想知道我为什么没有分别部署对 ISupportInitialize.EndInit 和 Dispose 的注册和注销调用。因为 ValidatorManager 管理一个或多个由父窗体进行哈希运算的 ValidatorCollection,所以我希望确保每个 ValidatorCollection 都能在其关联窗体关闭时从 ValidatorManager 中删除,而不是等待进行垃圾回收。
枚举 ValidatorCollection
通过创建 ValidatorCollection、ValidatorManager 以及更新 BaseValidator,可以完成为启用需要的 BaseValidator 枚举所需的注册机制。图 2 显示了这些部分的结合方式的内部表示。

图 2 ValidatorManager、ValidatorCollection 和 BaseValidator 的内部表示
要充分利用更新后的设计,我们只需要对 OK 按钮的 Click 事件处理程序进行简单更新:
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; }
由于我们不必随着向窗体中添加更多的验证程序而编写越来越多的代码,因此得到的代码比我们最初编写的代码要精练得多,并且支持可伸缩性。这回,我们的代码可不是我母亲所能写出来的。
声明性窗体范围验证:FormValidator
如果目标是编写尽可能少的代码,那么我们可以通过将该解决方案重构为可重用性更好的模型将其进一步简化。ASP.NET 本质上使用 System.Web.UI.Page(所有 ASP.NET 代码隐藏页都派生于该类型)完成了这项工作。具体说来,Page 实现了下列面向验证的成员:
public class Page : TemplateControl, IHttpHandler { ... public virtual void Validate(); public bool IsValid { get; } public ValidatorCollection Validators { get; } ... }
我们已经有了 ValidatorCollection(这样命名是为了保持一致),而使用 Validate 和 IsValid 的结果与我们刚刚实现的基于窗体范围枚举的验证逻辑等效。遗憾的是,尽管 System.Windows.Forms.Form 实现了 Validate,但它与我们从自定义库中利用的 Windows 窗体本机验证相联系,而不是与集成相联系。因此,继续谈论本文章系列中的主要话题之一是有意义的,即将适当的逻辑重新部署到开发人员可根据需要拖放到其窗体上的可重用组件中。用于验证窗体的组件只能称为 FormValidator,它实现了 Validate 和 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(); } } }
除了实现 Validate 和 IsValid 以外,FormValidator 还实现了 HostingForm 属性。因为与控件从其 Parent 属性或 FindForm 方法中确定宿主窗体不同,组件本身无法确定自己的宿主窗体,所以我们需要采取一点设计时技巧以实现同样的目标。这一技巧显示在 HostingForm 属性中。魔术师永远不会披露他的戏法,但我不是魔术师,而这也不是我的戏法,所以请自由地深入研究这一技术,并请参阅 Chris Sells' book 的 Chapter 9。在重新生成 CustomValidation 项目并将 FormValidator 组件添加到 Toolbox 后,我们可以简单地将该组件拖动到窗体上以便使用,如图 3 所示。

图 3 使用 FormValidator 组件
借助于 FormValidator,OK 按钮的 Click 事件处理程序被简化为三行代码:
private void btnOK_Click(object sender, System.EventArgs e) { formValidator.Validate(); if( formValidator.IsValid ) DialogResult = DialogResult.OK; else MessageBox.Show("Form not valid."); }
图 4 显示了运行时的结果。

图 4 运行中的 FormValidator
尽管将客户端代码数量减少到三行已经不错了,但如能减少到零行代码可能会更好,尤其是在实现完全声明性窗体范围验证时。要达到此目标,FormValidator 需要实现自身与上述三行代码对应的版本,并且在适当的时刻(当窗体的 AcceptButton 被单击时)为我们执行该版本。窗体的 AcceptButton 和 CancelButton 都可以在设计时从 Property Browser 中设置,如图 5 所示。

图 5 指定窗体的 AcceptButton 和 CancelButton
这表示当用户在窗体上按 Enter 键时,指定的 AcceptButton 被单击;而当用户按 ESC 键时,指定的 CancelButton 被单击。FormValidator 需要确定其宿主窗体的 AcceptButton,然后处理该按钮的 Click 事件,该事件取决于从 InitializeComponent 内部设置的 AcceptButton。因此,我们必须重新实现 ISupportInitialize,如下所示:
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."); } ... }
按照 Tab 键顺序验证
对于用户还会有用的一点是以可视方式处理验证的顺序。当前,FormValidator 按照可视顺序选择第一个无效控件而不是第一个控件,这与 Tab 键顺序所指定的一样。图 6 显示了 Add New Employee 窗体正确的 Tab 键顺序。

图 6 指定 Tab 顺序
通过按 Tab 键顺序进行验证,用户可以在窗体上从上到下来纠正无效字段,这要比看上去随机的方法更直观一些。要确保验证按 Tab 键顺序进行,必须按如下方式更新 FormValidator:
[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(); } }
图 7 通过将焦点放在按 Tab 键顺序的第一个无效控件上,显示了结果。

图 7. 将焦点放在按 Tab 键顺序的第一个无效控件(即 Date of Birth)上。
我们所处的位置
在本期中,我们在第一期建立的针对每个控件的验证的基础上,通过 FormValidator 提供了窗体范围的验证。根据您使用模式对话框的方式,FormValidator 可支持完全声明性窗体范围验证。虽然如此,最终我们生成了两个极端的验证范围:针对每个控件 和窗体范围。然而,Windows 窗体可能包含带有数个选项卡的选项卡控件,其中每个选项卡松散相关或完全不相关,并且每个选项卡都需要其自身的验证。这方面的例子有 Windows 桌面属性对话框,它在每个属性选项卡上都使用 Apply 按钮。在上述方案中,容器特有的验证会更有意义。在验证主题文章系列中的下一期和最后一期中,我们将对该问题进行讨论。我们还将扩展验证组件库,使其能够通过基础实现和可扩展的设计显示验证错误摘要,从而允许进一步自定义摘要解决方案