代码改变世界

Windows Forms中通过自定义组件实现统一的数据验证(一)

2007-04-18 21:30  Anders Cui  阅读(8871)  评论(24编辑  收藏  举报

摘要

一直对WinForm中没有像WebForm中那样的验证控件耿耿于怀,这几天准备开发一套类似的控件。在网上找到大牛Michael Weinhardt的一个系列文章,写得非常棒,所以基本上按他的思路下来的。

在获取用户输入及后续的处理过程中,数据校验是关键的一步。本文将对Windows Forms中的校验机制进行探讨,分析如何通过开发自定义验证组件来提供更为高效的验证体验(类似于ASP.NET中的验证控件)。

Windows Forms 验证机制介绍

简单地说,验证是对数据进行处理前确保其完整和正确的过程。验证可以实现在数据层和业务规则层,而应当在表现层进行前端的”保护”。开发人员通常在UI中为用户提供友好的、可交互的验证体验,而要避免在N层应用程序中进行不必要的网络间往返验证。验证包含数据类型、范围或业务规则等类型,看下面这个简单的例子:

<!--[if !vml]-->
<!--[endif]-->

这个窗体中需要进行下列验证:

  • Name,Date of Birth和Phone Number为必填项
  • Date of Birth必须为正确的日期值
  • Phone Number必须为正确的格式
  • 新添加的雇员必须年满18岁(杜绝童工)

要完成这些验证需要一个合适的机制,Windows Forms已经提供了一种,内置在每个控件中。要使控件支持验证,须将它的CausesValidation 属性设置为true,这也是所有控件的默认值。如果控件的CausesValidation 属性设置为true,那么在它将焦点转移到另一个控件(并且它的CausesValidation也为true)时会触发Validating 事件。因此,我们可以处理控件的Validating事件,在这里实现验证逻辑,像下面这样:

private void txtName_Validating(object sender, CancelEventArgs e)
{
    
if (txtName.Text.Trim().Length == 0)
    {
        e.Cancel 
= true;
        
return;
    }
}

Validating 事件提供了CancelEventArgs 类型的参数,它的Cancel属性使我们可以指定控件的值是否有效。如果Cancel为true(即是无效的),焦点仍然停留在无效的控件中;如果Cancel值为false(即通过了验证),则会触发Validated事件,焦点也会转移到新的控件。

现在,责任落到了我们开发人员这边,要以可视化的方式通知用户数据是否有效,也许你想到的是状态栏,这种方式存在两个问题:

 

  • 状态栏只能每次显式一条错误信息,即使窗体包含多个无效的控件输入;
  • 状态栏离输入控件”很远”,很难确切指明哪个控件出现了错误。
据传闻,微软曾做过这么一个可用性研究:人们坐在椅子上运行一个程序,状态栏给出一个通知信息叫他们往椅子底下看,这样就可以得到50美元奖金。但在测试期间,竟没有任何人能拿走这50美元!

 

此时,ErrorProvider组件是更好的选择:


ErrorProvider组件的用法非常简单,此处不再赘述,Validating事件的代码如下:

if (txtName.Text.Trim().Length == 0)
{
    errorProvider1.SetError(txtName, 
"Name is required.");
    e.Cancel 
= true;
    
return;
}

errorProvider1.SetError(txtName, 
string.Empty);

CausesValidationValidatingErrorProvider提供了控件级验证的基础机制,我们可以用它们对控件逐一进行验证。

窗体级验证

ValidatingErrorProvider这对组合是一个不错的解决方案,可以在用户输入数据的时候进行验证。不幸的是,这种方法可能会使得我们无法进行窗体级的验证,而这在用户点击OK按钮提交数据时显然是必要的,因为用户在点击OK按钮前,有些控件可能未曾获得过焦点,它们的控件级验证代码也就不起作用了。先看看窗体级验证的代码:

foreach (Control ctrl in this.Controls)
{
    ctrl.Focus();

    
if (!Validate())
    {
        
this.DialogResult = DialogResult.None;
        
return;
    }
}

但Cancel按钮就不需要实现窗体级的验证了,它的工作往往是简单地将窗体关闭。但是现在,如果当前拥有焦点的控件数据是无效的,Cancel按钮将不能点击,因为Cancel按钮的CausesValidation属性默认为true,焦点会一直停留在无效的控件上。我们只要将Cancel按钮的CausesValidation属性设置为false就好了。 

注意:这里的窗体应当是模式窗体,否则即使CausesValidation属性设置为false,也不能点击。

至此,使用数十行代码,我们的AddEmployee窗体就可以支持基本的验证了。

编程式验证 vs. 声明式验证

从生产力的角度来看,上面的解决方案有一个根本的问题:如果一个程序包含多个窗体,而每个窗体又包含多个控件,那么将需要大量的用于验证的代码。这些代码增大了UI的复杂性,使得程序难以维护,显式是应当避免的。一种方法是将那些通用的验证逻辑抽象为可重用的类型。有了这样的类型,还仅仅是第一步,它仍需要编写代码。{TODO}我们需要这样的解决方案:它具有Windows Forms UI的特点,因此Windows Forms组件或控件是我们不错的选择。以这种方式封装后,开发人员的工作就变成从工具箱上拖一个组件或控件放到窗体上,通过诸如属性浏览器(Property Browser)这样的设计期特性来配置它,然后让Windows Forms设计器帮我们将这些配置转换为代码,这些代码会出现在InitializeComponent方法中。这样原来的编程式(programmatic)体验变成了声明式(declarative)体验,而后者往往意味着高效。

添加设计期支持

第一步是添加设计期的支持,如果我们的实现不需要UI支持,可以从三种设计期组件继承:System.ComponentModel.ComponentSystem.Windows.Forms.ControlSystem.Windows.Forms.UserControl. Component,否则可以继承ControlUserControlControlUserControl的不同之处在于其呈现的方式,前者需要编写代码来呈现它,而后者则通过其它控件或组件来呈现它。我们在前面使用的验证代码没有绘制任何内容,而是借助于ErrorProvider来提示用户。因此,Component是我们最合适的选择。

Imitation Is the Sincerest Form of Flattery

下一步是要确定我们需要哪些种类的验证组件,可以参考一下ASP.NET中验证控件的实现机制。这样能保持一定的一致性,而且也不需要”重新发明轮子”了。这样那些ASP.NET的开发人员也更容易上手。ASP.NET现在提供了如下的验证控件:

验证控件

描述

RequiredFieldValidator

计算输入控件的值以确保用户输入值。

RegularExpressionValidator

计算输入控件的值,以确定该值是否与某个正则表达式所定义的模式相匹配。

CompareValidator

将由用户输入到输入控件的值与输入到其他输入控件的值或常数值进行比较。

RangeValidator

检查输入控件的值是否在指定的值范围内。

CustomValidator

对输入控件执行用户定义的验证。

同时我们还要考虑可扩展性,开发人员在必要的时候可以较为容易地开发自定义的验证组件。最后,这个实现应当利用Windows Forms中已有的验证机制(前面提及的部分)。

引入RequiredFieldValidator

有了上面的设计思路,现在要来点真的了。让我们从最简单的验证情形开始:RequiredFieldValidator。

建立一个Component类,名称为RequiredFieldValidator,其接口应当与ASP.NET中的对应类相同:

public partial class RequiredFieldValidator : Component
{
    
string ControlToValidate { getset;}
    
string ErrorMessage { getset;}
    
string InitialValue { getset;}
    
bool IsValid { getset;}
    
void Validate();
}

下面是每个成员的含义:

成员

描述

ControlToValidate

指定要验证的控件

ErrorMessage

控件未通过验证时显式的信息。

InitialValue

某些情况下,控件的默认值用作提示,如”请选择种类”,这时必填项意味着必须与默认值不同。此时用InitialValue。

IsValid

在调用Validate方法后报告控件的数据是否有效,默认为true。

Validate

验证指定控件的值,并设置IsValid。


在ASP.NET中,ControlToValidate是字符串类型的,这种间接的做法在基于请求、无状态的Web应用程序中是必要的。但在Windows Forms中我们则不必这么做,我们可以直接引用控件。同时,我们要在内部使用ErrorProvider组件,所以为其添加一个Icon属性:

public partial class RequiredFieldValidator : Component
{
    …
    Control ControlToValidate { 
getset;}
    Icon Icon { 
getset;}
    …
}

好,来看看具体的实现代码:

public partial class RequiredFieldValidator : Component
    
{
        
Private Fields

        
Constructors

        
Public Properties

        
public void Validate()
        
{
            
if (controlToValidate == null)
            
{
                isValid 
= true;
                
return;
            }


            
string controlValue = controlToValidate.Text.Trim();
            
string _initValue;
            
if (initialValue == null)
            
{
                _initValue 
= string.Empty;
            }

            
else
            
{
                _initValue 
= initialValue.Trim();
            }

            isValid 
= (controlValue != _initValue);

            
if (isValid)
            
{
                errorProvider.SetError(controlToValidate, 
string.Empty);
            }

            
else
            
{
                errorProvider.SetError(controlToValidate, errorMessage);
            }

        }


        
private void controlToValidate_Validating(object sender, CancelEventArgs e)
        
{
            Validate();
        }

    }

 

这种实现的关键在于如何挂接ControlValidate控件的Validating事件,这种做法与前面的控件级验证相一致,还有一个额外的好处,这里的ControlToValidate_Validating方法中,没有设置CancelEventArgs参数的Cancel属性,这样就不会把用户困在一个控件中。

组件的验证功能已经实现了,同时还为其添加了设计期支持。最终实现还提供了其它一些设计期特性:

  • <!--[if !supportLists]-->指定了在属性浏览器中设置ControlToValidate时可以选择的控件种类;
  • 在属性浏览器中隐藏了IsValid属性,因为它是运行时的属性。

编译,然后将组件添加到工具箱。

让我们回到前面的AddEmployee窗体,现在不再需要处理Validating事件了,只要拖3个组件到窗体,然后为它们设置属性。

<!--[if !vml]-->
<!--[endif]-->

其中Phone Number域的验证组件的InitialValue为”Your number here.”。怎么样,是不是很high?

BaseValidator:分而治之

实现了RequiredFieldValidator后,其它类型的验证组件应当比较容易实现了。先别急,可没你想的那么简单。RequiredFieldValidator类把特定的”必填”逻辑和其它对每个验证组件都适用的通用逻辑耦合在一起了。这种情况下,应当把RequiredFieldValidator分解为两个类型:BaseValidator和减肥后的RequiredFieldValidator。

abstract class BaseValidator : Component 
{
    
    
void Validate() 
    {
        
        _isValid 
= EvaluateIsValid();
        
    }
    
protected abstract bool EvaluateIsValid();
}

这样定义的效果是,BaseValidator必须通过继承后才能使用,而EvaluateIsValid则必须实现。Validate方法通过EvaluateIsValid方法来设置IsValid。这种技术也应用在了ASP.NET的验证控件上。

BaseValidator实现后,需要对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中效果不错。下面会看到,对于其它类型的验证组件同样很好,它们是:

  • <!--[if !supportLists]-->RegularExpressionValidator
  • CustomValidator
  • CompareValidator
  • RangeValidator

现在把它们一一实现。

RegularExpressionValidator

正则表达式是一种强大的文本模式匹配技术。如果文本域需要一定的模式,正则表达式无疑是很好的选择。

using System.Text.RegularExpressions;

[ToolboxBitmap(
typeof(RegularExpressionValidator), "RegularExpressionValidator.ico")]
class RegularExpressionValidator : BaseValidator 
{
    
    
string ValidationExpression {}
    
protected override bool EvaluateIsValid() 
    {
        
// Don't validate if empty
        if( ControlToValidate.Text.Trim() == "" ) return true;
        
// Successful if match matches the entire text of ControlToValidate
        string field = ControlToValidate.Text.Trim();
        
return Regex.IsMatch(field, _validationExpression.Trim());  
    }
}

在设计时,开发人员可以通过属性浏览器提供用于验证的正则表达式。

CustomValidator

人生在世,不如意者十有八九。我们定义的验证组件不可能解决所有问题,尤其是面对复杂的业务规则的时候。这时只能编写自定义代码,CustomValidator 允许我们编写这些自定义代码,同时仍能与其它的验证组件保持一致,这在窗体级的统一验证过程中很重要。CustomValidator 提供了Validating事件和ValidatingCancelEventArgs:

处理CustomValidator的Validating事件时,只需在属性浏览器中双击:

然后,只需添加合适的验证逻辑,以确保新增的雇员不小于18岁:

private void customValidator1_Validating(object sender, CustomValidator.ValidatingCancelEventArgs e)
{
    DateTime birth;
    
bool isDate = DateTime.TryParse(txtBirth.Text, out birth);
    
if (isDate)
    {
        DateTime legal 
= DateTime.Now.AddYears(-18);
        e.Valid 
= (birth <= legal);
    }
    
else
    {
        e.Valid 
= false;
    }
}

 

如果小于18岁,就会提示用户:

 

BaseCompareValidator

到目前为止,我们的组件只能处理单个文本域的值。但在某些情况下,验证过程可能涉及多个文本域或值,比如确保文本域的值在两个值之间(RangeValidator);或比较两个文本域的值是否相等(CompareValidator)。不管哪种情况,我们都需要考虑类型检查、转换和比较等过程。这个功能应当封装在一个新的类型中:BaseCompareValidator,而RangeValidator和CompareValidator则继承自它。

ValidationDataType是一个自定义枚举类型,在何种数据类型下进行比较验证。

RangeValidator

如果需要确保控件的输入值在指定的范围内,RangeValidator 可以满足需要。它需要开发人员指定最大值和最小值,还有输入值的数据类型。

<!--[if !vml]-->
<!--[endif]-->

CompareValidator

最后来看看CompareValidator,它用来进行控件的等值测试,可以与另一个控件的值或者指定的值进行比较。Operator属性指定了比较操作的类型,ControlToCompare和 ValueToCompare则指定了要比较的控件和指定值。如果Operator属性为DataTypeCheck,则还可以判断控件的值是否为指定类型。

<!--[if !vml]-->
<!--[endif]-->

完整的自定义验证组件结构




我们身在何处

首先我们对Windows Forms中的校验机制进行了探讨,然后将这些验证逻辑封装到了几个支持设计时操作的组件,通过开发自定义验证组件来提供更为高效的验证体验(类似于ASP.NET中的验证控件)。但目前还仅限于控件级的验证。下一篇文章中将讨论如何进行窗体级的验证,届时ValidationSummary组件也会闪亮登场。

示例代码下载:CustomValidatorSample.rar

参考:
1. Extending Windows Forms with a Custom Validation Component Library. By Michael Weinhardt
2. Windows Forms Programming in C#. By Chris Sells