Web Control 开发系列(四) Validation机制
2011-04-28 15:34
441 查看
转自 :http://www.cnblogs.com/joeliu/archive/2008/11/13/1240206.html
前言:
前一段时间写Web
Control开发系列的文章,后来由于工作实在忙,就没有继续写了,如今我要继续写下去,研究了微软的Web
Control体系结构这么久,我有一个总体的感觉,就是微软把所有自己认为有用的东西,无论大小,都设计了,都实现了,以至于我们能发挥的空间很有限了,一旦我们设计一个自认为更好的结构,虽然确实很好,但是因为和微软的结构不一致,也会很难和微软的其它Control协同工作,所以要做一个Composite
Web
Control,最大的功力就是要彻底弄清楚微软的Control体系结构,以求达到“天人合一”的效果。想起来真是悲哀,我们大量的时间和精力都浪费在学习人家东西上面了,等你搞熟练了,人家又升级了,所以永远也赶不上,真是悲哀!
正文:
WebForm组件中Validataion控件是比较有用的,可以很方便的为我们的应用程序提供方便的输入校验。这种校验不仅仅在服务器端进行,也同时会在客户端发生。深入理解WebForm的Validation机制,对于合理设计我们的Control是非常有帮助的,这样可以让我们的Control可以和系统的validator进行协作。下面我一步一步介绍微软Validation的体系结构:
1. IValidator
接口
这个接口的Member有:IsValid, ErrorMessage,
Validate()。所有的Validator必须实现这个接口。在一个Page页面上会有一个ValidatorCollection类型的属性Validators,该集合只能添加实现了IValidator接口的对象。它负责维护这个页面所有的Validator。目前系统只有一个类实现了这个接口,就是BaseValidator。系统所有的其它的Validator类型都是从BaseValidator派生的。
2. BaseValidator类
BaseValidator实现了Validator一些基础的功能,如果我们自己写一个新的Validator,我建议从这个类派生,如果不从这个类派生,而是直接实现IValidator接口
,从理论上来说是可以的,但是可能会有相当大的挑战,因为BaseValidator这个类搭建了.net
framework的整个Validation体系结构,即使自己直接实现IValidator接口也必须重写一遍BaseValidator所实现的功能,否则自己写的Validator很难表现的和系统的Validator效果一致,下面介绍BaseBalidator
所做的工作
维护Validator和Page的关系
BaseValidator是从Label派生的,所以也是从Control派生的,Control的OnInit函数是在Control加入到Page的时候执行的,而与之对应Control的OnUnload函数是在Control从Page移除的时候执行的,因此如果需要对Control一些和Page有关的状态初始化的时候,最好在这两个时机处理。
Code
protected
internal
override
void
OnInit(EventArgs e)
{
base
.OnInit(e);
this
.Page.Validators.Add(
this
);
}
Code
protected
internal
override
void
OnUnload(EventArgs e)
{
if
(
this
.Page
!=
null
)
{
this
.Page.Validators.Remove(
this
);
}
base
.OnUnload(e);
}
通过上面的代码我们可以发现,Validator总是会在OnInit函数里面把自己加入Page的Validators集合中,而在OnUnload函数里面从Page的Validators集合中移除自己,这样Page上可以拿到所有的Validators。
实现客户端验证体系
为了实现客户端验证的功能,BaseValidator必须注册必要的客户端脚本来完成这个功能。从原理上面讲,就是在客户端也实现了一套和服务器端一样的数据结构,这个结构包含Javascript
的Validator对象, Validators集合,
以及各种Validator的Validate方法。下面来看看BaseValidator是如何搭建这一套客户端数据结构的。
a.
在Render
函数中注册当前的Validator到客户端的Validators集合中。
首先需要把当前页面的所有Validator都在客户端生成JavaScript的Validator对象,这个是通过BaseValidator的RegisterValidatorDeclaration函数完成的。
Code
protected
virtual
void
RegisterValidatorDeclaration()
{
string
arrayValue
=
"
document.getElementById(/
""
+ this.ClientID +
"
/
"
)
"
;
if
(
!
this
.Page.IsPartialRenderingSupported)
{
this
.Page.ClientScript.RegisterArrayDeclaration(
"
Page_Validators
"
, arrayValue);
}
else
{
ValidatorCompatibilityHelper.RegisterArrayDeclaration(
this
,
"
Page_Validators
"
, arrayValue);
ValidatorCompatibilityHelper.RegisterStartupScript(
this
,
typeof
(BaseValidator),
this
.ClientID
+
"
_DisposeScript
"
,
string
.Format(CultureInfo.InvariantCulture,
"
/r/ndocument.getElementById('{0}').dispose = function() {{/r/n Array.remove({1}, document.getElementById('{0}'));/r/n}}/r/n
"
,
new
object
[] {
this
.ClientID,
"
Page_Validators
"
}),
true
);
}
}
上面的函数调用ScriptManager.RegisterArrayDeclaration在注册一个数组item,这个操作实际就是在
ScriptManager的一个指定的数组里面增加一个item,当Page
Render到客户端的时候,该数组会自动Render为一个客户端的JavaScript的数组声明语句。改语句在直接写在页面里面,当页面加载的时候立即执行。生成的脚本如下:
Code
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
var
Page_Validators
=
new
Array(document.getElementById(
"
RequiredFieldValidator1
"
));
//
]]> // 这里页面上只有一个validator,所以数组中只有一个
<
/
script>
这样Page上所有的Validator都会在客户端加入Page_Validators变量里面。当页面提交的时候,可以遍历所有Page_Validators里面的Validator,调用其evaluationfunction来做客户端校验。
b.
在OnPreRender
函数中首先通过下面的函数判断客户端是否支持脚本
Code
protected
virtual
bool
DetermineRenderUplevel()
{
Page page
=
this
.Page;
if
((page
==
null
)
||
(page.RequestInternal
==
null
))
{
return
false
;
}
return
((
this
.EnableClientScript
&&
(page.Request.Browser.W3CDomVersion.Major
>=
0x1
))
&&
(page.Request.Browser.EcmaScriptVersion.CompareTo(
new
Version(
0x1
,
0x2
))
>=
0x0
));
}
如果支持脚本,就调用RegisterValidatorCommonScript()函数来注册脚本,这个函数主要做了三件事:
1.
注册.net framework的脚本文件WebUIValidation.js (这个文件包含了客户端Validator的主要脚本实现)
2.
注册一段初始化脚本。该脚本会对本Validator的客户端对象做一些初始化的工作。把服务器端Validator的属性值Clone到客户端Validator里面。
3.
注册一段脚本,当Form在提交(submit)的时候执行,这是通过ScriptManger.RegisterOnSubmitStatement
语句完成的,该函数可以在Form的OnSubmit
脚本函数里面插入指定的脚本语句
。这个主要是在Form提交的时候查看页面的Validation结果变量,如果为false,那么阻止提交,否则就允许提交。
完成上面三个注册任务的是下面函数:
Code
protected
void
RegisterValidatorCommonScript()
{
if
(
!
this
.Page.IsPartialRenderingSupported)
{
if
(
!
this
.Page.ClientScript.IsClientScriptBlockRegistered(
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
))
{
this
.Page.ClientScript.RegisterClientScriptResource(
typeof
(BaseValidator),
"
WebUIValidation.js
"
);
this
.Page.ClientScript.RegisterStartupScript(
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
,
"
/r/n<script type=/
"
text
/
javascript/
"
>/r/n<!--/r/nvar Page_ValidationActive = false;/r/nif (typeof(ValidatorOnLoad) == /
"
function/
"
) {/r/n ValidatorOnLoad();/r/n}/r/n/r/nfunction ValidatorOnSubmit() {/r/n if (Page_ValidationActive) {/r/n return ValidatorCommonOnSubmit();/r/n }/r/n else {/r/n return true;/r/n }/r/n}/r/n// -->/r/n</script>/r/n
"
);
this
.Page.ClientScript.RegisterOnSubmitStatement(
typeof
(BaseValidator),
"
ValidatorOnSubmit
"
,
"
if (typeof(ValidatorOnSubmit) == /
"
function/
"
&& ValidatorOnSubmit() == false) return false;
"
);
}
}
else
{
ValidatorCompatibilityHelper.RegisterClientScriptResource(
this
,
typeof
(BaseValidator),
"
WebUIValidation.js
"
);
ValidatorCompatibilityHelper.RegisterStartupScript(
this
,
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
,
"
/r/n<script type=/
"
text
/
javascript/
"
>/r/n<!--/r/nvar Page_ValidationActive = false;/r/nif (typeof(ValidatorOnLoad) == /
"
function/
"
) {/r/n ValidatorOnLoad();/r/n}/r/n/r/nfunction ValidatorOnSubmit() {/r/n if (Page_ValidationActive) {/r/n return ValidatorCommonOnSubmit();/r/n }/r/n else {/r/n return true;/r/n }/r/n}/r/n// -->/r/n</script>/r/n
"
,
false
);
ValidatorCompatibilityHelper.RegisterOnSubmitStatement(
this
,
typeof
(BaseValidator),
"
ValidatorOnSubmit
"
,
"
if (typeof(ValidatorOnSubmit) == /
"
function/
"
&& ValidatorOnSubmit() == false) return false;
"
);
}
}
注册的脚本内容是
Code
<
script type
=
"
text/javascript
"
>
<!--
var
Page_ValidationActive
=
false
;
if
(
typeof
(ValidatorOnLoad)
==
"
function
"
) {
ValidatorOnLoad();
//
该函数在WebUIValidation.js中定义
}
function
ValidatorOnSubmit() {
if
(Page_ValidationActive) {
return
ValidatorCommonOnSubmit();
//
该函数在WebUIValidation.js中定义
}
else
{
return
true
;
}
}
//
-->
<
/
script>
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
function
WebForm_OnSubmit() {
if
(
typeof
(ValidatorOnSubmit)
==
"
function
"
&&
ValidatorOnSubmit()
==
false
)
return
false
;
return
true
;
//
这三句脚本就是BaseValidator注册进来的,它会在Form submit的时候触发
}
//
]]>
<
/
script>
ValidatorOnLoad函数里面做了下面的事情:
一方面初始化校验函数,
Code
if
(
typeof
(val.evaluationfunction)
==
"
string
"
)
//
该操作对所有的Validators执行
{
eval(
"
val.evaluationfunction =
"
+
val.evaluationfunction
+
"
;
"
);
}
//
val就是客户端的Validator对象,执行这个代码来给这个对象赋值一个函数,这个函数就是客户端调用函数
另一方面给Control的客户端的Html元素(INPUT,TEXTAREA,SELECT等)挂相应的event处理函数,这样当event发生的时候,就可以自动调用该Html元素关联的所有validator的校验脚本函数。
c.
在AddAttributesToRender
函数里面注册Client
Validator对象的初始化脚本,主要调用ScriptManger.RegisterExpandoAttribute完成的,
这个脚本也是在页面加载的时候立即执行的。,
如下:
Code
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
var
RequiredFieldValidator1
=
document.all
?
document.all[
"
RequiredFieldValidator1
"
] : document.getElementById(
"
RequiredFieldValidator1
"
);
RequiredFieldValidator1.controltovalidate
=
"
TextBox1
"
;
RequiredFieldValidator1.errormessage
=
"
RequiredFieldValidator
"
;
RequiredFieldValidator1.evaluationfunction
=
"
RequiredFieldValidatorEvaluateIsValid
"
;
RequiredFieldValidator1.initialvalue
=
""
;
//
]]>
<
/
script>
如果设置了更多的属性,生成的脚本会更多,其中evaluationfunction的值是一个JavaScript函数的名称,对于.net
framework定义的几种Validator,这些函数都定义在WebUIValidation.js
里面.
通过上面三个地方脚本的注册,Validator就可以实现了客户端的验证的功能。
1.
验证的过程可以是IButtonControl(Button,LinkButton,ImageButton等)在提交的时候调用WebForm_DoPostBackWithOptions
()脚本,如果IButtonControl.CauseValidation==true,那么它就会调用Page_ClientValidate
()函数,这个是Validator客户端体系的一个重要入口,它首先会遍历页面的所有Validator对象,依次执行它们的校验函数
进行校验,其次会处理ValidationSummary对象,设置其输出错误信息。
2.我们通过上面注册脚本的过程了解到在ValidatorOnLoad
()里面调用了ValidatorHookupControlID
()函数,这个函数实际就是监听特殊的Html
Element的校验时机,这包括“INPUT",”TEXTAREA",“SELECT"元素,而且查找这些元素的逻辑递归向下的,一个都不放过。如何监听分为两种,一种是type为radio的INPUT元素,通过挂onclick事件得到通知,其它的元素都是挂onchange事件得到通知。如果需要在校验失败的时候把焦点置回去,那么还需要挂onblur事件。这样当这些元素的onclick或者onchange事件发生的时候,就会执行该Validator的校验逻辑。
以上是两个执行客户端校验的时机,下面给出两点建议:
1.
如果我们做一个Control,该Control具有引起页面Postback的能力,那么我们最好通过ScriptManager.GetPostBackEventReference()函数来注册客户端的WebForm_DoPostBackWithOptions
()脚本,该脚本不仅仅具备引起Postback的能力,还具有执行客户端校验的能力。如果自己通过Form.submit来做就和Asp.net体系结构结合的不是很紧密了。
2.
如何可以让系统的Validator Control可以用来在客户端(服务器端后面讲)校验我们的Control呢?
通过分析源码发现,标准的Validator总是递归查找待校验的Control所生成的Html
Elment节点上的value属性,然后拿着value属性进行校验。因此如果希望我们的Control可以用系统的Validator进行校验,就必须在生成的Html
Element里面有一个节点有value属性,而且该属性的值就是希望被校验的值。
3.
Validator的服务器端工作原理
由上面的分析,可以看出,BaseValidator类搭建了一套完整的客户端验证体系结构,但这并不是IValidator接口所必须的,IValidator接口定义的是服务器端的验证,那么在服务器端Validator如何和整个WebForm里面的Control进行协作工作的呢?我们从下面三个方面来阐述:Validate的调用入口,Validate的调用时机,Validate的执行过程
Validate调用入口
在Page上管理所有的IValidator对象的集合,并且由Page执行相应的Validate的调用,因此Validate调用的入口是在Page上面的。通过前面的介绍,我们了解到,当Validator在OnInit的阶段,它会把自己添加到Page的Validators的集合里面,而在OnUnload阶段它会把自己从Page的Validators集合里面移除。Page上关于Validate的调用方法有两个(如下),总体思想很简单,就是一个遍历。
Code
public
virtual
void
Validate()
{
this
._validated
=
true
;
if
(
this
._validators
!=
null
)
{
//
遍历页面所有的Validator,进行Validate的函数调用
for
(
int
i
=
0x0
; i
<
this
.Validators.Count; i
++
)
{
this
.Validators[i].Validate();
}
}
}
public
virtual
void
Validate(
string
validationGroup)
{
this
._validated
=
true
;
if
(
this
._validators
!=
null
)
{
ValidatorCollection validators
=
this
.GetValidators(validationGroup);
if
(
string
.IsNullOrEmpty(validationGroup)
&&
(
this
._validators.Count
==
validators.Count))
{
this
.Validate();
}
else
{
//
遍历页面所有validationGroup为指定值的Validator,进行Validate的函数调用
for
(
int
i
=
0x0
; i
<
validators.Count; i
++
)
{
validators[i].Validate();
}
}
}
}
Validate的调用时机
Validate的时机仅仅发生在PostBack阶段,在我前面的两篇文章中( Web
Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
,Web Control 开发系列(三)
解析IPostBackEventHandler和WebForm的事件机制
),我详细讲解了页面在PostBack过程中的两个重要阶段:1.
ProcessPostData, 2. ProcessPostEvent。如果对这个不是很了解的话,就需要复习复习了:-)
Validate的调用就是在这两个阶段执行的。 主要调用的地方有三处:
SomeControl.RasiePostDataChangedEvent()
SomeControl.RaisePostBackEvent(String)
Page.RaisePostBackEvent(NameValueCollection nameValueCollection)
当调用Page上面的Validate方法的时候,需要了解引起PostBack的起源,这样的起源大致有两类,一类是实现了IPostBackDataHandler或者IPostBackEventHandler接口的标准Asp.net
Control,另一类是Html的Input元素或者一些引发回传的脚本等。
为什么要了解引起PostBack的起源呢?因为有些引起PostBack的Control可能有个属性叫CauseValidation,当设置为false的时候,不希望引起Validation。而且还有的Control会有ValidationGroup这样的属性,来控制部分Control进行Validation,可见了解PostBack的起源对于调用Page.Validate()和Page.Validate(validationGroup)是至关重要的。而对于无法得到PostBack起源Control的情况,则一律简单调用Page.Validate()进行页面内所有未分组的Control的校验。
对于第一类,又有两种情况:实现了IPostBackDataHandler的Control,
实现了IPostBackEventHandler的Control,这些Control对Validate的调用分别在
SomeControl.RasiePostDataChangedEvent()
SomeControl.RaisePostBackEvent(String)
函数里面调用,其实现基本都是一直的,见下面的代码:
Code
//
下面是TextBox里面引发Page.Validation(validationGroup)调用入口的函数,其它的
//
Control,如CheckBox,RadioBox的实现和这个是一致的。
protected
virtual
void
RaisePostDataChangedEvent()
{
//
下面的条件是判断引起这次Postback的Control就是当前Control
if
(
this
.AutoPostBack
&&
!
this
.Page.IsPostBackEventControlRegistered)
{
//
设置Page上面的这个变量的唯一目的就是告诉Page,我已经做过Validation了
//
你不用再关心了
this
.Page.AutoPostBackControl
=
this
;
if
(
this
.CausesValidation)
{
this
.Page.Validate(
this
.ValidationGroup);
}
}
this
.OnTextChanged(EventArgs.Empty);
}
上面的代码是对于实现了IPostDataHandler接口的Control的处理。下面来看看实现了IPostBackEventHandler接口的Control的处理,以Button为列,其它的LinkButton,ImageButton
等Control实现差不多都这样
Code
protected
virtual
void
RaisePostBackEvent(
string
eventArgument)
{
//
先校验Event
base
.ValidateEvent(
this
.UniqueID, eventArgument);
//
程序走到这里,就认为引起PostBack的起源就是当前Control,所以如果当前Control的
//
的CauseValidation为true,那么调用页面的Page.Validate(validationGroup)函数进行
//
Validate操作
if
(
this
.CausesValidation)
{
this
.Page.Validate(
this
.ValidationGroup);
}
this
.OnClick(EventArgs.Empty);
this
.OnCommand(
new
CommandEventArgs(
this
.CommandName,
this
.CommandArgument));
}
上面解释了对于第一类情况,我们都能查找到引起PostBack的起源Control,那么对于第二类情况如何处理呢,这个时候是无法查到引起PostBack的Control的,看下面的代码:
Code
private
void
RaisePostBackEvent(NameValueCollection postData)
{
//
1. 假如已经在Page上显式的注册了引起PostBackEvent的Control,就直接处理
if
(
this
._registeredControlThatRequireRaiseEvent
!=
null
)
{
//
这个函数会直接调用Control上面的RaisePostBackEvent(String)函数,这个函数里面
//
会进行Page.Validate函数的调用。上面刚刚解释了。
this
.RaisePostBackEvent(
this
._registeredControlThatRequireRaiseEvent,
null
);
}
else
{
//
这部分代码,我自己按照Reflector反编译的结果重新组织了,但是逻辑
//
没有任何变化,只是方便阅读理解
//
2. 假如没有注册,就查找__EVENTTARGET记录的Control来处理
string
eventTarget
=
postData[
"
__EVENTTARGET
"
];
bool
hasEventTarget
=
!
string
.IsNullOrEmpty(eventTarget);
Control eventTargetControl
=
null
;
if
(hasEventTarget)
{
eventTargetControl
=
this
.FindControl(eventTarget);
if
((eventTargetControl
!=
null
)
&&
(eventTargetControl.PostBackEventHandler
!=
null
))
{
string
eventArgument
=
postData[
"
__EVENTARGUMENT
"
];
//
这个函数会直接调用Control上面的RaisePostBackEvent(String)函数,这个函数里面
//
会进行Page.Validate函数的调用。上面刚刚解释了。
this
.RaisePostBackEvent(eventTargetControl.PostBackEventHandler, eventArgument);
}
}
else
if
(
this
.AutoPostBackControl
==
null
)
{
//
这个AutoPostBackControl如果不为null,那么说明了AutoPostBackControl 在自己的
//
RaisePostDataChanged()函数里面已经调用了Page.Validate()函数,上面刚刚解释了。
//
这个时候属性无法查找到引起这次PostBack的真正起源Control,也许是一个Input元素
//
引起的,也许是一段脚本引起的PostBack,这样,我们就对于所有未分组的Validator就行
//
一次校验,因此调用Page.Validate()
this
.Validate();
}
}
}
如此我们已经对于三处调用Page.Validate方法的调用地方都进行了解释。可以很了解服务器端的Validate的调用时机。
Validate的执行过程
Validate的执行主要是调用接口IValidator.Validate()方法完成的,这个在BaseValidator里面已经做了一个基本的实现,可以看看下面的实现代码:
Code
public
void
Validate()
{
this
.IsValid
=
true
;
if
(
this
.Visible
&&
this
.Enabled)
{
this
.propertiesChecked
=
false
;
//
检查是否能在当前的NameContainer里面查找到待校验的Control
if
(
this
.PropertiesValid)
{
//
下面的方法是一个abstract方法,依赖于具体的Validator来实现自己的逻辑
this
.IsValid
=
this
.EvaluateIsValid();
//
控制Focus的位置,如果希望失败后把Focus保留在校验的Control上,那么就
//
通过Page的调用完成这个操作。
if
((
!
this
.IsValid
&&
(
this
.Page
!=
null
))
&&
this
.SetFocusOnError)
{
this
.Page.SetValidatorInvalidControlFocus(
this
.ControlToValidate);
}
}
}
}
为了让用户从BaseValidator派生的Control专注于实现Validate的具体逻辑,BaseValidator还默认实现了获得待校验Control的Valud的方法:
Code
protected
string
GetControlValidationValue(
string
name)
{
//
1. 在当前的NameContainer里面查找待校验的Control
Control component
=
this
.NamingContainer.FindControl(name);
if
(component
==
null
)
{
return
null
;
}
//
2. 查找Control上面需要用来校验的Property,它是通过一个Attribute来标识的。
PropertyDescriptor validationProperty
=
GetValidationProperty(component);
if
(validationProperty
==
null
)
{
return
null
;
}
//
3. 从Porperty上面取出要校验的Value
object
obj2
=
validationProperty.GetValue(component);
if
(obj2
is
ListItem)
{
return
((ListItem) obj2).Value;
}
if
(obj2
!=
null
)
{
return
obj2.ToString();
}
return
string
.Empty;
}
public
static
PropertyDescriptor GetValidationProperty(
object
component)
{
ValidationPropertyAttribute attribute
=
(ValidationPropertyAttribute) TypeDescriptor.GetAttributes(component)[
typeof
(ValidationPropertyAttribute)];
if
((attribute
!=
null
)
&&
(attribute.Name
!=
null
))
{
return
TypeDescriptor.GetProperties(component, (Attribute[])
null
)[attribute.Name];
}
return
null
;
}
因此,当执行Validate的时候,Validator会在当前的NameContainer里面查找需要校验的Control,然后自动取出Control上面需要校验的Property的Value,最后调用Validator自己的逻辑来校验这个Value。
总结
花了好多时间,终于写完了,总体来说微软的Validation机制还是比较强大的。
前言:
前一段时间写Web
Control开发系列的文章,后来由于工作实在忙,就没有继续写了,如今我要继续写下去,研究了微软的Web
Control体系结构这么久,我有一个总体的感觉,就是微软把所有自己认为有用的东西,无论大小,都设计了,都实现了,以至于我们能发挥的空间很有限了,一旦我们设计一个自认为更好的结构,虽然确实很好,但是因为和微软的结构不一致,也会很难和微软的其它Control协同工作,所以要做一个Composite
Web
Control,最大的功力就是要彻底弄清楚微软的Control体系结构,以求达到“天人合一”的效果。想起来真是悲哀,我们大量的时间和精力都浪费在学习人家东西上面了,等你搞熟练了,人家又升级了,所以永远也赶不上,真是悲哀!
正文:
WebForm组件中Validataion控件是比较有用的,可以很方便的为我们的应用程序提供方便的输入校验。这种校验不仅仅在服务器端进行,也同时会在客户端发生。深入理解WebForm的Validation机制,对于合理设计我们的Control是非常有帮助的,这样可以让我们的Control可以和系统的validator进行协作。下面我一步一步介绍微软Validation的体系结构:
1. IValidator
接口
这个接口的Member有:IsValid, ErrorMessage,
Validate()。所有的Validator必须实现这个接口。在一个Page页面上会有一个ValidatorCollection类型的属性Validators,该集合只能添加实现了IValidator接口的对象。它负责维护这个页面所有的Validator。目前系统只有一个类实现了这个接口,就是BaseValidator。系统所有的其它的Validator类型都是从BaseValidator派生的。
2. BaseValidator类
BaseValidator实现了Validator一些基础的功能,如果我们自己写一个新的Validator,我建议从这个类派生,如果不从这个类派生,而是直接实现IValidator接口
,从理论上来说是可以的,但是可能会有相当大的挑战,因为BaseValidator这个类搭建了.net
framework的整个Validation体系结构,即使自己直接实现IValidator接口也必须重写一遍BaseValidator所实现的功能,否则自己写的Validator很难表现的和系统的Validator效果一致,下面介绍BaseBalidator
所做的工作
维护Validator和Page的关系
BaseValidator是从Label派生的,所以也是从Control派生的,Control的OnInit函数是在Control加入到Page的时候执行的,而与之对应Control的OnUnload函数是在Control从Page移除的时候执行的,因此如果需要对Control一些和Page有关的状态初始化的时候,最好在这两个时机处理。
Code
protected
internal
override
void
OnInit(EventArgs e)
{
base
.OnInit(e);
this
.Page.Validators.Add(
this
);
}
Code
protected
internal
override
void
OnUnload(EventArgs e)
{
if
(
this
.Page
!=
null
)
{
this
.Page.Validators.Remove(
this
);
}
base
.OnUnload(e);
}
通过上面的代码我们可以发现,Validator总是会在OnInit函数里面把自己加入Page的Validators集合中,而在OnUnload函数里面从Page的Validators集合中移除自己,这样Page上可以拿到所有的Validators。
实现客户端验证体系
为了实现客户端验证的功能,BaseValidator必须注册必要的客户端脚本来完成这个功能。从原理上面讲,就是在客户端也实现了一套和服务器端一样的数据结构,这个结构包含Javascript
的Validator对象, Validators集合,
以及各种Validator的Validate方法。下面来看看BaseValidator是如何搭建这一套客户端数据结构的。
a.
在Render
函数中注册当前的Validator到客户端的Validators集合中。
首先需要把当前页面的所有Validator都在客户端生成JavaScript的Validator对象,这个是通过BaseValidator的RegisterValidatorDeclaration函数完成的。
Code
protected
virtual
void
RegisterValidatorDeclaration()
{
string
arrayValue
=
"
document.getElementById(/
""
+ this.ClientID +
"
/
"
)
"
;
if
(
!
this
.Page.IsPartialRenderingSupported)
{
this
.Page.ClientScript.RegisterArrayDeclaration(
"
Page_Validators
"
, arrayValue);
}
else
{
ValidatorCompatibilityHelper.RegisterArrayDeclaration(
this
,
"
Page_Validators
"
, arrayValue);
ValidatorCompatibilityHelper.RegisterStartupScript(
this
,
typeof
(BaseValidator),
this
.ClientID
+
"
_DisposeScript
"
,
string
.Format(CultureInfo.InvariantCulture,
"
/r/ndocument.getElementById('{0}').dispose = function() {{/r/n Array.remove({1}, document.getElementById('{0}'));/r/n}}/r/n
"
,
new
object
[] {
this
.ClientID,
"
Page_Validators
"
}),
true
);
}
}
上面的函数调用ScriptManager.RegisterArrayDeclaration在注册一个数组item,这个操作实际就是在
ScriptManager的一个指定的数组里面增加一个item,当Page
Render到客户端的时候,该数组会自动Render为一个客户端的JavaScript的数组声明语句。改语句在直接写在页面里面,当页面加载的时候立即执行。生成的脚本如下:
Code
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
var
Page_Validators
=
new
Array(document.getElementById(
"
RequiredFieldValidator1
"
));
//
]]> // 这里页面上只有一个validator,所以数组中只有一个
<
/
script>
这样Page上所有的Validator都会在客户端加入Page_Validators变量里面。当页面提交的时候,可以遍历所有Page_Validators里面的Validator,调用其evaluationfunction来做客户端校验。
b.
在OnPreRender
函数中首先通过下面的函数判断客户端是否支持脚本
Code
protected
virtual
bool
DetermineRenderUplevel()
{
Page page
=
this
.Page;
if
((page
==
null
)
||
(page.RequestInternal
==
null
))
{
return
false
;
}
return
((
this
.EnableClientScript
&&
(page.Request.Browser.W3CDomVersion.Major
>=
0x1
))
&&
(page.Request.Browser.EcmaScriptVersion.CompareTo(
new
Version(
0x1
,
0x2
))
>=
0x0
));
}
如果支持脚本,就调用RegisterValidatorCommonScript()函数来注册脚本,这个函数主要做了三件事:
1.
注册.net framework的脚本文件WebUIValidation.js (这个文件包含了客户端Validator的主要脚本实现)
2.
注册一段初始化脚本。该脚本会对本Validator的客户端对象做一些初始化的工作。把服务器端Validator的属性值Clone到客户端Validator里面。
3.
注册一段脚本,当Form在提交(submit)的时候执行,这是通过ScriptManger.RegisterOnSubmitStatement
语句完成的,该函数可以在Form的OnSubmit
脚本函数里面插入指定的脚本语句
。这个主要是在Form提交的时候查看页面的Validation结果变量,如果为false,那么阻止提交,否则就允许提交。
完成上面三个注册任务的是下面函数:
Code
protected
void
RegisterValidatorCommonScript()
{
if
(
!
this
.Page.IsPartialRenderingSupported)
{
if
(
!
this
.Page.ClientScript.IsClientScriptBlockRegistered(
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
))
{
this
.Page.ClientScript.RegisterClientScriptResource(
typeof
(BaseValidator),
"
WebUIValidation.js
"
);
this
.Page.ClientScript.RegisterStartupScript(
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
,
"
/r/n<script type=/
"
text
/
javascript/
"
>/r/n<!--/r/nvar Page_ValidationActive = false;/r/nif (typeof(ValidatorOnLoad) == /
"
function/
"
) {/r/n ValidatorOnLoad();/r/n}/r/n/r/nfunction ValidatorOnSubmit() {/r/n if (Page_ValidationActive) {/r/n return ValidatorCommonOnSubmit();/r/n }/r/n else {/r/n return true;/r/n }/r/n}/r/n// -->/r/n</script>/r/n
"
);
this
.Page.ClientScript.RegisterOnSubmitStatement(
typeof
(BaseValidator),
"
ValidatorOnSubmit
"
,
"
if (typeof(ValidatorOnSubmit) == /
"
function/
"
&& ValidatorOnSubmit() == false) return false;
"
);
}
}
else
{
ValidatorCompatibilityHelper.RegisterClientScriptResource(
this
,
typeof
(BaseValidator),
"
WebUIValidation.js
"
);
ValidatorCompatibilityHelper.RegisterStartupScript(
this
,
typeof
(BaseValidator),
"
ValidatorIncludeScript
"
,
"
/r/n<script type=/
"
text
/
javascript/
"
>/r/n<!--/r/nvar Page_ValidationActive = false;/r/nif (typeof(ValidatorOnLoad) == /
"
function/
"
) {/r/n ValidatorOnLoad();/r/n}/r/n/r/nfunction ValidatorOnSubmit() {/r/n if (Page_ValidationActive) {/r/n return ValidatorCommonOnSubmit();/r/n }/r/n else {/r/n return true;/r/n }/r/n}/r/n// -->/r/n</script>/r/n
"
,
false
);
ValidatorCompatibilityHelper.RegisterOnSubmitStatement(
this
,
typeof
(BaseValidator),
"
ValidatorOnSubmit
"
,
"
if (typeof(ValidatorOnSubmit) == /
"
function/
"
&& ValidatorOnSubmit() == false) return false;
"
);
}
}
注册的脚本内容是
Code
<
script type
=
"
text/javascript
"
>
<!--
var
Page_ValidationActive
=
false
;
if
(
typeof
(ValidatorOnLoad)
==
"
function
"
) {
ValidatorOnLoad();
//
该函数在WebUIValidation.js中定义
}
function
ValidatorOnSubmit() {
if
(Page_ValidationActive) {
return
ValidatorCommonOnSubmit();
//
该函数在WebUIValidation.js中定义
}
else
{
return
true
;
}
}
//
-->
<
/
script>
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
function
WebForm_OnSubmit() {
if
(
typeof
(ValidatorOnSubmit)
==
"
function
"
&&
ValidatorOnSubmit()
==
false
)
return
false
;
return
true
;
//
这三句脚本就是BaseValidator注册进来的,它会在Form submit的时候触发
}
//
]]>
<
/
script>
ValidatorOnLoad函数里面做了下面的事情:
一方面初始化校验函数,
Code
if
(
typeof
(val.evaluationfunction)
==
"
string
"
)
//
该操作对所有的Validators执行
{
eval(
"
val.evaluationfunction =
"
+
val.evaluationfunction
+
"
;
"
);
}
//
val就是客户端的Validator对象,执行这个代码来给这个对象赋值一个函数,这个函数就是客户端调用函数
另一方面给Control的客户端的Html元素(INPUT,TEXTAREA,SELECT等)挂相应的event处理函数,这样当event发生的时候,就可以自动调用该Html元素关联的所有validator的校验脚本函数。
c.
在AddAttributesToRender
函数里面注册Client
Validator对象的初始化脚本,主要调用ScriptManger.RegisterExpandoAttribute完成的,
这个脚本也是在页面加载的时候立即执行的。,
如下:
Code
<
script type
=
"
text/javascript
"
>
//
<![CDATA[
var
RequiredFieldValidator1
=
document.all
?
document.all[
"
RequiredFieldValidator1
"
] : document.getElementById(
"
RequiredFieldValidator1
"
);
RequiredFieldValidator1.controltovalidate
=
"
TextBox1
"
;
RequiredFieldValidator1.errormessage
=
"
RequiredFieldValidator
"
;
RequiredFieldValidator1.evaluationfunction
=
"
RequiredFieldValidatorEvaluateIsValid
"
;
RequiredFieldValidator1.initialvalue
=
""
;
//
]]>
<
/
script>
如果设置了更多的属性,生成的脚本会更多,其中evaluationfunction的值是一个JavaScript函数的名称,对于.net
framework定义的几种Validator,这些函数都定义在WebUIValidation.js
里面.
通过上面三个地方脚本的注册,Validator就可以实现了客户端的验证的功能。
客户端验证的过程
1.
验证的过程可以是IButtonControl(Button,LinkButton,ImageButton等)在提交的时候调用WebForm_DoPostBackWithOptions
()脚本,如果IButtonControl.CauseValidation==true,那么它就会调用Page_ClientValidate
()函数,这个是Validator客户端体系的一个重要入口,它首先会遍历页面的所有Validator对象,依次执行它们的校验函数
进行校验,其次会处理ValidationSummary对象,设置其输出错误信息。
2.我们通过上面注册脚本的过程了解到在ValidatorOnLoad
()里面调用了ValidatorHookupControlID
()函数,这个函数实际就是监听特殊的Html
Element的校验时机,这包括“INPUT",”TEXTAREA",“SELECT"元素,而且查找这些元素的逻辑递归向下的,一个都不放过。如何监听分为两种,一种是type为radio的INPUT元素,通过挂onclick事件得到通知,其它的元素都是挂onchange事件得到通知。如果需要在校验失败的时候把焦点置回去,那么还需要挂onblur事件。这样当这些元素的onclick或者onchange事件发生的时候,就会执行该Validator的校验逻辑。
以上是两个执行客户端校验的时机,下面给出两点建议:
1.
如果我们做一个Control,该Control具有引起页面Postback的能力,那么我们最好通过ScriptManager.GetPostBackEventReference()函数来注册客户端的WebForm_DoPostBackWithOptions
()脚本,该脚本不仅仅具备引起Postback的能力,还具有执行客户端校验的能力。如果自己通过Form.submit来做就和Asp.net体系结构结合的不是很紧密了。
2.
如何可以让系统的Validator Control可以用来在客户端(服务器端后面讲)校验我们的Control呢?
通过分析源码发现,标准的Validator总是递归查找待校验的Control所生成的Html
Elment节点上的value属性,然后拿着value属性进行校验。因此如果希望我们的Control可以用系统的Validator进行校验,就必须在生成的Html
Element里面有一个节点有value属性,而且该属性的值就是希望被校验的值。
3.
Validator的服务器端工作原理
由上面的分析,可以看出,BaseValidator类搭建了一套完整的客户端验证体系结构,但这并不是IValidator接口所必须的,IValidator接口定义的是服务器端的验证,那么在服务器端Validator如何和整个WebForm里面的Control进行协作工作的呢?我们从下面三个方面来阐述:Validate的调用入口,Validate的调用时机,Validate的执行过程
Validate调用入口
在Page上管理所有的IValidator对象的集合,并且由Page执行相应的Validate的调用,因此Validate调用的入口是在Page上面的。通过前面的介绍,我们了解到,当Validator在OnInit的阶段,它会把自己添加到Page的Validators的集合里面,而在OnUnload阶段它会把自己从Page的Validators集合里面移除。Page上关于Validate的调用方法有两个(如下),总体思想很简单,就是一个遍历。
Code
public
virtual
void
Validate()
{
this
._validated
=
true
;
if
(
this
._validators
!=
null
)
{
//
遍历页面所有的Validator,进行Validate的函数调用
for
(
int
i
=
0x0
; i
<
this
.Validators.Count; i
++
)
{
this
.Validators[i].Validate();
}
}
}
public
virtual
void
Validate(
string
validationGroup)
{
this
._validated
=
true
;
if
(
this
._validators
!=
null
)
{
ValidatorCollection validators
=
this
.GetValidators(validationGroup);
if
(
string
.IsNullOrEmpty(validationGroup)
&&
(
this
._validators.Count
==
validators.Count))
{
this
.Validate();
}
else
{
//
遍历页面所有validationGroup为指定值的Validator,进行Validate的函数调用
for
(
int
i
=
0x0
; i
<
validators.Count; i
++
)
{
validators[i].Validate();
}
}
}
}
Validate的调用时机
Validate的时机仅仅发生在PostBack阶段,在我前面的两篇文章中( Web
Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
,Web Control 开发系列(三)
解析IPostBackEventHandler和WebForm的事件机制
),我详细讲解了页面在PostBack过程中的两个重要阶段:1.
ProcessPostData, 2. ProcessPostEvent。如果对这个不是很了解的话,就需要复习复习了:-)
Validate的调用就是在这两个阶段执行的。 主要调用的地方有三处:
SomeControl.RasiePostDataChangedEvent()
SomeControl.RaisePostBackEvent(String)
Page.RaisePostBackEvent(NameValueCollection nameValueCollection)
当调用Page上面的Validate方法的时候,需要了解引起PostBack的起源,这样的起源大致有两类,一类是实现了IPostBackDataHandler或者IPostBackEventHandler接口的标准Asp.net
Control,另一类是Html的Input元素或者一些引发回传的脚本等。
为什么要了解引起PostBack的起源呢?因为有些引起PostBack的Control可能有个属性叫CauseValidation,当设置为false的时候,不希望引起Validation。而且还有的Control会有ValidationGroup这样的属性,来控制部分Control进行Validation,可见了解PostBack的起源对于调用Page.Validate()和Page.Validate(validationGroup)是至关重要的。而对于无法得到PostBack起源Control的情况,则一律简单调用Page.Validate()进行页面内所有未分组的Control的校验。
对于第一类,又有两种情况:实现了IPostBackDataHandler的Control,
实现了IPostBackEventHandler的Control,这些Control对Validate的调用分别在
SomeControl.RasiePostDataChangedEvent()
SomeControl.RaisePostBackEvent(String)
函数里面调用,其实现基本都是一直的,见下面的代码:
Code
//
下面是TextBox里面引发Page.Validation(validationGroup)调用入口的函数,其它的
//
Control,如CheckBox,RadioBox的实现和这个是一致的。
protected
virtual
void
RaisePostDataChangedEvent()
{
//
下面的条件是判断引起这次Postback的Control就是当前Control
if
(
this
.AutoPostBack
&&
!
this
.Page.IsPostBackEventControlRegistered)
{
//
设置Page上面的这个变量的唯一目的就是告诉Page,我已经做过Validation了
//
你不用再关心了
this
.Page.AutoPostBackControl
=
this
;
if
(
this
.CausesValidation)
{
this
.Page.Validate(
this
.ValidationGroup);
}
}
this
.OnTextChanged(EventArgs.Empty);
}
上面的代码是对于实现了IPostDataHandler接口的Control的处理。下面来看看实现了IPostBackEventHandler接口的Control的处理,以Button为列,其它的LinkButton,ImageButton
等Control实现差不多都这样
Code
protected
virtual
void
RaisePostBackEvent(
string
eventArgument)
{
//
先校验Event
base
.ValidateEvent(
this
.UniqueID, eventArgument);
//
程序走到这里,就认为引起PostBack的起源就是当前Control,所以如果当前Control的
//
的CauseValidation为true,那么调用页面的Page.Validate(validationGroup)函数进行
//
Validate操作
if
(
this
.CausesValidation)
{
this
.Page.Validate(
this
.ValidationGroup);
}
this
.OnClick(EventArgs.Empty);
this
.OnCommand(
new
CommandEventArgs(
this
.CommandName,
this
.CommandArgument));
}
上面解释了对于第一类情况,我们都能查找到引起PostBack的起源Control,那么对于第二类情况如何处理呢,这个时候是无法查到引起PostBack的Control的,看下面的代码:
Code
private
void
RaisePostBackEvent(NameValueCollection postData)
{
//
1. 假如已经在Page上显式的注册了引起PostBackEvent的Control,就直接处理
if
(
this
._registeredControlThatRequireRaiseEvent
!=
null
)
{
//
这个函数会直接调用Control上面的RaisePostBackEvent(String)函数,这个函数里面
//
会进行Page.Validate函数的调用。上面刚刚解释了。
this
.RaisePostBackEvent(
this
._registeredControlThatRequireRaiseEvent,
null
);
}
else
{
//
这部分代码,我自己按照Reflector反编译的结果重新组织了,但是逻辑
//
没有任何变化,只是方便阅读理解
//
2. 假如没有注册,就查找__EVENTTARGET记录的Control来处理
string
eventTarget
=
postData[
"
__EVENTTARGET
"
];
bool
hasEventTarget
=
!
string
.IsNullOrEmpty(eventTarget);
Control eventTargetControl
=
null
;
if
(hasEventTarget)
{
eventTargetControl
=
this
.FindControl(eventTarget);
if
((eventTargetControl
!=
null
)
&&
(eventTargetControl.PostBackEventHandler
!=
null
))
{
string
eventArgument
=
postData[
"
__EVENTARGUMENT
"
];
//
这个函数会直接调用Control上面的RaisePostBackEvent(String)函数,这个函数里面
//
会进行Page.Validate函数的调用。上面刚刚解释了。
this
.RaisePostBackEvent(eventTargetControl.PostBackEventHandler, eventArgument);
}
}
else
if
(
this
.AutoPostBackControl
==
null
)
{
//
这个AutoPostBackControl如果不为null,那么说明了AutoPostBackControl 在自己的
//
RaisePostDataChanged()函数里面已经调用了Page.Validate()函数,上面刚刚解释了。
//
这个时候属性无法查找到引起这次PostBack的真正起源Control,也许是一个Input元素
//
引起的,也许是一段脚本引起的PostBack,这样,我们就对于所有未分组的Validator就行
//
一次校验,因此调用Page.Validate()
this
.Validate();
}
}
}
如此我们已经对于三处调用Page.Validate方法的调用地方都进行了解释。可以很了解服务器端的Validate的调用时机。
Validate的执行过程
Validate的执行主要是调用接口IValidator.Validate()方法完成的,这个在BaseValidator里面已经做了一个基本的实现,可以看看下面的实现代码:
Code
public
void
Validate()
{
this
.IsValid
=
true
;
if
(
this
.Visible
&&
this
.Enabled)
{
this
.propertiesChecked
=
false
;
//
检查是否能在当前的NameContainer里面查找到待校验的Control
if
(
this
.PropertiesValid)
{
//
下面的方法是一个abstract方法,依赖于具体的Validator来实现自己的逻辑
this
.IsValid
=
this
.EvaluateIsValid();
//
控制Focus的位置,如果希望失败后把Focus保留在校验的Control上,那么就
//
通过Page的调用完成这个操作。
if
((
!
this
.IsValid
&&
(
this
.Page
!=
null
))
&&
this
.SetFocusOnError)
{
this
.Page.SetValidatorInvalidControlFocus(
this
.ControlToValidate);
}
}
}
}
为了让用户从BaseValidator派生的Control专注于实现Validate的具体逻辑,BaseValidator还默认实现了获得待校验Control的Valud的方法:
Code
protected
string
GetControlValidationValue(
string
name)
{
//
1. 在当前的NameContainer里面查找待校验的Control
Control component
=
this
.NamingContainer.FindControl(name);
if
(component
==
null
)
{
return
null
;
}
//
2. 查找Control上面需要用来校验的Property,它是通过一个Attribute来标识的。
PropertyDescriptor validationProperty
=
GetValidationProperty(component);
if
(validationProperty
==
null
)
{
return
null
;
}
//
3. 从Porperty上面取出要校验的Value
object
obj2
=
validationProperty.GetValue(component);
if
(obj2
is
ListItem)
{
return
((ListItem) obj2).Value;
}
if
(obj2
!=
null
)
{
return
obj2.ToString();
}
return
string
.Empty;
}
public
static
PropertyDescriptor GetValidationProperty(
object
component)
{
ValidationPropertyAttribute attribute
=
(ValidationPropertyAttribute) TypeDescriptor.GetAttributes(component)[
typeof
(ValidationPropertyAttribute)];
if
((attribute
!=
null
)
&&
(attribute.Name
!=
null
))
{
return
TypeDescriptor.GetProperties(component, (Attribute[])
null
)[attribute.Name];
}
return
null
;
}
因此,当执行Validate的时候,Validator会在当前的NameContainer里面查找需要校验的Control,然后自动取出Control上面需要校验的Property的Value,最后调用Validator自己的逻辑来校验这个Value。
总结
花了好多时间,终于写完了,总体来说微软的Validation机制还是比较强大的。
相关文章推荐
- [Joe 原创]Web Control 开发系列(四) Validation机制
- [Joe 原创]Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制
- [导入][Joe 原创]Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制
- Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制
- [Joe 原创]Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制
- Web Control 开发系列(一) 页面的生命周期
- [Joe 原创]Web Control 开发系列(一) 页面的生命周期
- 【转】Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
- Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
- ASP.NET Web Server Control 开发系列
- [Joe 原创]Web Control 开发系列(一) 页面的生命周期
- Web Control 开发系列 之 前言
- [Joe 原创] Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
- ZT Web Control 开发系列(一) 页面的生命周期
- [Joe 原创] Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
- Web Control 开发系列(一) 页面的生命周期
- Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler
- [Joe 原创]Web Control 开发系列(一) 页面的生命周期
- Web Control 开发系列(一) 页面的生命周期
- Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler