Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制


      WebForm编程过程当中,若是咱们但愿向客户端输出脚本或者一些Hidden的<input>元素,咱们一般是经过 Page.ClientScript对象完成的,这个对象是一个ClientScriptManager类型的实例,咱们通常(也有特殊状况)在Control.OnPreRender()方法里面调用Page.ClientScript.RegisterHiddenFiled或者Page.ClientScript.RegisterStartScript,还能够得到一些内置的脚本,好比 Page.ClientScript.GetPostBackEventReference,这些方法的调用都会记录一些标记数据,真正输出到客户端,是在Page的Render方法调用的时候,而完成输出的是下面两个方法:
   
   Page.BeginFormRender 和Page.EndFormRender
     这两个方法会在HtmlForm.RenderChildren里面调用,用来给<form>的开始和结束位置添加一些脚本和hidden field。具体完成的功能有:

    * Render全部Register的Hidden Fields,同时也Render用来保存ViewState的Hidden Field
    * Render用来保存当前 <form>的滚动位置的Hidden Field和Start Javascript
    * Render控制当前焦点的Focus.js脚本引用语句
    * Render用来执行回调的__doPostBack()函数,仅仅在相关标记打开的时候才会Render.
    * Render用来执行PostBack的WebForms.js脚本引用语句。这个文件主要包含了WebForm经常使用的脚本,有PostBack的脚本和CallBack的脚本。
    * Render已经注册的脚本块(Script Block)
    * Renderl Client Startup Script(启动即执行的脚本)

__doPostBack()脚本
Render出来的__doPostBack()以下:javascript

复制代码
< script type = " text/javascript " >
// <![CDATA[
var  theForm  =  document.forms[ ' form1 ' ];
if  ( ! theForm)  {
    theForm 
= document.form1;
}

function  __doPostBack(eventTarget, eventArgument)  {
    
if (!theForm.onsubmit || (theForm.onsubmit() != false)) {
        theForm.__EVENTTARGET.value 
= eventTarget;
        theForm.__EVENTARGUMENT.value 
= eventArgument;
        theForm.submit();
    }

}

// ]]>
< / script>
复制代码

其实一旦用户注册了__doPostBack函数,两个配套的Hidden字段也会同步注册html

< input  type ="hidden"  name ="__EVENTTARGET"  id ="__EVENTTARGET"  value =""   />
< input  type ="hidden"  name ="__EVENTARGUMENT"  id ="__EVENTARGUMENT"  value =""   />

上面的代码就是先把Event target和Event Argument存入指定的Hidden Field中,而后调用<form>的submit方法来提交数据。这段脚本是Framework提供的最普通的引起PostBack的脚本,咱们写Control的时候能够经过Page.ClientScript.GetPostBackEventReference来得到这个脚本(注意,这个方法有好几个重载的版本,其中当选用了一些参数的时候,也可能得到另外一个脚本WebForm_DoPostBackWithOptions,下面将介绍)。

WebForm_DoPostBackWithOptions脚本
还有一个比较重要的脚本就是WebForm_DoPostBackWithOptions,它的做用比__doPostBack更强,好比对于支持CauseValidation属性的Control,如Checkbox,TextBox,这些Control当CauseValidation为true的时候,它就会在onclick属性里面Render出WebForm_DoPostBackWithOptions脚本。这样能够在调用theForm.submit()方法以前执行当前Form的全部Validator的客户端校验。
      因此事实上,WebForm_DoPostBackWithOptions是包容__doPostBack函数的,凡是注册了WebForm_DoPostBackWithOptions的地方,必须注册__doPostBack,由于WebForm_DoPostBackWithOptions里面在执行完不少附加功能的代码(如对全部的Validator进行校验,控制Focus在没有经过校验的Control等)后,若是一切正常而且须要作ClientSubmit(Button当UseSumitBehavior为true和ImageButton不使用Client Script Submit,它们本身提交,由于它们本身输出的就是能够触发<form>提交的<input>元素),就调用__doPostBack。只有调用__doPostBack才会给__EVENTTARGET和__EVENTARGUMENT设置值,因此Button(UseSumitBehavior为true)和ImageButton引发的回传的时候,咱们观察__EVENTTARGET和__EVENTARGUMENT,会发现都为“”。

2、使用脚本进行PostBack的Control分析
java

注意首先要说明的一点是在HTML的表单域里面,具有自动让<form>想服务器端发送数据,引发回传的只有两个:
   <input type="submit">
   <input type="image">

Button
这是个特殊的Control,由于它们自己具有了自动调用<form>submit的能力,Button上面有一个属性叫UseSubmitBehavior,这个属性用来控制生成的<Input>的type是"submit"仍是"button",若是为true,那么就是"submit".
1. 若是CauseValidation为false,UserSubmitBehavior为true,那么意味着仅仅进行提交,并不校验,因此这个时候,生成的代码是web

< input  type ="submit"  name ="Button1"  value ="Button"  id ="Button1"   />

