WinForm中通过自定义组件实现统一的数据验证

摘要

一直对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 {  get set ;}
    
string  ErrorMessage {  get set ;}
    
string  InitialValue {  get set ;}
    
bool  IsValid {  get set ;}
    
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 { 
get set ;}
    Icon Icon { 
get set ;}
    …
}

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

 

 

这种实现的关键在于如何挂接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
备注:本文引用自http://www.cnblogs.com/anderslly/archive/2007/04/18/customvalidatorpart1.html

我们身在何处?

上一篇中,我们利用Windows Forms中的验证机制实现了一套组件,它们是可重用的,并且可以利用VS的窗体设计器,最终我们实现了控件级的验证。也就是说当用户在控件间转移时进行验证。

不幸的是,用户数据填写完毕进行提交时,我们无法保证他们能够填写过每个控件,当然也就没法验证所有控件了。这时窗体级的验证就很有必要了。好了,我们先来看看如何利用已有的组件以编程的方式进行窗体级的验证。

编程式的窗体级验证

最简单的方法是,在用户提交数据时,对控件逐一进行验证。每个验证组件都提供了Validate方法和IsValid属性,可以以此来判断是不是每个控件的输入都是有效的。备注:本文引用自http://www.cnblogs.com/anderslly/archive/2007/05/08/customvalidationlibrary2.html

private   void  btnOK_Click( object  sender, EventArgs e)
        
{
            
// 验证所有控件
            reqName.Validate();
            reqBirth.Validate();
            rngSpeed.Validate();
            reqCommence.Validate();

            
// 判断输入是否有效
            
if ((reqName.IsValid) && (reqBirth.IsValid) && (reqCommence.IsValid) &&
                (rngSpeed.IsValid))
            
{
                
this.DialogResult = DialogResult.OK;
            }

            
else
            
{
                MessageBox.Show(
"Form not valid.");
            }

        }

这种方法确实可以,却相当无趣,而且每当我们添加了新的验证组件,都要编写相应的代码。

可以看到,上面的代码出现了很多重复的ValidateIsValid,出现这种情况,我们往往可以考虑进行枚举风格的重构。Form窗体类没有实现像Controls这样的用于组件的可枚举集合属性,但Windows Forms窗体设计器却有一个设计器生成的组件集合,名称为components

private  System.ComponentModel.IContainer components  =   null ;

components管理那些需要使用非托管资源的组件,在其宿主窗体释放时也将它们释放。一个例子是System.Windows.Forms.Timer它使用了非托管的Win32系统的定时器。components集合是由窗体设计器管理的,而且我们的自定义组件没有使用非托管资源,所以我们不能使用components来进行枚举。我们需要自己去创建这样的集合类。

[Serializable]
public   class  ValidatorCollection : ICollection, IList, IEnumerable, ICloneable 
{
    
// CollectionGen implementation
    
}


ValidationManager

现在我们已经有了一个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);
    }

}


ValidationManager
使用_validators哈希表来管理一个或多个ValidationCollection,而每一个ValidationCollection则用于表示特定窗体上的验证组件集合。通过ValidationManagerRegisterDeRegister方法将组件注册和反注册至其宿主窗体。

 

另一方面,更新BaseValidator

RegisterDeRegister方法需要在合适的地方进行调用,而把这个调用过程放在BaseValidator中是再合适不过了,因为这个逻辑对于所有验证组件都是一样的。因为BaseValidator的创建与销毁与其宿主窗体息息相关,对RegisterDeRegister的调用还要与宿主窗体的生命期保持同步,尤其是通过处理窗体的LoadClosed事件:

BaseValidator Update


下一步是将这些事件处理函数与
LoadClosed事件“挂接”。我们需要的窗体是BaseValidatorControlToValidator的宿主窗体,ControlToValidator的类型为Control,我们可以调用它的FindForm方法来获取窗体。很不幸,我们不能在BaseValidator的构造函数内调用FindForm,此时ControlToValidate可能还没有设置窗体。这是窗体设计器使用InitializeComponeng来保存构造窗体的代码,然后将控件赋给窗体的结果。

 
正如你所见的,控件实例在赋给父窗体之前就已创建。这时,我们可以转向ISupportInitialize,可以帮我们解决上面的问题。

BaseValidator : ISupportInitialize


 

