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

简介:

我们身在何处?

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

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

编程式的窗体级验证

最简单的方法是,在用户提交数据时,对控件逐一进行验证。每个验证组件都提供了Validate方法和IsValid属性,可以以此来判断是不是每个控件的输入都是有效的。

None.gif private   void  btnOK_Click( object  sender, EventArgs e)
ExpandedBlockStart.gif        
{
InBlock.gif            
// 验证所有控件
InBlock.gif            reqName.Validate();
InBlock.gif            reqBirth.Validate();
InBlock.gif            rngSpeed.Validate();
InBlock.gif            reqCommence.Validate();
InBlock.gif
InBlock.gif            
// 判断输入是否有效
InBlock.gif            
if ((reqName.IsValid) && (reqBirth.IsValid) && (reqCommence.IsValid) &&
InBlock.gif                (rngSpeed.IsValid))
ExpandedSubBlockStart.gif            
{
InBlock.gif                
this.DialogResult = DialogResult.OK;
ExpandedSubBlockEnd.gif            }

InBlock.gif            
else
ExpandedSubBlockStart.gif            
{
InBlock.gif                MessageBox.Show(
"Form not valid.");
ExpandedSubBlockEnd.gif            }

ExpandedBlockEnd.gif        }

None.gif

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

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

None.gif private  System.ComponentModel.IContainer components  =   null ;

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

None.gif [Serializable]
None.gif
public   class  ValidatorCollection : ICollection, IList, IEnumerable, ICloneable 
ExpandedBlockStart.gif
{
InBlock.gif    
// CollectionGen implementation
InBlock.gif
    dot.gif
ExpandedBlockEnd.gif}


ValidationManager

现在我们已经有了一个ValidatorCollection进行枚举了,但还要保证它能够包含窗体中所有的验证组件,我们需要实现一种机制,以在运行时添加和移除验证组件。ValidationManager可以满足这个需要:

None.gif public   class  ValidatorManager
ExpandedBlockStart.gif
{
InBlock.gif    
private static Hashtable _validators = new Hashtable();
InBlock.gif
InBlock.gif    
public static void Register(BaseValidator validator, Form hostingForm)
ExpandedSubBlockStart.gif    
{
InBlock.gif
InBlock.gif        
// Create form bucket if it doesn't exist
InBlock.gif
        if (_validators[hostingForm] == null)
ExpandedSubBlockStart.gif        
{
InBlock.gif            _validators[hostingForm] 
= new ValidatorCollection();
ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        
// Add this validator to the list of registered validators
InBlock.gif
        ValidatorCollection validators =
InBlock.gif          (ValidatorCollection)_validators[hostingForm];
InBlock.gif        validators.Add(validator);
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
public static ValidatorCollection GetValidators(Form hostingForm)
ExpandedSubBlockStart.gif    
{
InBlock.gif        
return (ValidatorCollection)_validators[hostingForm];
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
public static void DeRegister(BaseValidator validator, Form hostingForm)
ExpandedSubBlockStart.gif    
{
InBlock.gif        
// Remove this validator from the list of registered validators
InBlock.gif
        ValidatorCollection validators = (ValidatorCollection)_validators[hostingForm];
InBlock.gif        validators.Remove(validator);
InBlock.gif
InBlock.gif        
// Remove form bucket if all validators on the form are de-registered
InBlock.gif
        if (validators.Count == 0) _validators.Remove(hostingForm);
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}


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

 

另一方面,更新BaseValidator

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

ContractedBlock.gif BaseValidator Update


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

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

ContractedBlock.gif BaseValidator : ISupportInitialize

枚举ValidatiorCollection

 

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



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


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

ExpandedBlockEnd.gif}

None.gifDialogResult 
=  DialogResult.OK;

 

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

声明式的窗体级验证

 

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


ContractedBlock.gif ASP.NET Page Class


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

 

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


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

 

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

 

窗体的 AcceptButton CancelButton 属性都可以在属性浏览器中进行设置。这实际上指定了:当用户按下回车键时, 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
 

参考:
1. 
Extending Windows Forms with a Custom Validation Component Library Part2 . By Michael Weinhardt

2. Windows Forms Programming in C#. By Chris Sells


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2007/05/08/customvalidationlibrary2.html,如需转载请自行联系原作者。

目录
相关文章
|
消息中间件 安全 API
C#实现操作Windows窗口句柄:SendMessage/PostMessage发送系统消息、事件和数据【窗口句柄总结之二】
SendMessage/PostMessage API 可以实现发送系统消息,这些消息可以定义为常见的鼠标或键盘事件、数据的发送等各种系统操作......
3697 1
C#实现操作Windows窗口句柄:SendMessage/PostMessage发送系统消息、事件和数据【窗口句柄总结之二】
|
5月前
|
监控 C# Windows
内网桌面监控软件中的远程控制功能实现(基于C#和Windows Forms)
近年来,随着远程办公的兴起,对内网桌面监控软件的需求逐渐增加。本文将探讨如何通过C#和Windows Forms实现内网桌面监控软件中的远程控制功能,并在结尾部分介绍监控到的数据如何自动提交到网站。
279 0
|
API C# Windows
C#实现操作Windows窗口句柄:常用窗口句柄相关API、Winform中句柄属性和Process的MainWindowHandle问题【窗口句柄总结之三】
本篇主要介绍一些与窗口句柄相关的一些API,比如设置窗口状态、当前激活的窗口、窗口客户区的大小、鼠标位置、禁用控件等,以及介绍Winform中的句柄属性,便于直接获取控件或窗体句柄,以及不推荐...
1655 0
C#实现操作Windows窗口句柄:常用窗口句柄相关API、Winform中句柄属性和Process的MainWindowHandle问题【窗口句柄总结之三】
|
Java Windows Spring
java实现spring boot项目启动时,重启Windows进程
java实现spring boot项目启动时,重启Windows进程
474 0
|
Windows
Windows下实现gettimeofday()函数
Windows下实现gettimeofday()函数
313 0
|
JSON Java API
C#winforms实现windows窗体人脸识别
C#winforms实现windows窗体人脸识别
295 0
|
Linux iOS开发 开发者
实现在windows、linux下上传ios app到App Store
实现在windows、linux下上传ios app到App Store
实现在windows、linux下上传ios app到App Store
|
JSON JavaScript 安全
基于Windows微信实现实时收发微信消息App
基于Windows微信实现实时收发微信消息App
1152 0
基于Windows微信实现实时收发微信消息App
|
Ubuntu 安全 Linux
【过关斩将般的一步步实现】windows本机通过xftp/xshell连接Ubuntu虚拟机服务器
【过关斩将般的一步步实现】windows本机通过xftp/xshell连接Ubuntu虚拟机服务器
733 1
【过关斩将般的一步步实现】windows本机通过xftp/xshell连接Ubuntu虚拟机服务器
|
Web App开发 前端开发 JavaScript
【我的前端】CSS在Windows下实现Mac浏览器滚动条
众所周知,Windows 和 macOS 浏览器的滚动条在默认情况下是不一致的,最为显著的是 macOS 的滚动条是不占据屏幕尺寸的,macOS 的滚动条如下:
【我的前端】CSS在Windows下实现Mac浏览器滚动条