ASP.NET AJAX UpdatePanel 控件實現剖析
使用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.GetStateString() + "');\r\n") : "\r\n" };
string script = string.Concat(strArray);
RegisterStartupScript(this, typeof(ScriptManager), "HistoryStartup", script, true);
}
}
else
{
this.RegisterScripts();
if (this.EnableHistory)
{
if ((this._initialState != null) && (this._initialState.Count == 0))
{
this._initialState = null;
}
if (this._newPointCreated)
{
this.RegisterDataItem(this, "'" + this.GetStateString() + "'", true);
}
}
}
}
- OnPreRender,在PreRender事件中如果判定本次回發為AJAX回發,則會調用PageRequestManager對象的OnPreRender方法。而PageRequestManager對象則會調用Page對象的SetRenderMethodDelegate方法來代理Page的畫法,PageRequestManager對象會真正負責本次AJAX回發最終的HTML代碼。
見下面的代碼:
public class ScriptManager : Control,
{
protected internal override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (this.IsInAsyncPostBack)
{
this.PageRequestManager.OnPreRender();
}
}
}
internal sealed class PageRequestManager
{
internal void OnPreRender()
{
this._owner.IPage.SetRenderMethodDelegate(new RenderMethod(this.RenderPageCallback));
}
}
PageRequestManager的RenderPageCallback方法最終處理了AJAX回發所需要的HTML代碼,在這個方法中會遍歷頁面上所有涉及到的UpdatePanel控件,得到其更新后的HTML代碼后,與隱藏字段還有一些額外信息一起打包,然后傳遞給客戶端。
見下面的代碼:
private void RenderPageCallback(HtmlTextWriter writer, Control pageControl)
{
this.ProcessUpdatePanels();
IHttpResponse response = this._owner.IPage.Response;
response.ContentType = "text/plain";
response.Cache.SetNoServerCaching();
IHtmlForm form = this._owner.IPage.Form;
form.SetRenderMethodDelegate(new RenderMethod(this.RenderFormCallback));
this._updatePanelWriter = writer;
ParserStringWriter writer2 = new ParserStringWriter();
ParserHtmlTextWriter writer3 = new ParserHtmlTextWriter(writer2);
writer2.ParseWrites = true;
form.RenderControl(writer3);
writer2.ParseWrites = false;
foreach (KeyValuePair<string, string> pair in writer2.HiddenFields)
{
if (ControlUtil.IsBuiltInHiddenField(pair.Key))
{
EncodeString(writer, "hiddenField", pair.Key, pair.Value);
}
}
EncodeString(writer, "asyncPostBackControlIDs", string.Empty, this.GetAsyncPostBackControlIDs(false));
EncodeString(writer, "postBackControlIDs", string.Empty, this.GetPostBackControlIDs(false));
EncodeString(writer, "updatePanelIDs", string.Empty, this.GetAllUpdatePanelIDs());
EncodeString(writer, "childUpdatePanelIDs", string.Empty, this.GetChildUpdatePanelIDs());
EncodeString(writer, "panelsToRefreshIDs", string.Empty, this.GetRefreshingUpdatePanelIDs());
EncodeString(writer, "asyncPostBackTimeout", string.Empty, this._owner.AsyncPostBackTimeout.ToString(CultureInfo.InvariantCulture));
if (writer3.FormAction != null)
{
EncodeString(writer, "formAction", string.Empty, writer3.FormAction);
}
if (this._owner.IPage.Header != null)
{
string title = this._owner.IPage.Title;
if (!string.IsNullOrEmpty(title))
{
EncodeString(writer, "pageTitle", string.Empty, title);
}
}
this.RenderDataItems(writer);
this.ProcessScriptRegistration(writer);
this.ProcessFocus(writer);
}
3.3 客戶端更新
當服務器端相應完畢后,客戶端會得到響應信息,然后調用客戶端對象PageRequestManager的_onFormSubmitCompleted方法來進行頁面局部更新,最終會調用_updatePanel方法來更新UpdatePanel控件。
參見_onFormSubmitCompleted方法的代碼:
function Sys$WebForms$PageRequestManager$_onFormSubmitCompleted(sender, eventArgs) {
this._processingRequest = true;
var delimitByLengthDelimiter = '|';
if (sender.get_timedOut()) {
this._endPostBack(this._createPageRequestManagerTimeoutError(), sender);
return;
}
if (sender.get_aborted()) {
this._endPostBack(null, sender);
return;
}
if (!this._request || sender.get_webRequest() !== this._request) {
return;
}
var errorMessage;
var delta = [];
if (sender.get_statusCode() !== 200) {
this._endPostBack(this._createPageRequestManagerServerError(sender.get_statusCode()), sender);
return;
}
var reply = sender.get_responseData();
var delimiterIndex, len, type, id, content;
var replyIndex = 0;
var parserErrorDetails = null;
while (replyIndex < reply.length) {
delimiterIndex = reply.indexOf(delimitByLengthDelimiter, replyIndex);
if (delimiterIndex === -1) {
parserErrorDetails = this._findText(reply, replyIndex);
break;
}
len = parseInt(reply.substring(replyIndex, delimiterIndex), 10);
if ((len % 1) !== 0) {
parserErrorDetails = this._findText(reply, replyIndex);
break;
}
replyIndex = delimiterIndex + 1;
delimiterIndex = reply.indexOf(delimitByLengthDelimiter, replyIndex);
if (delimiterIndex === -1) {
parserErrorDetails = this._findText(reply, replyIndex);
break;
}
type = reply.substring(replyIndex, delimiterIndex);
replyIndex = delimiterIndex + 1;
delimiterIndex = reply.indexOf(delimitByLengthDelimiter, replyIndex);
if (delimiterIndex === -1) {
parserErrorDetails = this._findText(reply, replyIndex);
break;
}
id = reply.substring(replyIndex, delimiterIndex);
replyIndex = delimiterIndex + 1;
if ((replyIndex + len) >= reply.length) {
parserErrorDetails = this._findText(reply, reply.length);
break;
}
content = reply.substr(replyIndex, len);
replyIndex += len;
if (reply.charAt(replyIndex) !== delimitByLengthDelimiter) {
parserErrorDetails = this._findText(reply, replyIndex);
break;
}
replyIndex++;
Array.add(delta, {type: type, id: id, content: content});
}
if (parserErrorDetails) {
this._endPostBack(this._createPageRequestManagerParserError(String.format(Sys.WebForms.Res.PRM_ParserErrorDetails, parserErrorDetails)), sender);
return;
}
var updatePanelNodes = [];
var hiddenFieldNodes = [];
var arrayDeclarationNodes = [];
var scriptBlockNodes = [];
var scriptStartupNodes = [];
var expandoNodes = [];
var onSubmitNodes = [];
var dataItemNodes = [];
var dataItemJsonNodes = [];
var scriptDisposeNodes = [];
var asyncPostBackControlIDsNode, postBackControlIDsNode,
updatePanelIDsNode, asyncPostBackTimeoutNode,
childUpdatePanelIDsNode, panelsToRefreshNode, formActionNode;
for (var i = 0; i < delta.length; i++) {
var deltaNode = delta[i];
switch (deltaNode.type) {
case "updatePanel":
Array.add(updatePanelNodes, deltaNode);
break;
case "hiddenField":
Array.add(hiddenFieldNodes, deltaNode);
break;
case "arrayDeclaration":
Array.add(arrayDeclarationNodes, deltaNode);
break;
case "scriptBlock":
Array.add(scriptBlockNodes, deltaNode);
break;
case "scriptStartupBlock":
Array.add(scriptStartupNodes, deltaNode);
break;
case "expando":
Array.add(expandoNodes, deltaNode);
break;
case "onSubmit":
Array.add(onSubmitNodes, deltaNode);
break;
case "asyncPostBackControlIDs":
asyncPostBackControlIDsNode = deltaNode;
break;
case "postBackControlIDs":
postBackControlIDsNode = deltaNode;
break;
case "updatePanelIDs":
updatePanelIDsNode = deltaNode;
break;
case "asyncPostBackTimeout":
asyncPostBackTimeoutNode = deltaNode;
break;
case "childUpdatePanelIDs":
childUpdatePanelIDsNode = deltaNode;
break;
case "panelsToRefreshIDs":
panelsToRefreshNode = deltaNode;
break;
case "formAction":
formActionNode = deltaNode;
break;
case "dataItem":
Array.add(dataItemNodes, deltaNode);
break;
case "dataItemJson":
Array.add(dataItemJsonNodes, deltaNode);
break;
case "scriptDispose":
Array.add(scriptDisposeNodes, deltaNode);
break;
case "pageRedirect":
if (Sys.Browser.agent === Sys.Browser.InternetExplorer) {
var anchor = document.createElement("a");
anchor.style.display = 'none';
anchor.attachEvent("onclick", cancelBubble);
anchor.href = deltaNode.content;
document.body.appendChild(anchor);
anchor.click();
anchor.detachEvent("onclick", cancelBubble);
document.body.removeChild(anchor);
function cancelBubble(e) {
e.cancelBubble = true;
}
}
else {
window.location.href = deltaNode.content;
}
return;
case "error":
this._endPostBack(this._createPageRequestManagerServerError(Number.parseInvariant(deltaNode.id), deltaNode.content), sender);
return;
case "pageTitle":
document.title = deltaNode.content;
break;
case "focus":
this._controlIDToFocus = deltaNode.content;
break;
default:
this._endPostBack(this._createPageRequestManagerParserError(String.format(Sys.WebForms.Res.PRM_UnknownToken, deltaNode.type)), sender);
return;
}
}
var i;
if (asyncPostBackControlIDsNode && postBackControlIDsNode &&
updatePanelIDsNode && panelsToRefreshNode &&
asyncPostBackTimeoutNode && childUpdatePanelIDsNode) {
this._oldUpdatePanelIDs = this._updatePanelIDs;
var childUpdatePanelIDsString = childUpdatePanelIDsNode.content;
this._childUpdatePanelIDs = childUpdatePanelIDsString.length ? childUpdatePanelIDsString.split(',') : [];
var asyncPostBackControlIDsArray = this._splitNodeIntoArray(asyncPostBackControlIDsNode);
var postBackControlIDsArray = this._splitNodeIntoArray(postBackControlIDsNode);
var updatePanelIDsArray = this._splitNodeIntoArray(updatePanelIDsNode);
this._panelsToRefreshIDs = this._splitNodeIntoArray(panelsToRefreshNode);
for (i = 0; i < this._panelsToRefreshIDs.length; i++) {
var panelClientID = this._uniqueIDToClientID(this._panelsToRefreshIDs[i]);
if (!document.getElementById(panelClientID)) {
this._endPostBack(Error.invalidOperation(String.format(Sys.WebForms.Res.PRM_MissingPanel, panelClientID)), sender);
return;
}
}
var asyncPostBackTimeout = asyncPostBackTimeoutNode.content;
this._updateControls(updatePanelIDsArray, asyncPostBackControlIDsArray, postBackControlIDsArray, asyncPostBackTimeout);
}
this._dataItems = {};
for (i = 0; i < dataItemNodes.length; i++) {
var dataItemNode = dataItemNodes[i];
this._dataItems[dataItemNode.id] = dataItemNode.content;
}
for (i = 0; i < dataItemJsonNodes.length; i++) {
var dataItemJsonNode = dataItemJsonNodes[i];
this._dataItems[dataItemJsonNode.id] = Sys.Serialization.JavaScriptSerializer.deserialize(dataItemJsonNode.content);
}
var handler = this._get_eventHandlerList().getHandler("pageLoading");
if (handler) {
handler(this, this._getPageLoadingEventArgs());
}
if (formActionNode) {
this._form.action = formActionNode.content;
}
Sys._ScriptLoader.readLoadedScripts();
Sys.Application.beginCreateComponents();
var scriptLoader = Sys._ScriptLoader.getInstance();
this._queueScripts(scriptLoader, scriptBlockNodes, true, false);
this._updateContext = {
response: sender,
updatePanelNodes: updatePanelNodes,
scriptBlockNodes: scriptBlockNodes,
scriptDisposeNodes: scriptDisposeNodes,
hiddenFieldNodes: hiddenFieldNodes,
arrayDeclarationNodes: arrayDeclarationNodes,
expandoNodes: expandoNodes,
scriptStartupNodes: scriptStartupNodes,
onSubmitNodes: onSubmitNodes
};
scriptLoader.loadScripts(0,
Function.createDelegate(this, this._scriptIncludesLoadComplete),
Function.createDelegate(this, this._scriptIncludesLoadFailed),
null);
}
4.結語
使用UpdatePanel是給已經存在的ASP.NET應用程序添加AJAX體驗的最快捷方式,對于應用程序的架構也不會有影響,我們可以使用它來逐步的提高應用程序的用戶體驗。但是其性能與純粹的AJAX方式相比較,還是比較差的。
浙公網安備 33010602011771號