使用ASP.NET AJAX框架我们可以搭建快速响应、具有丰富的用户体验的AJAX Web应用程序,而该框架的UpdatePanel控件则提供了一种非常简单的方式来实现Web页面的局部更新,我们不需要在每次回发的时候都加载整个页面。
那这个控件是如何实现这种局部刷新的哪,透过其实现机制我们可以更清楚其优缺点,便于我们确定其使用场合。本文将重点阐述ASP.NET AJAX控件UpdatePanel的实现机制。
1. ASP.NET AJAX 简介
ASP.NET AJAX是微软在ASP.NET 2.0之上对AJAX技术的一个封装,为Web应用程序提供完整的AJAX解决方案。ASP.NET AJAX有两种编程模型:部分更新和远程服务。
部分更新使得用户可以用传统的ASP.NET 2.0应用程序的方式来搭建AJAX应用,具体就是使用UpdatePanel控件来实现无闪烁页面更新。而远程服务则是直接通过前端JavaScript来调用的服务器端服务,前段获取数据后,进行页面更新,这就要求服务器端代码必须分解为特定于应用程序的服务,这是与传统的ASP.NET应用程序完全不同的体系结构。
部分更新着重于对现有应用程序进行渐进式增强,帮助用户逐渐转换到纯粹的AJAX应用。本文主要对部分更新编程模型中核心控件UpdatePanel的实现进行剖析,讲述其背后的故事。
ASP.NET AJAX框架分为客户端以及服务器端两个部分,基于客户端的 Microsoft AJAX Library包含了对浏览器兼容性、网络访问以及客户端控件组件等支持, 而服务器端则包括了服务器控件,Web 服务等等。
见下图:

Microsoft Ajax Library就是ASP.NET AJAX的客户端脚本库,其中MicrosoftAjax.js包含了ASP.NET AJAX的核心内容,包括跨浏览器的支持、基于面向对象对JavaScript的扩展以及网络访问等等。MicrosoftAjaxWebForm.js文件则是完全服务于ASP.NET AJAX页面局部更新这样一个功能的,在该文件中定义了一个客户端对象PageRequestManager,该对象将会负责客户端异步回送的全过程。
2. ScriptManager 和 UpdatePanel
ScriptManager和UpdatePanel是ASP.NET AJAX服务器端中最重要的两个控件,ScriptManager控件用来管理ASP.NET页面中的客户端脚本,生成及注册所需要的客户端脚本,通过UpdatePanel控件可以更新页面的指定部分而无需加载整个页面。
看个例子:
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
</asp:ScriptManager>
<div>
<asp:UpdatePanel ID="UpdatePanel1" runat="server">
<ContentTemplate>
<%= DateTime.Now %>
<br />
<asp:Button ID="Button1" runat="server" Text="Button" />
</ContentTemplate>
</asp:UpdatePanel>
</div>
</form>构建如上代码所示的页面,在Runtime点击UpdatePanel中的Button控件,则不会引起整个页面刷新,只是用来显示当前时间的Label得到更新。
这是如何实现的哪?
3. ASP.NET AJAX部分呈现剖析
3.1 先从客户端讲起
看一下上面的示例代码在客户端的HTML代码, 这里只列出核心部分,其他全部隐去。

