使用自定义验证组件库扩展 Windows 窗体
摘要:数据验证是确保正常的数据捕获以及后续处理和报告的关键步骤。本文介绍了 Windows 窗体固有的程序验证基础结构,并以此为基础开发了用于提供更高效验证功能的自定义验证组件库,该验证功能与使用 ASP.NET 的验证控件相似。
下载 winforms03162004_sample.msi 示例文件。
本页内容
引言
Windows 窗体验证的主要功能
程序验证与声明性验证
建立设计时支持
模仿是最真诚的恭维
必需字段验证程序简介
BaseValidator:分治法
一个放便士,一个放英镑
已完成的自定义验证基础结构
结论
其他自定义验证解决方案
C# 与 Visual Basic .NET
Genghis
致谢
参考资料
引言
大家都把我称作怪人,我最喜欢的影片之一是 Amazon Women on the Moon,该片模仿了二十世纪五十年代的 B 级科幻电影,风格同 The Kentucky Fried Movie 一样,由几个喜剧短片组成。其中的一个短片“两份身份证明”,讲述了 Karen 和 Jerry(分别由 Rosanna Arquette 和 Steve Guttenberg 扮演)在他们初次相会的晚上发生的故事。故事从 Jerry 到达 Karen 的公寓开始。经过几分钟轻松友好的谈话后,Karen 突然要求 Jerry 出示两份身份证明-一张信用卡,一份有效的驾驶执照。Karen 通过身份证明对 Jerry 进行约会检查,正是这种突然的变化构成了该喜剧的主线。遗憾的是,Jerry 没有通过身份检查,Karen 因此拒绝了此次约会。除了滑稽可笑以外,这个故事还具有现实意义,Windows 窗体开发人员从中可以清楚地认识到:应始终对数据进行验证,以免出现意外情况,就像对剧中的 Steve Guttenberg 进行约会检查一样。相反,成功的数据验证就像对剧中的 Rosanna Arquette 进行约会检查一样,这对于该短剧的作者来说会是一件相当愉快的事情。
Windows 窗体验证的主要功能
简单地说,验证是指在进行后续处理或存储之前,确保数据的完整性和准确性的过程。尽管验证在数据层和业务规则应用程序层中都可以实现,但应将其包含在表示层中,以构成前沿验证防御。利用 UI,开发人员通常可以为最终用户构造一个更具人性化、响应性更高并提供更多信息的验证过程,同时还可以避免出现类似于跨 N 层应用程序进行不必要的双向网络通信这样的问题。验证可以包含类型、范围和业务规则检查,图 1 中的窗体可以应用所有这些类型的验证。
图 1. 要求验证的 Add New Employee 窗体
该窗体需要验证以下内容:
• 必须输入“姓名”、“出生日期”和“电话号码”
• “出生日期”必须是有效的日期/时间值
• “电话号码”必须符合澳大利亚电话号码格式:(xx) xxxx xxxx
• 新员工的年龄至少为 18 周岁
实现此验证需要一个相应的基础结构,Windows 窗体提供了该基础结构,并将其直接内置于每个控件中。为指示控件支持验证,将控件 CausesValidation 的属性设置为 True,即所有控件的默认值。 如果某个控件的 CausesValidation 属性设置为 True,则当焦点切换到另一个 CausesValidation 值也设置为 True 的控件时,将引发前一个控件的 Validating 事件。随后,应处理 Validating 以实现验证逻辑,如确保提供“姓名”。
void txtName_Validating(object sender, CancelEventArgs e) {
// 要求输入姓名
if(txtName.Text.Trim() == "" ) {
e.Cancel = true;
return;
}
}
Validating 提供了一个 CancelEventArgs 参数,通过设置该参数的 Cancel 属性可以指示该字段是否有效。如果 Cancel 为 True(无效字段),则焦点将停留在无效控件上。如果设置为默认值 False(有效字段),则将引发 Validated 事件,同时焦点将切换到新控件。图 2 演示了此过程。
图 2. Windows 窗体验证过程
您所要做的是以可视方式通知用户其输入数据的有效性,直觉告诉我们使用状态栏实现此功能比较合适。从可视化角度而言,该技术存在两个问题:
1.
状态栏只能显示一个错误,而一个窗体可能包含多个无效控件。
2.
由于状态栏通常位于窗体底部,因此很难确定错误消息究竟针对哪个控件。
经证实,在针对一个或更多控件提供错误通知方面,ErrorProvider 组件是一个更出色的机制。该组件组合使用了图标和工具提示向用户发出错误通知,并在相关控件的旁边显示相应的消息,如图 3 所示。
图 3. ErrorProvider 演示
启用该组件很简单,只需将 ErrorProvider 组件拖到一个窗体并配置其图标、闪烁速率和闪烁样式,然后即可将 ErrorProvider 合并到验证代码:
void txtName_Validating(object sender, CancelEventArgs e) {
// 需要输入姓名
if(txtName.Text.Trim() == "" ) {
errorProvider.SetError(txtName "Name is required");
e.Cancel = true;
return;
}
// 姓名有效
errorProvider.SetError(txtName, "");
}
CausesValidation、Validating 和 ErrorProvider 提供了实现按控件验证的基础结构,可将其实现方式重复用于其他控件,如 txtDateOfBirth 和 txtPhoneNumber:
void txtDateOfBirth_Validating(object sender, CancelEventArgs e) {
DateTime dob;
// 需要输入 DOB,且 DOB 必须是有效日期
if( !DateTime.TryParse(txtDateOfBirth.Text, out dob) ) {
errorProvider.SetError(txtDateOfBirth, "Must be a date/time value");
e.Cancel = true;
return;
}
// 年龄至少为十八周岁
if( dob > DateTime.Now.AddYears(-18) ) {
errorProvider.SetError(txtDateOfBirth, "Must be 18 or older");
e.Cancel = true;
return;
}
// DOB 有效
errorProvider.SetError(txtDateOfBirth, "");
}
void txtPhoneNumber_Validating(object sender, CancelEventArgs e) {
// 需要输入电话号码,且格式必须符合澳大利亚电话号码格式
Regex re = new Regex(@"^\(\d{2}\) \d{4} \d{4}$");
if( !re.IsMatch(txtPhoneNumber.Text) ) {
errorProvider.SetError(txtPhoneNumber, "Must be (xx) xxxx xxxx");
e.Cancel = true;
return;
}
// 电话号码有效
errorProvider.SetError(txtPhoneNumber, "");
}
窗体范围的验证
Validating 事件和 ErrorProvider 组件的组合提供了一个优秀的解决方案,可以在需要时(即当用户输入数据时)动态验证每个控件。遗憾的是,对 Validating 事件的依赖使该解决方案无法自动扩展为支持窗体范围的验证(当用户单击 OK 按钮完成数据输入时,需要此验证)。单击 OK 按钮前,一个或更多个控件可能没有焦点,因此不引发其 Validating 事件。窗体范围的验证通过手动调用捆绑在每个 Validating 事件中的验证逻辑来实现,方法是枚举窗体中的所有控件,为每个控件设置焦点,然后调用该窗体的 Validate 方法,如下所示:
void btnOK_Click(object sender, System.EventArgs e) {
foreach( Control control in Controls ) {
// 在控件上设置焦点
control.Focus();
// 验证导致引发控件的验证事件,
// 如果 CausesValidation 为 True
if( !Validate() ) {
DialogResult = DialogResult.None;
return;
}
}
}
Cancel 按钮不需要实现窗体范围的验证,因为它只用于关闭窗体。如果用户当前所在的控件无效,则用户将无法单击 Cancel 按钮,原因是 Cancel 按钮的 CausesValidation 属性在默认情况下也设置为 True,因此焦点被保留。将 Cancel 按钮的 CausesValidation 设置为 False,可以轻松地避免该情况,从而避免在焦点已从中转移的控件中引发 Validating,如图 4 所示。
图 4. 阻止验证
Add New Employee 窗体中大约包含 60 行代码,它支持基本的验证功能。
程序验证与声明性验证
从工作效率的角度来看,该解决方案存在一个根本性的问题,即大型应用程序通常包含多个窗体,每个窗体通常比本文的小程序示例包含更多的控件,因此需要更多的验证代码。在 UI 日益复杂的情况下编写越来越多的代码是一项不具伸缩性的技术,因此应尽可能地避免。解决方案之一是标识通用的验证逻辑,并将其封装到可重复使用的类型。这有助于减少用于创建和配置所需的验证对象实例的客户端代码。然而,尽管向正确的方向迈进了一步,该解决方案仍需要编写程序代码。但它提供了一种 Windows 窗体 UI 通用的模式-代表 Windows 窗体执行大多数任务的类型(大多数客户端代码用于配置这些类型的属性,这对于 Windows 窗体组件或控件来讲是理想的选择方案。以这种方式封装代码,开发人员的工作转换为将组件或控件从 Toolbox 拖放到窗体中,从设计时功能(如“属性浏览器”)中配置它,然后让 Windows 窗体设计器实施将设计时目标转换成保留在 InitializeComponent 中的代码这项工作量繁重的任务。这样,编程性工作转换为声明性工作,其中声明性是高效率的同义词。这对 Windows 窗体验证来说是一种理想的情况。
建立设计时支持
第一步是建立设计时支持,这要求实现派生自三种类型的设计时组件之一:System.ComponentModel.Component、System.Windows.Forms.Control 或 System.Windows.Forms.UserControl。如果逻辑不需要 UI,则 Component 是正确的选择;否则,Control 或 UserControl 是正确的选择。Control 与 UserControl 之间的区别在于 UI 的呈现方式。前者由您编写的自定义绘图代码呈现,后者则由其他控件和组件呈现。现有验证代码不进行自身绘制,因为它将该任务传递给了 ErrorProvider。因此,Component 成为封装验证类的最佳选择。
模仿是最真诚的恭维
下一步是弄清所需的验证程序类型。首先,应了解 ASP.NET 实现的验证程序。为什么是这样呢?本人是一致性的忠实推崇者,并且在我的前额刺有“不要多此一举”的字样(当然是反着刺上去的,所以在刷牙的时候能在镜子里面看到它)。因此,我认为主要目标是创建在公开的类型和成员方面与 ASP.NET 的验证程序保持一致的 Windows 窗体验证程序(如果该解决方案适用于 Windows 窗体)。除了在技术方面向 ASP.NET 小组表示敬意之外,该选择还可以增进应用不同开发模式的开发人员之间的了解。ASP.NET 当前提供了下表列出的验证程序。
验证控件 说明
RequiredFieldValidator
确保字段包含值
RegularExpressionValidator
使用正则表达式验证字段的格式
CompareValidator
对目标字段和其他字段或值进行等同性测试
RangeValidator
测试字段属于某种特殊的类型并/或在指定的范围内
CustomValidator
为复杂程度超过其他验证程序处理能力的自定义验证逻辑创建一个服务器端事件处理程序
此外,我认为另一个目标是具有可扩展性,以便当提供的验证程序不能满足要求时,开发人员只需进行少量的工作便能将他们自己的自定义验证程序并入基础结构中。最后,为减少工作量,实现应利用我们先前讨论的现有 Windows 窗体验证基础结构。
必需字段验证程序简介
明确以上目标之后,现在我们开始了解具体的操作细节。首先,我们将创建一个最简单的验证程序,即 RequiredFieldValidator。
Visual Studio .NET 解决方案
我们将需要一个 Visual Studio® .NET 解决方案来实现 RequiredFieldValidator。生成组件或控件库时,我倾向于采用“两个项目”方法,即将解决方案拆分为一个组件或控件项目和一个相应的测试工具项目,方法是:
1.
创建一个解决方案 (winforms03162004_CustomValidation)
2.
向解决方案 CustomValidationSample 中添加一个 Windows Forms Application 项目
3.
向解决方案 CustomValidation 中添加一个 Windows Forms Control 项目
4.
从 CustomValidation 项目中删除默认的 UserControl1
5.
添加一个名为 RequiredFieldValidator 的新类,并使其从 System.ComponentModel.Component 中派生
注意 由于这些验证组件不局限于 Whidbey,因此我使用了 Visual Studio .NET 2003 和 .NET Framework 1.1。
接口
保持一致性要求实现同样适用于 Windows 窗体的接口。这意味着实现由 ASP.NET RequiredFieldValidator 公开的相同成员:
class RequiredFieldValidator : Component {
string ControlToValidate {get; set;}
string ErrorMessage {get; set;}
string InitialValue {get; set;}
bool IsValid {get; set;}
void Validate();
}
下表详细描述了每个成员。
成员 说明
ControlToValidate
指定要验证的控件。
ErrorMessage
当 ControlToValidate 无效时显示的消息。默认值是 ""。
InitialValue
强制值并不一定意味着 "" 以外的值。某些情况下,默认的控件值可能用于提示,例如在 ListBox 控件中使用 "[Choose a value]"。这种情况下,所需的值必须不同于初始值 "[Choose a value]".。InitialValue 支持此项要求。默认值为 ""。
IsValid
调用 Validate 后,报告 ControlToValidate's 数据是否有效。同 ASP.NET 实现一样,当需要覆盖 Validate 时,可以从客户端代码设置 IsValid。默认值为 True。
Validate
验证 ControlToValidate's 值并将结果存储到 IsValid 中。
在 ASP.NET 中,ControlToValidate 是一个用于为服务器端控件实例的 ViewState 编制索引的字符串在处理 Web 应用程序的基于请求的无连接特性时需要这种间接的方法。由于在 Windows 窗体中不需要做同样的考虑,因此可以直接引用该控件。此外,由于 ErrorProvider 将在内部使用,因此我们将添加一个 Icon 属性以供开发人员进行配置。明确以上要求后,需要对接口进行如下调整:
class RequiredFieldValidator : Component {
...
Control ControlToValidate {get; set;}
Icon Icon {get; set;}
...
}
实现
以下是最终的实现:
class RequiredFieldValidator : Component {
// 专用字段
...
Control ControlToValidate
get { return _controlToValidate; }
set {
_controlToValidate = value;
// 运行时(即不从 VS.NET 中)关联 ControlToValidate 的
// Validating 事件
if( (_controlToValidate != null) && (!DesignMode) ) {
_controlToValidate.Validating +=
new CancelEventHandler(ControlToValidate_Validating);
}
}
}
string ErrorMessage {
get { return _errorMessage; }
set { _errorMessage = value; }
}
Icon Icon {
get { return _icon; }
set { _icon = value; }
}
string InitialValue {
get { return _initialValue; }
set { _initialValue = value; }
}
bool IsValid {
get { return _isValid; }
set { _isValid = value; }
}
void Validate() {
// 如果与初始值(该值不一定是空字符串)不同,
// 则有效
string controlValue = ControlToValidate.Text.Trim();
string initialValue;
if( _initialValue == null ) initialValue = "";
else initialValue = _initialValue.Trim();
_isValid = (controlValue != initialValue);
// 如果 ControlToValidate 无效,则显示错误消息
string errorMessage = "";
if( !_isValid ) {
errorMessage = _errorMessage;
_errorProvider.Icon = _icon;
}
_errorProvider.SetError(_controlToValidate, errorMessage);
}
private void ControlToValidate_Validating(
object sender,
CancelEventArgs e) {
// 无效时不取消,因为我们不希望在无效时
// 强制焦点保留在 ControlToValidate 上
Validate();
}
}
该实现的关键是如何将其关联到 ControlToValidate 的 Validating 事件:
Control ControlToValidate {
get {...}
set {
...
// 运行时(即不在 VS.NET 设计时)关联 ControlToValidate 的
// Validating 事件
if( (_controlToValidate != null) && (!DesignMode) ) {
_controlToValidate.Validating +=
new CancelEventHandler(ControlToValidate_Validating);
}
}
这使得能够在输入数据时动态执行所需的字段验证,如同标准的 Windows 窗体实现一样。此外,还获得了并不明显的额外好处。在初始实现中,焦点保留在控件中,直到该控件有效,即直到它的 Validating 事件的 CancelEventArgs.Cancel 属性设置为 False(即在该控件中捕获用户)。数据输入 UI 在任何地方都不应捕获用户,该设计思想在 ControlToValidate_Validating 中得到反映,即 ControlToValidate_Validating 作为其自身验证的引发器(而不是作为与基础验证结构集成的机制)来处理该事件。图 5 显示了已更新的进程。
图 5. 已更新的验证进程
设计时集成
除设计时方面以外,该实现的功能性方面是完整的。所有优秀的 Visual Studio .NET 设计时成员都使用各种功能指定它们的设计时行为。例如,RequiredFieldValidator 使用 ToolboxBitmapAttribute 指定在 Toolbox 和 Component Tray 中显示的自定义图标,并使用 CategoryAttribute 和 DescriptionAttribute 使 RequiredFieldValidator 的公共属性在“属性浏览器”中更具有描述性:
[ToolboxBitmap(
typeof(RequiredFieldValidator),
"RequiredFieldValidator.ico")]
class RequiredFieldValidator : Component {
...
[Category("Behaviour")]
[Description("Gets or sets the control to validate.")]
Control ControlToValidate {...}
[Category("Appearance")]
[Description("Gets or sets the text for the error message.")]
string ErrorMessage {...}
[Category("Appearance")]
[Description("Gets or sets the Icon to display ErrorMessage.")]
Icon Icon {...}
[Category("Behaviour")]
[Description("Gets or sets the default value to validate against.")]
string InitialValue {...}
}
完整的实现使用其他几个设计时功能(这些功能超出了本文介绍的范畴),包括:
• 指定从“属性浏览器”中可以为 ControlToValidate 选择的控件。 该实现允许使用 TextBox、ListBox、ComboBox 和 UserControl 控件
• 从“属性浏览器”中隐藏 IsValid,因为它是只限运行时属性(所有公共属性在设计时自动显示在 Property Browser 中的)。
要了解有关设计时的详细介绍,请参阅以下文章:
• Building Windows Forms Controls and Components with Rich Design-Time Features, Part 1
• Building Windows Forms Controls and Components with Rich Design-Time Features, Part 2
使用 RequiredFieldValidator
要使用 RequiredFieldValidator,首先应将其添加到 Toolbox,然后才能将它拖动到窗体并对其进行配置。为此,应执行以下操作:
1.
重新生成解决方案。
2.
在 Visual Studio .NET 中打开测试窗体。
3.
右键单击 Toolbox 并选择“添加选项卡”。
4.
创建一个名为 Custom Validation 的选项卡并选中它。
5.
右键单击 Toolbox 并选择“添加/删除项”。
6.
浏览到新生成的组件程序集 (CustomValidation.dll) 并选中它。
通过以上步骤可以在 Windows 窗体设计器中该使用组件,如图 6 所示。
图 6. 包含运行时属性的 RequiredFieldValidator 演示
我对 Add New Employee 窗体中的所有字段都应用了 RequiredFieldValidator,并包含有关用户可以在每个字段中输入的值类型的提示。图 7 显示它在运行时的工作方式。
图 7. 运行时的乐趣
BaseValidator:分治法
有了 RequiredFieldValidator,便可以轻松地实现其余的验证程序。但也许并不会如此轻松。RequiredFieldValidator 将特定的“必需”验证逻辑与每个验证程序需要实现的逻辑的余下主要部分(如指定要验证的控件)紧密关联在一起。这种情况下,一个适当的解决方案是将 RequiredFieldValidator 分成两个新类型,即 BaseValidator 和由其派生且比较简单的 RequiredFieldValidator。严格地说,从一致性角度考虑,我们还应创建 IValidator 接口并通过与 ASP.NET 相同的方式实现 BaseValidator。然而,该方法提供的可扩展性的程度超出了我们当前的需要。BaseValidator 实现与原始的 RequiredFieldValidator 相类似的验证逻辑的可重用部分:
abstract class BaseValidator : Component, IValidator {
Control ControlToValidate {...}
string ErrorMessage {...}
Icon Icon {...}
bool IsValid {...}
void Validate() {...}
private void ControlToValidate_Validating(...) {...}
}
尽管 IsValid 和 Validate 是所有验证程序所共有的,但它们在新基础结构中只是作为一种形式存在,从而将特定的验证留给了每个派生的验证程序。因此,BaseValidator 需要一种机制,通过该机制它能够“询问”派生的验证程序,它是否有效。这是通过一个额外的抽象方法 EvaluateIsValid 实现的。EvaluateIsValid 和 BaseValidator 都是抽象的,以确保每个派生的验证程序都知道它们能够回答这个问题。新添加部分要求一些重构,如下所示:
abstract class BaseValidator : Component {
...
void Validate() {
...
_isValid = EvaluateIsValid();
...
}
protected abstract bool EvaluateIsValid();
}
结果是 BaseValidator 只能由派生方法使用,因此必须实现 EvaluateIsValid。Validate 方法已经过更新,可以向下调用派生的 EvaluateIsValid 以查询其是否有效。该项技术还应用于 ASP.NET 控件,从而增强了所需的一致性。
RequiredFieldValidator:Redux
完成 BaseValidator 后,我们现在需要重构 RequiredFieldValidator,以实现其自身的功能,即从 BaseValidator 派生并在 EvaluateIsValid 和 InitialValue 中实现指定的必须字段验证位。本着以简代蘩的原则,编写了下面这个更简单的新 RequiredFieldValidator:
[ToolboxBitmap(
typeof(RequiredFieldValidator),
"RequiredFieldValidator.ico")]
class RequiredFieldValidator : BaseValidator {
string InitialValue {...}
protected override bool EvaluateIsValid() {
string controlValue = ControlToValidate.Text.Trim();
string initialValue;
if( _initialValue == null ) initialValue = "";
else initialValue = _initialValue.Trim();
return (controlValue != initialValue);
}
}
一个放便士,一个放英镑
在基类与派生类之间合理地分隔普通验证功能与特定验证功能使我们能够将注意力只集中在特定的验证上。这不但适用于 RequiredFieldValidator,而且对其余几个用于与 ASP.NET 保持一致而应实现的验证程序更为适用,这些验证程序包括:
• RegularExpressionValidator
• CustomValidator
• CompareValidator
• RangeValidator
让我们了解一下每个验证程序以及用于实现它们的逻辑。
RegularExpressionValidator
正则表达式是一个基于大量令牌而构建的功能强大的文本模式匹配技术。当字段需要特定形式的值时,正则表达式是优秀的字符串操作替代方法,尤其是对于重要的文本模式。例如,确保电话号码符合澳大利亚格式((xx) xxxx xxxx)可以简化为一个正则表达式:
^\(\d{2}\) \d{4} \d{4}$
其好处是为什么 ASP.NET 实现 RegularExpressionValidator,并且我们也会实现:
using System.Text.RegularExpressions;
...
[ToolboxBitmap(
typeof(RegularExpressionValidator),
"RegularExpressionValidator.ico")]
class RegularExpressionValidator : BaseValidator {
...
string ValidationExpression {...}
protected override bool EvaluateIsValid() {
// 如果为空,则不验证
if( ControlToValidate.Text.Trim() == "" ) return true;
// 如果与 ControlToValidate 的整个文本匹配,则成功
string field = ControlToValidate.Text.Trim();
return Regex.IsMatch(field, _validationExpression.Trim());
}
}
在设计时,开发人员可以通过属性浏览器提供验证表达式,如图 8 所示。
图 8. 指定验证的正则表达式
CustomValidator
有时,特定验证程序无法解决特定的验证问题,尤其是当复杂的业务规则为需要数据库访问时。这种情况下,可以编写自定义代码,且 CustomValidator 使我们能够编写该自定义代码并确保它与自定义验证基础结构集成在一起,这对于以后的窗体范围的验证是很重要的。CustomValidator 包含的 Validating 事件和 ValidatingCancelEventArgs 可以满足此要求:
[ToolboxBitmap(typeof(CustomValidator), "CustomValidator.ico")]
class CustomValidator : BaseValidator {
class ValidatingCancelEventArgs {...}
delegate void ValidatingEventHandler(object sender, ValidatingCancelEventArgs e);
event ValidatingEventHandler Validating;
void OnValidating(ValidatingCancelEventArgs e) {
if( Validating != null ) Validating(this, e);
}
protected override bool EvaluateIsValid() {
// 将验证进程传递至事件处理程序并等待响应
ValidatingCancelEventArgs args =
new ValidatingCancelEventArgs(false,ControlToValidate);
OnValidating(args);
return args.Valid;
}
}
要处理 CustomValidator 的 Validating 事件,在“属性浏览器”中双击该事件,如图 9 所示:
图 9. 为自定义的复杂验证创建验证处理程序
然后,只需添加相应的验证逻辑,以下示例中的验证逻辑确保新员工的年龄大于或等于 18 岁:
class AddNewEmployeeForm : Form
void cstmDOB_Validating(object sender, ValidatingCancelEventArgs e) {
try {
// 必须大于或等于 18 岁
DateTime dob = DateTime.Parse(((Control)e.ControlToValidate).Text);
DateTime legal = DateTime.Now.AddYears(-18);
e.Valid = ( dob < legal );
}
catch( Exception ex ) { e.Valid = false; }
}
注意,CustomValidator 根据 ASP.NET 的服务器端处理特性命名其等价事件 ServerValidate。我们之所以使用 Validating,是因为我们不需要做这种区分,且 BaseValidator 已经使用了 Validate 派生于的 Validate。
BaseCompareValidator
该验证程序只能处理一个字段。然而,在某些情况下,验证可能涉及多个字段和/或值、是否确保某个字段的值在最大/最小值范围内 (RangeValidator) 或将一个字段与另一个字段进行比较 (CompareValidator)。不论在哪种情况下,都需要进行额外的工作以执行类型检查、转换和比较,其中包括验证时必须参照的类型。该功能应封装在新类型 BaseCompareValidator 中,从该类型可以派生出 RangeValidator 和 CompareValidator 以节省某些额外的工作。BaseCompareValidator 自身派生自 BaseValidator,以获取基本的验证支持并实现四个新成员:
abstract class BaseCompareValidator : BaseValidator {
...
ValidationDataType Type {...}
protected TypeConverter TypeConverter {...}
protected bool CanConvert(string value) {...}
protected string Format(string value) {...}
}
ValidationDataType 是自定义枚举,用于指定支持比较和范围验证的类型:
enum ValidationDataType {
Currency,
Date,
Double,
Integer,
String
}
RangeValidator
在需要确定某个字段值是否在指定范围内的情况下,可以使用 RangeValidator。它允许开发人员输入最小值和最大值以及适用于所有相关值的类型。在以下示例中,RangeValidator 派生自 BaseCompareValidator:
[ToolboxBitmap(typeof(RangeValidator), "RangeValidator.ico")]
class RangeValidator : BaseCompareValidator {
...
string MinimumValue {...}
string MaximumValue {...}
protected override bool EvaluateIsValid() {
// 如果为空,则不验证,除非要求进行验证
// 验证并转换最大值
// 验证并转换最大值
// 检查最小值是否 <= 最大值
// 检查并转换 ControlToValue
// 是否最小值 <= 值 <= 最大值
}
}
“属性浏览器”用于配置范围和类型属性,如图 10 所示。
图 10. 配置 RangeValidator
CompareValidator
最后介绍的是 CompareValidator(将它放在最后介绍并不意味着它的重要性最低),CompareValidator 用于针对 ControlToValidate 以及其它控件或值执行等同性测试。控件或值的等同性测试分别由 ControlToCompare 或 ValueToCompare 指定:
[ToolboxBitmap(typeof(CompareValidator), "CompareValidator.ico")]
class CompareValidator : BaseCompareValidator {
...
Control ControlToCompare {...}
ValidationCompareOperator Operator {...}
string ValueToCompare {...}
protected override bool EvaluateIsValid() {
// 如果为空,则不验证,除非要求进行验证
// 检查提供的 ControlToCompare
// 检查提供的 ValueToCompare
// 验证并转换 CompareFrom
// 验证并转换 CompareTo
// 执行比较,如 ==, >, >=, <, <=, !=
}
}
Operator 定义在 ControlToValidate 和 ControlToCompare 或 ValueToCompare 中执行的等同性测试。Operator 由 ValidationCompareOperator 指定:
enum ValidationCompareOperator {
DataTypeCheck,
Equal,
GreaterThan,
GreaterThanEqual,
LessThan,
LessThanEqual,
NotEqual
}
正如您所知,还可以使用 CompareValidator 以验证 ControlToValidate 是否是特殊的数据类型(由 DataTypeCheck 值指定)。这是唯一一个既不需要 ControlToCompare 也不需要 ValueToCompare 的比较。图 11 显示如何配置 CompareValidator。
图 11. 配置 CompareValidator
已完成的自定义验证基础结构
至此,所有与 ASP.NET 等价的验证程序组件均已列出并予以了说明。每个实现都直接或间接地从 BaseValidator 派生以继承基本的、可重复使用的验证功能。此类继承创建了可隐式扩展的基础结构,其他开发人员可以像我们在本文中那样轻松地使用该基础结构。图 12 显示了该自定义验证解决方案以及开发人员可以在哪些情况下使用它。必须使用该自定义验证解决方案的情况包括:
• 创建新的普通验证程序
• 创建业务规则特定的验证程序
• 向一个现有的验证程序中添加额外的相关逻辑
图 12 显示了该设计为这些情况提供的可扩展支持。
图 12. 自定义验证的可扩展支持
结论
首先,我们介绍了内置于 .NET Framework 的 Windows 窗体中的验证框架。然后,我们将验证逻辑重新封装到多个设计时组件中,以将最初的程序验证进程转换为更具声明性、且效率更高的进程。在此过程中,我们基于 BaseValidator 创建了可扩展的验证基础结构,以实现 RequiredFieldValidator、RegularExpressionValidator、CustomValidator、CompareValidator 和 RangeValidator。该设计允许其他开发人员能够轻松地创建新验证程序,方法也是从 BaseValidator 派生以继承普通功能,并将实现问题的范围缩小至特定的验证。本文主要介绍了按控件验证,并且只对窗体范围的验证提供了说明。下一篇文章将在自定义验证的基础之上并入窗体范围的验证,介绍如何组合现有的验证程序并添加 ASP.NET 验证控件集合的另一个主要部分,即 ValidationSummary 控件。现在,我建议您看一看 Amazon Women on the Moon,仅仅为找些笑料。
其他自定义验证解决方案
我最初为 Genghis 创建验证组件要追溯到 2002 年下半年。在研究所参考的 MSDN 杂志中有关设计时的文章时,我发现了 Billy Hollis 在他(以及 Rocky)的 MSDN Online 专栏中提供的验证程序片段,其网址是 Adventures in Visual Basic .NET。强烈建议您阅读一下该文章,创建一个出色的 Windows 窗体验证方案,以代替我创建的那一个,该文章还深入介绍了如何在实现设计时的扩展属性问题。
C# 与 Visual Basic .NET
我前面写的两篇文章强调了文中编写的代码片段只适用于 C#,而该专栏的许多读者是 Visual Basic® .NET 开发人员。本人对 C# 偏爱有加,但同时也很乐于为 Visual Basic.NET 编写程序。我原本希望同时提供 Visual Basic .NET 和 C# 两种版本的代码实例,但由于时间方面的原因,未能将两者合并在一起并写入各自的专栏。但我不想排除两者中的任何一个,作为一种折衷方法,我将在撰写的每一篇文章中,在 Visual Basic .NET 和 C# 代码之间进行转换。这会使得第一个针对 Visual Basic .NET 的程序片段显示在两篇文章中(下一个验证程序片段将继续从此处开始的 C# 工作)。在此项工作的时间内,很乐意保持有关两种代码的交流。该解决方案是否满足需要?请给我发邮件并告诉我您的想法。
Genghis
一个更完整并与该解决方案稍有不同的解决方案当前可以在 Genghis 中获得,Genghis 是一个在 Microsoft Foundation Classes (MFC) 上服务于 .NET Windows 窗体开发人员的共享源代码集合。有趣的是,我在编写本文时,有些地方需要进行改进,但这些改进将在 Genghis 的下一版中反映出来。但是,如果您已经迫不及待,则可以随时下载本文,如果您有时间,请将错误或希望改进的地方发送给我。
致谢
本文受到了 Ron Green、Chris Sells 和 Amazon Women on the Moon 的启发。