AJAX在ASP.NET中的应用(四)——探寻UpdatePanel的工作原理

      前言:ASP.NET的AJAX_Extensions虽然用起来很方便,也能应付大部分的开发需求,但是如果对它的实现机制一点也不了解的话,编写程序的时候总有种被微软绑住手脚的感觉。于是,我花了几天时间大致了解了UpdatePanel实现AJAX的方法,也基本摸清了头脑,这里把我的所想所得写出来,希望能给读者带来一些帮助。当然,我仅仅是从整体布局上的了解,具体细节了解不多,因此这里也不能将其所有机制完全了解透彻,敬请见谅;如果读者有任何进一步的见解也请不吝指教。

      PS:本文不少地方都会涉及到ASP.NET页面周期的知识,以及一些Web开发工具的使用方法,本文假设读者已经具有相关的基本知识,这些额外的辅助知识本文将只会简要说明。

     一,ScriptManager的工作原理

      ScriptManager是实 现AJAX的核心控件,先对它进行分析。在VS上创建一个空WebFrom,并在一个空页面上只放置ScriptManager,启动后在解决方案资源管理器中可以看到页面加载了三个未知文件,然后查看生成的页面的源文件,结果如下。



      可以看到,除了添加ASP.NET本身的完成页面周期而需要的必要标签外(如__VIEWSTATE),还加载了三个JS脚本文件,可以肯定这些js文件就是微软为傻瓜化Ajax编程而封装好的,其中包含的方法与ASP.NET页面内部机制配合完成Ajax的请求过程。

      首先来找找这几个文件从哪里冒出来的。从三个JS的地址来看,它们都是被嵌入的程序集中的资源文件中,因此可以通过反编译文件来找到它们。我用Reflector来分析System.Web程序集, 可以看到程序集有如下的结构,

    