<script type="text/javascript">
//<![CDATA[
Sys.WebForms.PageRequestManager._initialize('ScriptManager1', document.getElementById('form1'));
Sys.WebForms.PageRequestManager.getInstance()._updateControls(['tUpdatePanel1'], [], [], 90);
//]]>
</script>
<div id="UpdatePanel1">
7/25/2008 4:54:36 PM
<br />
<input type="submit" name="Button1" value="Button" id="Button1" />
</div>
看一下上面的两句JavaScript代码,第一句代码中的_initialize 方法是客户端PageRequestManager对象上的静态方法,它会创建一个 PageRequestManager 类的全局实例,并将其初始化。在这个初始化函数中,ageRequestManager对象注册了当前表单对象的submit事件,以及window对象的load和unload事件。
而第二句代码则是通过PageRequestManager的getInstance方法来检索其唯一实例, 得到该实例后调用_updateControls方法来注册UpdatePanel以及其Trigger控件。
我们可以从MicrosoftAjaxWebForm.js文件中得到_updateControls方法的声明:
function Sys$WebForms$PageRequestManager$_updateControls(updatePanelIDs, asyncPostBackControlIDs, postBackControlIDs, asyncPostBackTimeout) {}
其中第一个参数代表了当前页面上所有的UpdatePanel控件的ID集合,如果该UpdatePanel的ChildrenAsTrigger为True的话,应在ID前添加字符't',否则添加字符'f';而第二个参数是所有引发异步回送的控件ID;第三个参数是所有引发同步回送的控件ID;第四个参数设定了异步回送的Timeout时间,单位为秒。
由于PageRequestManager对象注册了当前表单的submit时间,所以每当页面有提交动作的时候,PageRequestManager对象就会介入,看一下PageRequestManager对象页面提交处理函数_onFormSubmit(evt)。
如果需要执行一次异步回送的话,会中止原有的普通浏览器会回发,代之使用XMLHttpRequest进行AJAX回发。在封装这个请求的时候,当前页面的所有字段以及视图状态都会被打包在请求中,另外还设置了这次Request的HTTP头:request.get_headers()['X-MicrosoftAjax'] = 'Delta=true';
在服务器端将会根据这个HTTP头标记来判定是否为一次AJAX异步回发。
_onFormSubmit(evt)函数代码:

function Sys$WebForms$PageRequestManager$_onFormSubmit(evt)
{
var continueSubmit = true;
var isCrossPost = this._isCrossPost;
this._isCrossPost = false;
if (this._onsubmit)
{
continueSubmit = this._onsubmit();
}
if (continueSubmit)
{
for (var i = 0; i < this._onSubmitStatements.length; i++)
{
if (!this._onSubmitStatements[i]())
{
continueSubmit = false;
break;
}
}
}
if (!continueSubmit)
{
if (evt)
{
evt.preventDefault();
}
return;
}
var form = this._form;
if (isCrossPost)
{
return;
}
if (this._activeDefaultButton && !this._activeDefaultButtonClicked)
{
this._onFormElementActive(this._activeDefaultButton, 0, 0);
}
if (!this._postBackSettings.async)
{
return;
}
var formBody = new Sys.StringBuilder();
formBody.append(encodeURIComponent(this._scriptManagerID) + '=' + encodeURIComponent(this._postBackSettings.panelID) + '&');
var count = form.elements.length;
for (var i = 0; i < count; i++)
{
var element = form.elements[i];
var name = element.name;
if (typeof(name) === "undefined" || (name === null) || (name.length === 0))
{
continue;
}
var tagName = element.tagName;
if (tagName === 'INPUT')
{
var type = element.type;
if ((type === 'text') ||
(type === 'password') ||
(type === 'hidden') ||
(((type === 'checkbox') || (type === 'radio')) && element.checked))
{
formBody.append(encodeURIComponent(name));
formBody.append('=');
formBody.append(encodeURIComponent(element.value));
formBody.append('&');
}
}
else if (tagName === 'SELECT')
{
var optionCount = element.options.length;
for (var j = 0; j < optionCount; j++)
{
var option = element.options[j];
if (option.selected)
{
formBody.append(encodeURIComponent(name));
formBody.append('=');
formBody.append(encodeURIComponent(option.value));
formBody.append('&');
}
}
}
else if (tagName === 'TEXTAREA')
{
formBody.append(encodeURIComponent(name));
formBody.append('=');
formBody.append(encodeURIComponent(element.value));
formBody.append('&');
}
}
if (this._additionalInput)
{
formBody.append(this._additionalInput);
this._additionalInput = null;
}
var request = new Sys.Net.WebRequest();
var action = form.action;
if (Sys.Browser.agent === Sys.Browser.InternetExplorer)
{
var queryIndex = action.indexOf('?');
if (queryIndex !== -1)
{
var path = action.substr(0, queryIndex);
if (path.indexOf("%") === -1)
{
action = encodeURI(path) + action.substr(queryIndex);
}
}
else if (action.indexOf("%") === -1)
{
action = encodeURI(action);
}
}
request.set_url(action);
request.get_headers()['X-MicrosoftAjax'] = 'Delta=true';
request.get_headers()['Cache-Control'] = 'no-cache';
request.set_timeout(this._asyncPostBackTimeout);
request.add_completed(Function.createDelegate(this, this._onFormSubmitCompleted));
request.set_body(formBody.toString());
var handler = this._get_eventHandlerList().getHandler("initializeRequest");
if (handler)
{
var eventArgs = new Sys.WebForms.InitializeRequestEventArgs(request, this._postBackSettings.sourceElement);
handler(this, eventArgs);
continueSubmit = !eventArgs.get_cancel();
}
if (!continueSubmit)
{
if (evt)
{
evt.preventDefault();
}
return;
}
this._scrollPosition = this._getScrollPosition();
this.abortPostBack();
handler = this._get_eventHandlerList().getHandler("beginRequest");
if (handler)
{
var eventArgs = new Sys.WebForms.BeginRequestEventArgs(request, this._postBackSettings.sourceElement);
handler(this, eventArgs);
}

if (this._originalDoCallback)
{
this._cancelPendingCallbacks();
}
this._request = request;
request.invoke();
if (evt)
{
evt.preventDefault();
}
}我们可以发现AJAX回发所提交的数据量其实跟普通回发过程中提交的数据量是一样的,并且还附加了一些额外信息。
3.2 服务器端的处理
AJAX回发请求到达服务器之后,当前页面的生命周期跟普通回发引起的请求是一样的,页面的Init、Load和Render等等事件都会被触发,差别只是在于AJAX回发使用了不同的呈现画法。
AJAX回发引起的请求生命周期:

