原文:http://www.blogcn.com/User8/flier_lu/blog/28981917.html
1.2 UpdatePanel 与局部重绘模式 (Partial Rendering Mode)
在上一节介绍 Altas 整体结构时曾经提到,可以在启用局部重绘模式的情况下,通过通过 <altas:UpdatePanel .../> 标签定义需要异步更新的范围。
我们知道,传统的 HTTP 协议应用场景中,客户端在用户点击 submit 提交 form 的时候,一个 GET/POST 请求被发送到后台服务器;服务器则根据 form 的 action 指定页面,调用相应的处理者返回 HTML 格式的文本;返回结果并最终由客户端在浏览器中绘制,通常导致浏览器一次明显的刷新。
这种模式从 Web 早期的 CGI 一直沿用到现在的 ASP.NET 中。其优点是简单易用且较为成熟,缺点则是刷新明显且速度慢。因为一个页面中可能大多数内容在此次请求中是无需改变的,而这部分冗余的内容在每次请求都会被反复传输。尤其是对一些交互性较强的页面,每个操作都沿用这个冗长的流程,响应速度和负载都是难以容忍的。期间大家也想过很多缓解方法,例如使用 iframe 等嵌入帧封装独立部件,或者在服务器端针对不同区域进行缓存等等,但因为都还是基于这个传统思路,无法从本质上解决问题。
而对遵循 AJAX 思想的 Altas 框架,则是大大迈出一步,真正实现按需出发进行重绘。
首先,页面在定义时可以根据逻辑被分成若干个更新区域,通过 <altas:UpdatePanel .../> 标签直接定义。
其次,Altas 将接管 ASP.NET 客户端的顶级 Post Back 用 form,并针对局部重绘模式加入特定的参数。
然后,Altas 将接管 ASP.NET 服务器端的页面重绘方法。如果是在局部重绘模式下,则对客户端请求进行解析,并判断需要对那些区域进行重绘。可以通过在 UpdatePanel 中指定重绘条件,来避免不必要的重绘操作。
最后,重绘的结果会被封装成 XML 脚本,通过异步的 XMLHTTP 方式传递会客户端。客户端 Altas 引擎对返回内容进行解析后,更新到页面的相应控件上。
整个过程完全由 Altas 自动在后台完成,不会对前台页面造成刷新或其它的影响。
接下来我们来详细了解一下,Altas 是如何完成这一奇妙的功能。
首先,在 ScriptManager 启用局部重绘模式后(ScriptManager.EnablePartialRendering = true),可以通过 UpdatePanel 定义任意多个更新区域,例如:
1
<
atlas:UpdatePanel
runat
="server"
ID
="UpdatePanel1"
>
2
<
ContentTemplate
>
3
<
strong
><
span
style
="text-decoration: underline"
>
Shipping Address
</
span
>
:
</
strong
>
4
<
br
/>
5
<
asp:Label
ID
="lblFirstLineShipping"
runat
="server"
Font-Bold
="False"
></
asp:Label
><
br
/>
6
<
asp:Label
ID
="lblSecondLineShipping"
runat
="server"
></
asp:Label
><
br
/>
7
<
asp:Label
ID
="lblThirdLineShipping"
runat
="server"
></
asp:Label
><
br
/>
8
</
ContentTemplate
>
9
</
atlas:UpdatePanel
>
|
|
更新区域的实际内容,在 ContentTemplate 属性定义。UpdatePanel.ContentTemplate 是一个 ITemplate 接口类型的属性。ASP.NET 通过此接口来定义服务端控件与其子控件的关系,定义如下:
1[ParseChildren(true), PersistChildren(false)] 2public class UpdatePanel : Control 3{ 4 [TemplateInstance(TemplateInstance.Single), PersistenceMode(PersistenceMode.InnerProperty)] 5 public ITemplate ContentTemplate { get; set; } 6} |
|
而如果希望显式指定更新的触发条件,则可以通过 Triggers 属性定义,例如下列代码指定,只有在触发了 btnTrigger 按钮的 Click 事件后,才需要对 UpdatePanel2 区域进行重绘。
1<asp:Button runat="server" ID="btnTrigger" Text="Trigger" 2 OnClick="btnTrigger_Click" /> 3.. 4<atlas:UpdatePanel runat="server" ID="UpdatePanel2" Mode="Conditional"> 5 <Triggers> 6 <atlas:ControlEventTrigger ControlID="btnTrigger" EventName="Click" /> 7 </Triggers> 8.. 9</atlas:UpdatePanel> |
|
触发条件目前支持针对控件事件和内容的两类: ControlEventTrigger 和 ControlValueTrigger。所有触发条件都继承自 UpdatePanelTrigger 抽象类。
1public abstract class UpdatePanelTrigger 2{ 3 internal UpdatePanelTrigger(); 4 protected internal abstract bool HasTriggered(Control ownerControl); 5 protected internal virtual void Initialize(Control ownerControl); 6 internal void SetOwner(UpdatePanelTriggerCollection owner); 7 8 private UpdatePanelTriggerCollection _owner; 9} |
|
UpdatePanel 在调用 Initialize 进行初始化的时候,会调用每个 UpdatePanelTrigger 的 Initialize 方法。具体的实现可在此事件中,接管服务器框架的相应事件,或者保存服务器控件的当前值。值得注意的是,这里的通过 ControlEventTrigger.EventName 指定的是服务器端控件的事件名称,而不是 HTML 控件的事件名称。因此上述例子的按钮点击事件,名称是 Click 而不是 onclick。而在 Altas 进行服务器端局部重绘时,会询问每个 UpdatePanel 是否需要进行重绘。此时被检查的 UpdatePanel.RequiresUpdate 属性,实际上会调用每个 UpdatePanelTrigger 的 HasTriggered 方法,判断是否需要对此 UpdatePanel 进行重绘。伪代码如下:
1public class UpdatePanel : Control 2{ 3 protected internal virtual void Initialize() 4 { 5 if (_triggers != null) 6 { 7 if (ScriptManager.GetCurrent(Page).IsInPartialRenderingMode) 8 { 9 _triggers.Initialize(this); 10 } 11 } 12 } 13} 14public sealed class UpdatePanelTriggerCollection : Collection<UpdatePanelTrigger> 15{ 16 internal void Initialize(Control ownerControl) 17 { 18 foreach(UpdatePanelTrigger trigger in this) 19 { 20 trigger.Initialize(ownerControl); 21 } 22 } 23} |
|
UpdatePanel.RequiresUpdate 判断是否需要重绘的代码与之基本类似。
而在 UpdatePanel.OnInit 事件中,则负责在局部重绘模式时,调用 ScriptManager.RegisterUpdatePanel 方法将自己注册到管理器中。然后注册 Page.InitComplete 事件,在 UpdatePanel.OnPageInitComplete 事件处理函数中,初始化自身。伪代码如下:
1public class UpdatePanel : Control 2{ 3 protected override void OnInit(EventArgs e) 4 { 5 if (!DesignMode) 6 { 7 // 如果没有指定 UpdatePanel 则抛出异常 8 if (string.IsNullOrEmpty(ID)) 9 throw new InvalidOperationException("UpdatePanel controls must have an explicit ID."); 10 11 // 如果没有找到 ScriptManager 也抛出异常 12 ScriptManager manager = ScriptManager.GetCurrent(this.Page); 13 14 if (manager == null) 15 throw new InvalidOperationException("An UpdatePanel requires a ScriptManager on the page"); 16 17 // 如果启用局部重绘模式,则将自己注册到管理器 18 if (manager.IsInPartialRenderingMode) 19 manager.RegisterUpdatePanel(this); 20 21 // 页面初始化完成时对自身进行初始化 22 Page.InitComplete += new EventHandler(OnPageInitComplete); 23 24 // 处理模板控件相关事宜 25 // 26 } 27 } 28 29 private void OnPageInitComplete(object sender, EventArgs e) 30 { 31 // 仅在第一次初始化页面时对 UpdatePanel 进行初始化 32 if (!Page.IsPostBack) 33 if (ScriptManager.GetCurrent(this.Page).EnablePartialRendering) 34 Initialize(); 35 36 _initialized = true; 37 } 38} |
|
最后,如果 UpdatePanel 需要进行完整重绘时,Page 会调用 UpdatePanel 从 Web.UI.Control 重载来的 void Render(HtmlTextWriter writer) 和 void RenderChildren(HtmlTextWriter writer) 方法进行绘制。后者会根据是否启用重绘模式,用一个 <span/> 标签将 ContentTemplate 中的子内容保护起来,用户在最终更新内容时定位。
了解了 UpdatePanel 的使用和实现后,我们回过头来看看 ScriptManager 是如何使用它们的。
在上一节我们曾提到,ScriptManager 在 OnPagePreRenderComplete 事件中,根据当前状态决定是否写入局部重绘模式的初始化脚本。
1private void OnPagePreRenderComplete(object sender, EventArgs e) 2{ 3 // 4 5 if (存在任意一种脚本服务、控件或引用) 6 { 7 // 8 9 if (proxyScript != null || _enablePartialRendering) 10 { 11 writer.Write("<script type=\"text/javascript\">"); 12 13 // 14 15 // 输出局部重绘模式初始化脚本 16 writer.WriteLine("Web.WebForms._PageRequest._setupAsyncPostBacks(document.getElementById('" + _page.Form.ClientID + "'), '" + UniqueID + "');"); 17 18 writer.Write("</script>"); 19 } 20 21 // 22 } 23} |
|
_setupAsyncPostBacks(...) 函数调用会在客户端载入页面时,完成 Altas 局部重绘引擎的初始化设置工作。
1Web.WebForms._PageRequestManager = function() 2{ 3 this._setupAsyncPostBacks = function(form, scriptManagerID, updatePanelIDs, asyncPostbackControlIDs) 4 { 5 // 在 _PageRequest 对象中保存参数 6 _form = form; 7 _scriptManagerID = scriptManagerID; 8 _updatePanelIDs = updatePanelIDs; 9 _asyncPostbackControlIDs = asyncPostbackControlIDs; 10 11 form._initialAction = form.action; 12 13 _onsubmit = form.onsubmit; 14 15 // 接管顶级 ASP.NET 的 form 之 onsubmit/onclick 方法 16 form.onsubmit = null; 17 form.attachEvent('onsubmit', Function.createDelegate(this, this._onFormSubmit)); 18 form.attachEvent('onclick', Function.createDelegate(this, this._onFormElementClick)); 19 20 // 接管 ASP.NET 处理 Post Back 请求的函数 21 _originalDoPostBack = window.__doPostBack; 22 if (_originalDoPostBack) { 23 window.__doPostBack = Function.createDelegate(this, this._doPostBack); 24 } 25 } 26} |
|
一般说来,ASP.NET 会在定义的 <form id="form1" runat="server"> 附近,增加一些处理 Post Back 的客户端代码,例如:
1<body> 2 <form name="aspnetForm" method="post" action="MyLists.aspx" id="aspnetForm"> 3<div> 4<input type="hidden" name="__EVENTTARGET" id="__EVENTTARGET" value="" /> 5<input type="hidden" name="__EVENTARGUMENT" id="__EVENTARGUMENT" value="" /> 6<input type="hidden" name="__LASTFOCUS" id="__LASTFOCUS" value="" /> 7<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="TCJEqyt9uS7OFuMId6rlbgLv+36H71Efw5hFAgjKyJ42XauLO8blWV/ofWtkx9Pg+SZ76WA7NvDr+/KDLacJvcKBot564ONmv4RYIXk+6GzGtINC2f4d7VDQPQXyRXwxIavJZsBZGQUwabITF0mTGs9Cus01SoG/cg2ACWQa/uofvZfU1ocGCnmKuu1SLVs6u9Y/UMOMC6lNVJgWOv3CILth90llrrIPN7nCVJC4Xyq3+nSZhzoN0/Oo4Xz4JMjUBFsy7KyDPXEaDHQGQXyRuA==" /> 8</div> 9 10<script type="text/javascript"> 11<!-- 12var theForm = document.forms['aspnetForm']; 13if (!theForm) { 14 theForm = document.aspnetForm; 15} 16function __doPostBack(eventTarget, eventArgument) { 17 if (!theForm.onsubmit || (theForm.onsubmit() != false)) { 18 theForm.__EVENTTARGET.value = eventTarget; 19 theForm.__EVENTARGUMENT.value = eventArgument; 20 theForm.submit(); 21 } 22} 23// --> 24</script> |
|
Altas 则通过接管上述几个客户端事件,在 ASP.NET 客户端脚本和服务端实现之间,增加了一个透明的代理层。
其中 _onFormSubmit 事件负责完成实际的局部重绘参数构建;_onFormElementClick 事件负责保存表单点击的额外信息,并传递回服务端;_doPostBack 事件则将自动的同步页面 Post Back 操作,重定向到异步的 _onFormSubmit 操作。
首先, _onFormSubmit 事件会针对各种状态进行判断
1Function.createDelegate = function(instance, method) { 2 return function() { 3 method.apply(instance, arguments); 4 } 5} 6 7Web.WebForms._PageRequestManager = function() 8{ 9 this.get_inPostBack = function() { 10 return _request != null; 11 } 12 13 this._onFormSubmit = function() 14 { 15 // 如果已经是 Post Back 模式,则直接返回不重复提交 16 if (this.get_inPostBack()) { 17 if (window.event) { 18 window.event.returnValue = false; 19 } 20 return false; 21 } 22 23 // 如果 form 有 onsubmit 事件处理函数,则对其进行包装 24 var continueSubmit = true; 25 26 if (_onsubmit) { 27 continueSubmit = Function.createDelegate(this, _onsubmit); 28 } 29 30 if (!continueSubmit) { 31 if (window.event) { 32 window.event.returnValue = false; 33 } 34 return false; 35 } 36 37 // 如果表单的 action 和初始化时不同,则跳过数据处理 38 var form = _form; 39 if (form.action != form._initialAction) { 40 return true; 41 } 42 43 // 如果不启用异步的 post back 模式,则跳过数据处理 44 if (!_postbackSettings.async) { 45 return true; 46 } 47 48 // 数据处理 49 // 50 } 51} |
|
然后,_onFormSubmit 事件会根据表单中每一个包含数据的元素,构建一个完整的请求参数表。
1Web.WebForms._PageRequestManager = function() 2{ 3 this._onFormSubmit = function() 4 { 5 // 针对各种状态进行判断 6 // 7 8 // 建立 StringBuilder 用于构建请求内容 9 var formBody = new Web.StringBuilder(); 10 formBody.append(_scriptManagerID + '=' + _postbackSettings.panelID + '&'); 11 12 for (遍历表单中每个元素) 13 { 14 // 处理 INPUT、SELECT 和 TEXTAREA 三类标记 15 // 将其 id 和 value,拼接成 id=value&id=value&id=value 类型的数据 16 } 17 18 // 如果有额外的输入信息,也添加到请求内容中,用户记录事件等信息 19 if (_additionalInput) { 20 formBody.append(_additionalInput); 21 _additionalInput = null; 22 } 23 24 // 通过 XMLHTTP 异步发送请求 25 // 26 } 27} |
|
最后,_onFormSubmit 将构造得到的请求内容,通过 XMLHTTP 的方式发送到服务端。
1Web.WebForms._PageRequestManager = function() 2{ 3 this._onFormSubmit = function() 4 { 5 // 针对各种状态进行判断 6 // 7 8 // 根据表单中每一个包含数据的元素,构建一个完整的请求参数表 9 // 10 11 // 构建 XMLHTTP 封装类 WebRequest 实例 12 var request = new Web.Net.WebRequest(); 13 14 // 填充基本请求信息,delta=true 表示启用局部重绘模式,并且关闭缓存 15 request.set_url(form.action); 16 request.get_headers()['delta'] = 'true'; 17 request.get_headers()['Cache-Control'] = 'no-cache'; 18 request.set_timeoutInterval(90000); 19 20 // 接管请求完成或超时的事件 21 request.completed.add(Function.createDelegate(this, this._onFormSubmitCompleted)); 22 request.timeout.add(Function.createDelegate(this, this._onFormSubmitTimeout)); 23 24 // 提交构造得到的请求参数表 25 request.set_body(formBody.toString()); 26 27 // 进入 post back 请求状态 28 _request = request; 29 this.raisePropertyChanged('inPostBack'); 30 request.invoke(); 31 32 if (window.event) { 33 window.event.returnValue = false; 34 } 35 return false; 36 } 37} |
|
当后台异步请求完成时,Altas 脚本会对返回结果进行解析并更新页面。这部分的讨论等完成服务端的结果讨论后再详细展开。
对异步请求超时的情况,仅仅是终止 post back 状态,因为信息不足以判断如何进行处理。个人觉得这种处理思路过于草率了,至少应该提供一些信息,让使用者能通过 inPostBack 属性变化事件,了解到请求到底是成功还是超时。
1Web.WebForms._PageRequestManager = function() 2{ 3 this._onFormSubmitTimeout = function(sender, eventArgs) { 4 _request = null; 5 this.raisePropertyChanged('inPostBack'); 6 } 7} |
|
_doPostBack 事件基本上就是 _onFormSubmit 的封装。它会根据 post back 请求事件的来源,判断是否需要启用异步 post back 模式。
1Web.WebForms._PageRequestManager = function() 2{ 3 this._doPostBack = function(eventTarget, eventArgument) 4 { 5 _additionalInput = null; 6 7 if (this.get_inPostBack()) { 8 if (window.event) { 9 window.event.returnValue = false; 10 } 11 return; 12 } 13 14 // 根据事件来源,判断是否需要启用异步模式 15 _postbackSettings = null; 16 17 var postbackElement = findNearestElement(eventTarget); 18 19 if (postbackElement) 20 _postbackSettings = getPostbackSettings(postbackElement); 21 else 22 _postbackSettings = createPostbackSettings(true, _scriptManagerID); 23 24 // 对同步模式,直接调用 ASP.NET 的 doPostBack 实现 25 if (!_postbackSettings.async) { 26 _originalDoPostBack(eventTarget, eventArgument); 27 return; 28 } 29 30 // 对异步模式,填充事件信息,并调用 _onFormSubmit 完成数据准备和提交操作 31 var form = _form; 32 form.__EVENTTARGET.value = eventTarget; 33 form.__EVENTARGUMENT.value = eventArgument; 34 this._onFormSubmit(); 35 36 if (window.event) { 37 window.event.returnValue = false; 38 } 39 } 40} |
|
至此,我们基本上完成了对局部重绘模式下,从 UpdatePanel 到客户端数据提交的分析。下一节将继续针对局部重绘模式,分析服务端对此模式下刷新并返回数据实现,以及客户端如何对返回数据进行解析,并更新到最终页面的控件。