枚举ValidatiorCollection

 

创建了ValidatorCollectionValidatorManager,更新了BaseValidator,我们也完成了枚举所需要的注册机制。下图描述了其内部实现:



要利用更新后的设计,我们要做的只是简单地更新OK按钮的Click事件处理函数:


//  更好的验证
ValidatorCollection validators  =  ValidatorManager.GetValidators( this );
//  确保检查每个验证组件
foreach  (BaseValidator validator  in  validators)
{
    
if (!validator.IsValid)
    
{
        MessageBox.Show(
"Form not valid.");
        
return;
    }

}

DialogResult 
=  DialogResult.OK;

 

这段代码比前面的实现要优雅得多,可维护性也更好——即使向窗体添加验证组件,验证代码却无须修改。

声明式的窗体级验证

 

懒的程序员才是好的程序员,我们还想把代码变得更少。还是看看ASP.NET中的机制,Page类提供了如下验证相关的成员:


ValidatorCollection(如此命名也是为了一致性),ValidateIsValid的功能是在提交时实现的。不幸的是,尽管Form类实现了Validate,却不能满足我们的需要。我们还是继续前进,实现可重用的组件FormValidator。

ASP.NET Page Class


我们已经有了

 

利用FormValidatorOK按钮的Click事件处理函数减少到三行代码:


 

formValidator1.Validate();
if  (formValidator1.IsValid)  { DialogResult = DialogResult.OK; }
else   { MessageBox.Show("Form not valid."); }

 

将客户代码减少到三行代码已经很棒了,如果完全不写代码岂不更好?这需要将这三行代码移到FormValidator中,然后在合适的时候执行,这应当是窗体的AcceptButton点击的时候。

 

窗体的AcceptButtonCancelButton属性都可以在属性浏览器中进行设置。这实际上指定了:当用户按下回车键时,AcceptButton会被点击;当用户按下ESC键时,CancelButton会被点击。FormValidator需要确定所在窗体的AcceptButton,然后处理其Click事件。AcceptButton是在InitializeComponent中设置的,所以我们需要实现ISupportInitialize。

事情的真相

 

这种方法之所以有效,是因为我对使用FormValidator的窗体进行了设置,包括将其FormBorderStyle属性为FixedDialog,还设置了它的AcceptButtonCancelButton,同时AcceptButtonCancelButtonDialogResult分别为NoneCancel。其结果是按下Cancel按钮会关闭窗体,而我们需要处理AcceptButtonClick事件,这正是FormValidator做的事情。

 

这样我们就不需要写任何代码了。但这是假定你的窗体模型是验证窗体,在返回父窗体后处理收集的数据。但很多时候我们需要另一种方式:验证窗体,在返回父窗体前处理收集的数据(如添加、编辑记录后返回)。后面方法的问题是,自动验证导致AcceptButton会有两个事件处理函数,一个由开发人员创建,另一个由FormValidator创建。这两种方法都可以采用,我们添加了一个属性ValidateOnAccept,它来指定是否进行自动验证。

ValidateOnAccept默认为true,此外还添加了ErrorMessage属性,进一步提高组件的可定制性。

 

Tab顺序验证

 

另一个对用户有用的是验证的顺序。现在,FormValidator会选中Tab顺序指定的第一个无效的控件,而不是窗体中所看到第一个控件,下图是Add New Employee窗体中的TabIndex


Tab顺序验证可使用户按指定的顺序修复各个无效的输入,这要比随机的修复更直观些。下图演示了将焦点置于第一个无效的控件:


如果控件是文本框,则会选中文本框的文本。

我们身在何处

 

这次我们继续上次的脚步,利用上篇中的验证组件实现了窗体级的验证。取决于你使用模式对话框的方式,FormValidator可支持彻底的声明式验证体验。别高兴得太早,我们仅仅是做到了两点:控件和窗体验证,但Windows Forms可能会包含Tab控件,它有几个属性页组成,相互之间无甚关联,需要各自的验证。Windows桌面的属性对话框是个很好的例子,在每个属性页点击应用按钮时都需要不同的验证。在这种情况下,容器级的验证将更有意义。

在下篇中,我们将着手解决这个问题。同时我们还将扩展验证组件库,使它可以显示总结性错误信息(Summary)。


示例代码下载:CustomValidatorSample part2

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值