没有任何onclick的脚本调用。
2. 若是CauseValidation为true, UseSubmitBehavior为true,那么生成的代码是编程

< input  type ="submit"  name ="Button1"  value ="Button"  onclick ="javascript:WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions(&quot;Button1&quot;, &quot;&quot;, true, &quot;&quot;, &quot;&quot;, false, false))"  id ="Button1"   />

由于要执行校验,因此必须调用WebForm_DoPostBackWithOptions方法。
3. 若是CauseValidation为false,UseSubmitBehavior为false,那么生成的代码是服务器

< input  type ="button"  name ="Button1"  value ="Button"  onclick ="javascript:__doPostBack('Button1','')"  id ="Button1"   />

这个时候,由于type是“button”,它不具有submit数据的能力,因此只能用脚本帮助解决,同时不须要校验,因此就使用__doPostBack函数,若是CauseValidataion为true,onclick里面的函数就为WebForm_DoPostBackWithOptions,由于__doPostBack不具有客户端校验的能力。

ImageButton
     imageButton 比较特别的是同时实现了IPostBackDataHandler, IPostBackEventHandler, 在Asp.net2.0里面只有两个Control同时实现了这两个接口,一个是ImageButton,一个是HtmlInputImage。这两个Control最后生成的都是<input type="image">.
     对于这个HTML元素,它在点击的时候,会引发<form>submit数据,同时会把当前点击的位置做为两个表单域被<form>收集。好比:<input type="image" name="myImageButton">,那么点击到(20,100)的时候,<form>表单域里面会多出两个:["myImageButton.x"] = "20",["myImageButton.y"] = "100".
     经过上面的介绍,咱们知道<input type="image">提交的数据和它的Name并不一致,根据我上一篇文章(Web Control 开发系列(二) 深刻解析Page的PostBack过程和IPostBackDataHandler)的介绍,咱们知道它必须使用注册的方法才能保证page在处理Request的时候调用当前Control的IPostBackDataHandler接口。具体作法就是在PreRender的时候调用Page.RegisterRequiresPostBack(this).
    同上面介绍的Button同样,当CauseValidation为true的时候,会给输出加一个onclick="WebForm_DoPostBackWithOptions"的属性设置,这样在提交前可使用脚本进行校验。不然就不会生成脚本。

CheckBox,TextBox,RadioButton,ListControl及其派生类 等
    这些Control输出的Html元素都没有自动submit的能力,因此这些Control普通状况下是不会引起回传的,可是为了方便用户,.net Framework在上面暴露了一个属性叫AutoPostBack,一旦这个属性为true,就表示这些Control具有了引起回传的能力,具体怎么实现回传呢,仍是依赖于上面介绍的两段脚本。
     当AutoPostBack为true,CauseValidation为true的时候就注册WebForm_DoPostBackWithOptions. 当AutoPostBack为true,CasuseValidataion为false的时候就注册 __doPostBack函数,同时在AutoPostBack属性为true的时候,为了防止性能问题,通常注册的脚本都用setTimeout(函数名,0)包起来,这样能够认为是一个模拟的异步调用(事实JavaScript是单线程的,这样的调用会自动进入调用队列,等待执行,不会阻塞如今的调用)。

3、服务器端处理
      经过上面的分析知道,.net Framework通常是经过注册脚本,在脚本里面调用theForm.submit()来进行提交的,这样就造成了一次PostBack,而能够致使回传的两个重要的脚本也已经在上面介绍了。那么当脚本致使回传了,服务器端是如何处理请求并引起Control的事件的呢?
      经过我第一篇文章(Web Control 开发系列(一) 页面的生命周期)的介绍,咱们知道在页面Load阶段结束后,若是Page.IsPostBack,咱们会先进入 IPostBackDataHandler的处理阶段,而后才进入IPostBackEventHandler处理阶段。咱们下面分析的就是 IPostBackEventHandler处理阶段的逻辑。这个逻辑是经过 Page.RaisePostBackEvent(NameValueCollection postData)进入的。
      在这个函数的处理里面有好几种状况:
      1. 其中最普通的一种处理是经过postData["__EVENTTARGET"]postData[“__EVENTARGUMENT”]拿到相应的 值,这些值都是在<form>提交前经过脚本设置上去的,而后经过Page.FindControl来找到合适的Control,这样就可 以取到Control.PostBackEventHandler,而后调用 IPostBackEventHandler.RaisePostBackEvent方法,就致使Control的服务端事件被触发。
       2. 还有一种状况,就是服务器端的Control是Button或者ImageButton,它们的提交是经过Html元素本身的能力,因此提交发生的时候,没有任何脚本调用,天然postData["__EVENTTARGET"]和 postData[“__EVENTARGUMENT”]都为"",这个时候咱们如何找到引起PostBack的Control而且调用它的 IPostBackEventHandler接口的方法呢?
           这就要利用另外一种发事件的机制——注册机制。这个机制主要经过 Page.RegisterRequiresRaiseEvent(IPostBackEventHandler control)函数实现的,这个函数在Asp.net2.0中有三个地方调用:异步

  •      HtmlInputImage.LoadPostData,
  •      ImageButton.LoadPostData,
  •      Page.ProcessPostData

     这三个地方的调用都是在处理PostBackData阶段,所以咱们能够认为这个注册机制最好在处理PostBackData阶段使用是比较符合规范的。
     对于HtmlInputImage和ImageButton这两个Control,它们都有PostBackData,并且经过注册的方法实现了IPostBackDataHandler接口,因此在LoadPostData阶段调用Page.RegisterRequiresRaiseEvent,这样就显式的告诉Page在PostBackEvent处理阶段调用本身的IPostBackEventHandler接口,就实现了服务端Click事件的触发。

     那么Page.ProcessPostData函数(在个人上一篇文章Web Control 开发系列(二) 深刻解析Page的PostBack过程和IPostBackDataHandler有介绍),它会收集全部的表单提交数据,若是有和这个数据对应的Control(经过Page.FindControl查找),那么就设法调用其IPostDataHandler,若是IPostDataHandler为null,那么设法取其IPostEventHandler,若是不为null,那么就调用Page.RegisterRequiresRaiseEvent函数来注册它。Button只实现了IPostBackEventHandler接口,没有实现IPostBackDataHandler接口,因此就经过这种发式来触发事件的。

     一旦在Page上进行了Page.RegisterRequiresRaiseEvent注册,系统就不会关心postData["__EVENTTARGET"]和postData["__EVENTARGUMENT"]了,直接就调用注册的IPostBackEventHandler.RaisePostBackEvent方法。