从上图我们可以看到,页面的生命周期与普通回发是一样的,同样页面上的控件也会经历相应的生命周期。
先了解一下ScriptManager控件在服务器端的处理:
- OnInit:在Init事件中,ScriptManager控件会注册页面的InitComplete, PreRenderComplete以及PreRender事件,另外还会根据本次请求的HTTP头来设定一个标记以确定本次回发是否为Ajax异步更新所引起的回发。
见下面的代码:
protected internal override void OnInit(EventArgs e)

{
base.OnInit(e);
if (this.EnableHistory)
{
this.RegisterAsyncPostBackControl(this);
}
if (!base.DesignMode)
{
IPage iPage = this.IPage;
if (GetCurrent(this.Page) != null)
{
throw new InvalidOperationException(AtlasWeb.ScriptManager_OnlyOneScriptManager);
}
iPage.Items[typeof(IScriptManager)] = this;
iPage.Items[typeof(ScriptManager)] = this;
iPage.InitComplete += new EventHandler(this.OnPageInitComplete);
iPage.PreRenderComplete += new EventHandler(this.OnPagePreRenderComplete);
if (iPage.IsPostBack)
{
this._isInAsyncPostBack = PageRequestManager.IsAsyncPostBackRequest(iPage.Request.Headers);
if (this.EnableHistory)
{
this._isNavigating = iPage.Request["__EVENTTARGET"] == this.UniqueID;
}
}
this.PageRequestManager.OnInit();
iPage.PreRender += new EventHandler(this.ScriptControlManager.OnPagePreRender);
}
} - OnPagePreRenderComplete,在PagePreRenderComplete事件中,ScriptManager控件会注册脚本文件以及Services代理脚本,MicrosoftAjax.js和MicrosoftAjaxWebForm.js就是在这个阶段被注册到客户端的。
见下面的代码:
private void OnPagePreRenderComplete(object sender, EventArgs e)

{
if (!this.IsInAsyncPostBack)
{
if (this.SupportsPartialRendering)
{
this.IPage.ClientScript.GetPostBackEventReference(new PostBackOptions(this, null, null, false, false, false, false, true, null));
}
this.RegisterGlobalizationScriptBlock();
this.RegisterScripts();
this.RegisterServices();
if (this.EnableHistory)
{
JavaScriptSerializer serializer = JavaScriptSerializer.CreateInstance();
string[] strArray = new string[]
{ "\r\nSys.Application.setServerId(", serializer.Serialize(this.ClientID), ", ", serializer.Serialize(this.UniqueID), ");\r\n", ((this._initialState != null) && (this._initialState.Count != 0)) ? (" Sys.Application.setServerState('" + this.G
RSS订阅





