zoukankan      html  css  js  c++  java
  • [Joe 原创]Web Control 开发系列(三) 解析IPostBackEventHandler和WebForm的事件机制

           WebForm最大的魅力大概就是它自己的一套事件处理机制了,要做一个好的Control,必须深入理解这套机制,只有这样才可以让我们的Control有一整套Professional的Event,而IPostBackDataHandler和IPostBackEventHandler是实现事件机制的核心接口,在我的上一篇文章(Web Control 开发系列(二) 深入解析Page的PostBack过程和IPostBackDataHandler)中已经介绍了IPostBackDataHandler的实现原理,本章主要介绍IPostBackEventHandler.最后进行总结,来看看Web Control整个事件机制的全貌。        要了解IPostBackEventHandler接口,首先就必须了解.net Framework的脚本注册原理和一些重要的内置脚本。   一、脚本注册          .net Framework可以在我们毫不知情的情况下,根据我们在服务端对Control属性的设置,在ControlRender的时候,根据需要动态向客户端注册脚本和Hidden的<input>元素,Hidden的<input>用来在客户端保存一些重要的信息,而脚本是用来完成一些逻辑行为的控制。我们先看看.net Framwork是如何实现脚本和Hidden <input>的.             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()如下:
    复制代码
    <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字段也会同步注册
    <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,会发现都为“”。 二、使用脚本进行PostBack的Control分析
    注意首先要说明的一点是在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,那么意味着仅仅进行提交,并不校验,所以这个时候,生成的代码是
    <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是单线程的,这样的调用会自动进入调用队列,等待执行,不会阻塞现在的调用)。
    三、服务器端处理       通过上面的分析知道,.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的分析:
    复制代码
    privatevoid 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);                     }                 }                 elseif (this.AutoPostBackControl ==null)                 {                     // 这个AutoPostBackControl的标记设置为了不重复做Validate,后面我在讲述                     // Validation机制的时候会介绍                     this.Validate();                 }             }         }
    复制代码
    四、Composite Control 的冒泡事件            在Control上面有一个方法RaiseBubbleEvent,这个方法就是沿着Control Tree向上一次调用OnBubbleEvent函数,知道返回true,就推出,是一个典型的冒泡事件。Control对于OnBubbleEvent的实现是简单的返回false,也就是说如果我们不做处理,那么事件会不停的向上冒泡知道最顶端的Page。
    复制代码
    protectedvoid 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的事件。
    五、总结      所有WebForm事件的根源依赖于Form的submit()执行而引起PostBack(CallBack这里不考虑),而引起PostBack主要依赖于Html Input (type="image" or "submit")元素和脚本。      然后在PostBack阶段分析数据,如果数据变化可以Raise相关的Event,如果客户端记录了谁发了Event,也可以发Event。如果想让Event冒泡,就call RaiseBubbleEvent     
  • 相关阅读:
    leetcode Remove Linked List Elements
    leetcode Word Pattern
    leetcode Isomorphic Strings
    leetcode Valid Parentheses
    leetcode Remove Nth Node From End of List
    leetcode Contains Duplicate II
    leetcode Rectangle Area
    leetcode Length of Last Word
    leetcode Valid Sudoku
    leetcode Reverse Bits
  • 原文地址:https://www.cnblogs.com/qianyz/p/2485615.html
Copyright © 2011-2022 走看看