上面介绍的内容都是对Page.RaisePostBackEvent的分析:函数

复制代码
private   void  RaisePostBackEvent(NameValueCollection postData)
        
{
            
// 1. 假如已经在Page上显式的注册了引发PostBackEvent的Control,就直接处理
            if (this._registeredControlThatRequireRaiseEvent != null)
            
{
                
this.RaisePostBackEvent(this._registeredControlThatRequireRaiseEvent, null);
            }

            
else
            
{
                
// 这部分代码,我本身按照Reflector反编译的结果从新组织了,可是逻辑
                
// 没有任何变化,只是方便阅读理解

                
// 2. 假如没有注册,就查找__EVENTTARGET记录的Control来处理
                string str = postData["__EVENTTARGET"];
                
bool flag = !string.IsNullOrEmpty(str);
                Control control 
= null;
                
if (flag)
                
{
                    control 
= this.FindControl(str);
                    
if ((control != null&& (control.PostBackEventHandler != null))
                    
{
                        
string eventArgument = postData["__EVENTARGUMENT"];
                        
this.RaisePostBackEvent(control.PostBackEventHandler, eventArgument);
                    }

                }

                
else if (this.AutoPostBackControl == null)
                
{
                    
// 这个AutoPostBackControl的标记设置为了避免重复作Validate,后面我在讲述
                    
// Validation机制的时候会介绍
                    this.Validate();
                }

            }

        }
复制代码


4、Composite Control 的冒泡事件
            在Control上面有一个方法RaiseBubbleEvent,这个方法就是沿着Control Tree向上一次调用OnBubbleEvent函数,知道返回true,就推出,是一个典型的冒泡事件。Control对于OnBubbleEvent的实现是简单的返回false,也就是说若是咱们不作处理,那么事件会不停的向上冒泡知道最顶端的Page。post

复制代码
protected   void  RaiseBubbleEvent( object  source, EventArgs args)
{
    
for (Control control = this.Parent; control != null; control = control.Parent)
    
{
        
if (control.OnBubbleEvent(source, args))
        
{
            
return;
        }

    }

}


复制代码

     咱们知道了这个冒泡的机制,那么冒泡的源头在哪里呢??这就是咱们作Control的人要考虑的,若是咱们但愿咱们的Control的Event支持冒泡,那么咱们就应该在Control的Event发生的时候调用RaiseBubbleEvent这个函数,这样当别人在一个复合控件里面使用咱们的Control的时候,它就能够在外面接收到咱们Control发的冒泡事件,目前调用了这个冒泡函数的Control有

     从上面,咱们最值得注意的是有三个简单Control实现了向上冒泡:Button, ImageButton, LinkButton,其它的都是一些复合Control在OnBubbleEvent里面进行二次冒泡。所以若是咱们作一个复合的Control,咱们能够在最外层的OnBubbleEvent函数里面监听这个Control内部的全部的Button,ImageButton,LinkButton的事件。

5、总结
     全部WebForm事件的根源依赖于Form的submit()执行而引发PostBack(CallBack这里不考虑),而引发PostBack主要依赖于Html Input (type="image" or "submit")元素和脚本。
     而后在PostBack阶段分析数据,若是数据变化能够Raise相关的Event,若是客户端记录了谁发了Event,也能够发Event。若是想让Event冒泡,就call RaiseBubbleEvent性能

相关文章
相关标签/搜索