其中Recources就是嵌入到该System.Web程序集的资源文件列表。因为其中js文件不多,所以只要一个个查看对比就能发现其中的WebForm.js与WebResource.axd?d=1Sl8iwPjUX………………文件代码一致。如下图所示:

      后面两个JS文件很容易找到,因为在这两个文件头部就有注释,如图所示,可以看出这两个文件中都位于System.Web.Extensions程序集中:


     反编译System.Web.Extensions程序集,结果如下:

     

       实际上,反编译System.Web.Extensions程序集后查看起资源文件,可以看到一些列命名整齐的js文件,从名称中包含Ajax的命名方式上看,这些文件应该都是实现满足各种情况下的Ajax功能而存在的。还可以发现一个现象,相同名称的js文件都有两个版本,如MicrosoftAjax.js和MicrosoftAjax.debug.js,熟悉Jquery的开发者肯定能看出,一个用于发布环境,一个用于测试环境(带有debug标志)。

      二,UpdatePanel的工作原理

      在分析UpdatePanel的工作原理之前,必须先对ASP.NET的页面周期和页面回发(Postback)机制有个最基本的认识。

      页面周期,顾名思义就是从客户端请求页面到服务器生成最终页面的整个生命过程,微软将其对ASP.NET开发者公开,而不是隐蔽地执行(微软完全可以这样设计,就像微软的很多产品一样),目的在于让ASP.NET开发者能够通过在页面生命周期的各个阶段根据需要添加特定的功能,从而灵活地控制最终呈现(render)给客户的页面,以满足各种各样的需求。

      严格来说,页面回发(Postback)机制属于页面生命周期的一部分,表单(form)是ASP.NET的基础,通过将form内的各个input元素(包括ASP.NET自身为保存视图状态生添加的名为”__VIEWSTATE”的hidden元素和为验证客户端请求合法性的”__EVENTVALIDATION”等等)的value作为form的元素传回给服务器,服务器在页面周期的Init阶段根据这些参数生成各个服务器控件,在运行玩Load、Render、SaveState、Unload之后销毁这些服务器控件的实例,而将已经Render的最终页面发送给客户端,客户端就将这个最终页面显示在浏览器上。

      上述就是一个普通的ASP.NET页面(这里指的就是非Ajax操作)的一次PostBack过程,可以看到在这个过程中,浏览器会一直处于等待相应状态,用户除了等待之外没有其他可以做的了,而且这种普通的PostBack回传,然后在经历一次页面生命周期再返回,实际上是非常浪费效率的办法。一来一般传传递服务器的参数根本不需要这么多,大多数时候客户的操作都是小范围的,需要更新的部分也仅占整个页面的很小一部分,form中input中的大部分值都是不需要传递的,浪费宽带,这种浪费尤其在客户访问量大的时候性能影响明显。二来,无需将整个页面返回给客户端,只需将需要更新的部分返回就可以了。

      现在,先来进行一次普通的Postback过程,用于待会与Ajax请求进行比较。

      前台页面代码如下:

    <form id="form1" runat="server">
    <asp:TextBox ID="asptbx_Content" runat="server"></asp:TextBox>
    <asp:Button ID="aspbtn_Trigger" runat="server" Text="Button" 
        οnclick="aspbtn_Trigger_Click" />
    </form>
      后台按钮事件方法如下:

        protected void aspbtn_Trigger_Click(object sender, EventArgs e)
        {
            asptbx_Content.Text += new Random().Next(100) + "_";
        }

      使用工具httpwatch记录一次当点击按钮Postback时的传递参数和接收到的内容,结果如下:

      可以看出,点击按钮后通过表单传递给服务器的值,有四对:__EVENTVALIDATION(用户验证客户端请求合法性)、__VIEWSTATE(用于保存视图状态)、aspbtn_Trigger(触发这次Postback的按钮ID)、asptbx_Content(文本控件的ID),而服务器端经历完页面生命周期后就将最终呈现页面返回给客户断,可以看到经过处理后asptbx_Content的文本从空变为”36_”,正是执行代码【asptbx_Content.Text += new Random().Next(100) + "_";】之后的变化,最后客户端接收到新页面,并由浏览器呈现出来。

      现在,保持也没后台方法不变,前台加上Ajax控件,使得点击按钮事件之成为Ajax请求,修改之后如下:

    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server">
    </asp:ScriptManager>
    <asp:UpdatePanel ID="UpdatePanel1" runat="server">
        <ContentTemplate>
            <asp:TextBox ID="asptbx_Content" runat="server"></asp:TextBox>
            <asp:Button ID="aspbtn_Trigger" runat="server" Text="Button" OnClick="aspbtn_Trigger_Click" />
        </ContentTemplate>
    </asp:UpdatePanel>
    </form>

     再次点击按钮并用httpwatch记录,结果如下:

      这次请求传递的参数增加了__ASYNCPOST:true的参数,作用在于告诉服务器这是一次Ajax请求;还有ScriptManager1: UpdatePanel1|aspbtn_Trigger,表示由ID为UpdatePanel1的UpdatePanel控件内的aspbtn_Trigger引发Postback。同时,服务器端返回的结果有了根本的变化,这次不是将最终页面返回,而是返回了一系列字符串,这些字符串就是经过排列整理的由客户端相关js脚本执行的“指令”。而执行这些字符串命令的JS正式本文前面说明的由ScriptManager生成的那三个JS文件,这三个JS内的方法通过将这写“指令”进行“解密”,然后对目标进行局部更新操作,这就是UpdatePanel控件的大致实现机制。

      当然,可以通过仔细研究这三个JS文件内的方法来弄透其内部具体是怎样实现的,但由于我精力有限,Javascript语法结构又复杂,不像C#明了,因此这里只进行最简单的说明。查看页面源文件可以看到如下语句:Sys.WebForms.PageRequestManager._initialize('ScriptManager1','form1', ['tUpdatePanel1','UpdatePanel1'], [], [], 90,'');那么就从_initialize方法研究,可以在第三个js文件中找到:

     通过_initialize方法可以追溯Sys.WebForms.PageRequestManager.getInstance方法,如下:


    以此又可以追溯到Sys.WebForms.PageRequestManager方法,可以看到:

      可以看出该方法仅仅是为了生成目标对象,并没有实质的操作方法。回过头来看prm._initializeInternal.apply(prm, arguments),可以看出生成了目标对象后执行该对象的_initializeInternal,并将原_initialize方法的参数列表(这里就是['ScriptManager1','form1', ['tUpdatePanel1','UpdatePanel1'], [], [], 90,''])作为参数,于是追溯prm 对象的_initializeInternal方法,可以找到:

_initializeInternal:Sys$WebForms$PageRequestManager$_initializeInternal,从而寻找Sys$WebForms$PageRequestManager$_initializeInternal:

    下面的截图就是该方法的主要部分:

      通过this._onFormSubmitHandler= Function.createDelegate(this,this._onFormSubmit);与 Sys.UI.DomEvent.addHandler(this._form,'submit',this._onFormSubmitHandler);这两句可以看到:初始化页面的时候页面的时候就已经将页面上form的提交事件方法替换成this._onFormSubmit了,所以也就是Sys$WebForms$PageRequestManager$_onFormSubmit方法。为了验证该结论,可以在设置断点,如下图所示:


     当点击按钮后,程序立刻运行至该断点处,验证了上述结论。

     在方法_onFormSubmit中我们还看到,方法内部收集form表单中的input值对,并在结尾添加”__ASYNCPOST=true&”,如下图所示:

    在formBody.append("__ASYNCPOST=true&")后设置断点,以读取formBody的值,结果如下:


     截图看不清楚,这里将值写出来:ScriptManager1=UpdatePanel1%7CButton1&__EVENTTARGET=&__EVENTARGUMENT=&TextBox1=_67_19_80_12_95&__VIEWSTATE=%2FwEPDwULLTE1ODE5NzM4NTFkZJMvrPl0%2F0L1qATerUkMz%2B9KwqhQ1xSG4xNhdH3vvtVX&__EVENTVALIDATION=%2FwEWAwL4x9GwBALs0bLrBgKM54rGBpDV2R16XziDyN41tVgFDLmIwKtILoBzu18g%2BqXPIcU6&__ASYNCPOST=true&。可以看出来,这与使用HttpWatch监视的PostData一致(这里TextBox1与asptbx_Content是两次测试使用文本框的ID,值也不同,原因在于两次测试的环境不同)。到这里客户端的行为大致了解一二了,服务端的行为从这几个js文件中无从知晓,现在只剩下找到客户端接收到服务端传回的“已加密“的字符串后如何解析并操作页面元素实现Ajax更新了。   

      从方法命名中可以猜测,在Sys$WebForms$PageRequestManager$_onFormSubmit方法后面紧跟的Sys$WebForms$PageRequestManager$_onFormSubmitCompleted方法,应该就是提交Ajax请求成功后的回调方法,在该方法内部设置断点,同样证明了该猜测。同时,添加对参数sender的监视,结果如图:


      Sender对象包含了两个重要的属性:_webRequest和_xmlHttpRequest。查看这两个的值可以看出来,_webRequest包含了对本次Ajax请求的参数设置,如_body是表单数据集合,_timeout是请求时效。而_xmlHttpRequest则很明显就是实现Ajax请求的基本XMLHttpRequest对象,查看该对象值发现,readyState为4,status为200,表示本次请求成功,而返回的responseText则为看似为乱码的“1|#||4|167|updatePanel|UpdatePanel1|\r\n            <inputname=\"TextBox1\" type=\"text\" value=\"_81\"id=\"TextBox1\" />\r\n           <input type=\"submit\" name=\"Button1\"value=\"Button\" id=\"Button1\" />\r\n       |68|hiddenField|__VIEWSTATE|/wEPDwULLTE1ODE5NzM4NTFkZJMvrPl0/0L1qATerUkMz+9KwqhQ1xSG4xNhdH3vvtVX|72|hiddenField|__EVENTVALIDATION|/wEWAwL4x9GwBALs0bLrBgKM54rGBpDV2R16XziDyN41tVgFDLmIwKtILoBzu18g+qXPIcU6|0|asyncPostBackControlIDs|||0|postBackControlIDs|||26|updatePanelIDs||tUpdatePanel1,UpdatePanel1|0|childUpdatePanelIDs|||25|panelsToRefreshIDs||UpdatePanel1,UpdatePanel1|2|asyncPostBackTimeout||90|0|formAction|||”,这个字符串就是服务器的返回值,但它还是经过简单加密的,需要经过js方法解密后才能使用。

    于是,接着进行一步调试,直到下面的这个方法:

   

    对该方法的返回值data进行监视,结果如下:


      可以看出this._parseDelta方法就是负责将服务端的加密的字符串进行解密,并拆分成几个目标对象,如hiddenFieldNodes是类型为hidden的input元素,包括记录视图状态的” __VIEWSTATE”和记录” 验证客户端请求合法性”的“__EVENTVALIDATION”;panelsToRefreshNode是需要更新的UpdatePanel控件;updatePanelNodes是对应更新UpdatePanel的值等等。而接下来的方法应该就是根据这些已解密的对象来实施更新了,如下图所示:

     

      总结:页面的Submit事件被Sys.WebForms.PageRequestManager对象拦截,PageRequestManager会判断是传统提交还是Ajax提交,如果是Ajax提交则收集form表单的原有值对,并添加__ASYNCPOST=true告诉服务器本次请求是以Ajax方式提交。需要注意的一点是:无论是传统提交方式后还是Ajax提交,服务器端都会将页面的生命周期走一遍,然后再判断是否有__ASYNCPOST=true标志,如果有,则返回更新目标UpdatePanel的需要的数据,并按一定的方式排列;否则返回整个页面的HTML编码。客户端收到服务端的返回之后,如果是AJax提交,则将服务端的字符串解码,并用Javascript代码对目标UpdatePanel进行局部更新;如果是传统提交,则将浏览器转到全新的页面中。

     根据上述总结,本人给出以下ASP.NET编程建议:

     1,如果不需要保留页面的状态(这里指不会点击浏览器的“后退”按钮),则尽量使用Ajax方式提交。原因在于:传统的提交方式中服务器端会返回整个页面的编码,实际上的交互根本不需要这么大的数据量来表达,尤其在页面非常大的情况下,如果每次Postback都获取一下几百K的数据,服务端的宽带是吃不消的。而Ajax方式提交时服务端的返回字符串非常有限。

     2,尽量使用多个UpdatePanel包含多个部分,而不是整个页面就一两个UpdatePanel,且UpdateMode尽量设置为Conditional。原因与建议1类似,都是为了缩小服务端返回的数据量。当整个页面只使用一个UpdatePanel的话,实际上服务端的返回值会包含整个UpdatePanel的HTML编码,基本就相当于整个页面了,那么这与使用传统方式提交的数据量相差也就不大了;再者,如果UpdatePanel的UpdateMode都设置为Always,情况与整个页面只使用一个UpdatePanel是类似的,因为每次页面的任何一处PostBack,所有的UpdatePanel都要更新,那么服务端返回的字符串都要包含这些UpdatePanel的HTML编码,数据量不能有效缩小。因此,页面中相互关联的控件分别使用同一个UpdatePanel,分隔成多个UpdatePanel,然后每个UpdatePanel的UpdateMode设置为Conditional是明智之举。

     3,到底是使用“原始的”Ajax,Jquery-Ajax还是微软的Ajax-Extensions就要看具体的需求了,不过做项目建议还是不要用这“原始的”Ajax了,只需要了解其原理就可以了,因为可读性和可维护性都比较差。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值