技術最前線
在 AJAX 中管理使用者體驗
Dino Esposito
下載程式碼位址:
CuttingEdge2007_11.exe
(183 KB)
Browse the Code Online

目錄
我們都習慣了 Web 的逐步處理特性。每當您按一下按鈕或連結來提出要求,瀏覽器就會提出要求並等待回應。您繼續看一會兒網頁,然後又會再次重複按一下並等候的模式。
正如我在最近專欄中提到的,我對於 AJAX 架構和相關的技術組合所提供的可能性非常看好。開發人員與架構設計師可以建置新一代網站,而且其中的逐步處理模式將由互動性更佳的結構取代。每次看到使用者第一次體驗 AJAX 應用程式不間斷的服務而雀躍不已時,我總是覺得很欣喜。
在最近的專欄中,我不斷讚揚 AJAX 的威力,並列出 AJAX 許多直接和間接的優點。本月我會將焦點放在使用 ASP.NET AJAX Extensions 實作 AJAX 架構時,可能產生的相關問題。
ASP.NET AJAX Extensions 一開始是建置在 ASP.NET 2.0 之上,現在已完全整合到 ASP.NET 3.5 版本的 ASP.NET 平台中。事實上,ASP.NET AJAX 支援兩種程式撰寫模型可供您選擇 -- 部分呈現與指令碼服務。有關這兩者的深入討論,建議您參考我在 2007 年 9 月和 2007 年 10 月 MSDN® Magazine 中的兩篇專欄。
AJAX 應用程式可以略過瀏覽器,直接從指令碼提出 Web 要求。當您使用瀏覽器來要求頁面時 (例如,經由標準 [送出] 按鈕或超連結),每次只會處理各個作用中瀏覽器視窗的單一要求。透過指令碼要求頁面沒有此限制,而且可以同時提出多個要求。在 2007 年 7 月份的《技術最前線》中,我利用這項特性來實作即時進度列,其中可向用戶端報告伺服器端執行中工作的真實進度。從單一瀏覽器視窗同時提出多個呼叫的能力,為解決各種問題 (包括許多與 UI 相關的問題) 的可能性開啟了全新紀元。
在本月的專欄中,我將討論在 ASP.NET AJAX 的基礎上實作 Web 應用程式時,如何實作有效的使用者介面,包括相關的問題、解決方案及必要工具。我會特別著重在使用部分呈現模型的應用程式上。
AJAX 和使用者介面
當使用者提出 AJAX 要求後,網頁仍會保持作用中。也就是說,使用者可以按一下作用中項目,並啟動可能干擾目前要求的新作業。因此,在設計桌面應用程式時,開發人員有時必須考慮暫時停用部分 UI。
您如何在伺服器作業期間停用用戶端頁面項目?有數種方法可達到此目的。您可以擷取相對應的文件物件模型 (DOM) 項目的識別碼,然後啟用或停用其狀態 -- 這是一項用戶端作業,您可以使用 DOM 或 CSS 樣式來透過指令碼完成這項作業。此外,ASP.NET 控制項具備 Visible 屬性。當 Visible 屬性設為 False 時,便會阻止伺服器控制項新增自己的標記到用戶端回應中。如此一來,該控制項的預期標記便不會顯示在用戶端頁面中。
若要在 ASP.NET AJAX 中停用和啟用部分的使用者介面,首先需要識別構成該部分之所有 HTML 項目的識別碼,如下列程式碼所示:
// Get the reference to the DOM element
var button1 = $get("Button1");
// Disable the status of the element
button1.disabled = true;
若要停用項目,您需要擷取識別碼,並將停用的屬性設為適當值。在理想的情況下,您應該在提出要求之前立即關閉使用者介面中的特定部分,然後在完成要求之後立即重新啟用該部分。至於實作方法,則需要視您選用的程式撰寫模型而定。
若您是使用部分呈現,請使用 PageRequestManager 物件的事件模型 -- 這個類別是部分呈現背後的用戶端樞紐。完整的類別名稱是 Sys.WebForms.PageRequestManager,其原始程式碼定義在 MicrosoftAjaxWebForms.js 中。PageRequestManager 有包含設定及控制 AJAX 回傳的所有必要邏輯。這個類別是單一類別,這意味著它能確保一次只有一個擱置中的要求。透過下列程式碼,您就可以取得頁面要求管理員唯一執行個體的參考:
var manager = Sys.WebForms.PageRequestManager.getInstance();
PageRequestManager 會公開幾個用戶端事件,可讓您攔截處理 AJAX 回傳時的關鍵步驟,如 [圖 1] 所示。發生 BeginRequest 事件時通常是停用 UI 項目不錯的時機,而 EndRequest 事件也是重新啟用 HTML 項目的正確時間點。

Figure 1 PageRequestManager 公開的事件
| 事件 |
引數 |
說明 |
| initializeRequest |
InitializeRequestEventArgs |
在偵測到新的 AJAX 要求已發出時立即引發。 |
| beginRequest |
BeginRequestEventArgs |
在要求送出之前引發。 |
| pageLoading |
PageLoadingEventArgs |
在回應已完全下載並處理之後,但在頁面上的內容更新之前引發。 |
| pageLoaded |
PageLoadedEventArgs |
在頁面上的內容全都重新整理之後引發。 |
| endRequest |
EndRequestEventArgs |
在所有工作都完成時引發。 |
如果您選擇指令碼服務方法,那麼您可能具有由 JavaScript 推動的前端,這可與 ASP.NET AJAX 指令碼 Web 服務所組成的伺服器端表面溝通。每一個服務的呼叫,都是經由明確的非同步指令碼呼叫進行。開發人員可完全掌控遠端呼叫的開始,並安全地停用任何 UI 部分。由於每個呼叫都是匿名進行,因此可以為呼叫的成功與失敗指定回呼。接著根據回呼,您可以將 UI 重設回適當的狀態。
話雖如此,停用和重新啟用 UI 項目時還是有一些問題。我會舉幾個案例來深入講解這些問題。
透過項目識別碼來停用 UI
[圖 2] 顯示的程式碼,是用來在回傳期間停用 AJAX 範例頁面上的一些項目。第一次載入頁面時,會登錄 BeginRequest 和 EndRequest 事件的處理常式。pageLoad 函式是按照慣例命名的處理常式,適用於 AJAX 用戶端程式庫觸發的應用程式載入事件。pageLoad 函式會接收兩個引數:一個屬於 Sys.Application 型別,另一個則是 ApplicationLoadEventArgs 物件。

Figure 2 在回傳期間停用 UI 的頁面
<html>
<body>
<script type="text/javascript">
function pageLoad()
{
var manager = Sys.WebForms.PageRequestManager.getInstance();
manager.add_beginRequest(OnBeginRequest);
manager.add_endRequest(OnEndRequest);
}
var lcPostbackElementID;
functionw OnBeginRequest(sender, args)
{
lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
if (lcPostbackElementID === "button1")
{
$get("Button1").disabled = true;
}
}
function OnEndRequest(sender, args)
{
if (lcPostbackElementID === "button1")
{
$get("Button1").disabled = false;
}
}
</script>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server" />
<div id="pageContent">
<b>Enter some data:</b><br />
<asp:TextBox ID="TextBox1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Post ..."
onclick="Button1_Click" />
<br /><br />
<asp:UpdateProgress runat="server" ID="UpdateProgress1">
<ProgressTemplate>
<img src="images/loading.gif" />
</ProgressTemplate>
</asp:UpdateProgress>
<asp:UpdatePanel ID="UpdatePanel2" runat="server"
UpdateMode="Conditional">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" />
</Triggers>
</asp:UpdatePanel>
</div>
</form>
</body>
</html>
BeginRequest (以及其他全部 [圖 1] 所列的事件) 會發生在每一次從頁面內部產生 AJAX 回傳時。這類事件只會通知開發人員已經提出要求或剛剛完成要求。舉例來說,這些事件不同於用戶端或伺服器按鈕的 click 事件。您知道即將進行回傳,但不會馬上知道為什麼或者是由哪個 UI 項目負責。但是為了停用使用者介面的正確部分,您必須確切知道被按一下的項目。
伴隨 BeginRequest 事件的資料結構中,有包含引起回傳的 DOM 項目參考。這個成員名為 postBackElement,您可以採用以下方法來存取:
var lcPostbackElement = args.get_postBackElement();
接著您就可以使用這項資訊來檢查被使用者按一下的項目,並決定需要停用哪些項目:
if (lcPostbackElement.id.toLowerCase() === "button1")
{
$get("Button1").disabled = true;
...
}
請注意,當您還原原始 UI 時,這項資訊與 EndRequest 事件無關。因此,您必須快取參考或只快取項目識別碼,以便從 EndRequest 事件處理常式內部再次檢視它。
若要停用或啟用 DOM 項目,您需要擷取項目的 DOM 參考,然後處理布林值停用屬性。在 ASP.NET AJAX 中,$get 函式等於 DOM document.getElementById 呼叫。更精確地說,$get 等於 getElementById 的參數化呼叫,也可接受選擇性參考來指示要搜尋之 DOM 樹狀子目錄的根目錄。
若要在指令碼服務呼叫中暫時停用 UI 項目,其實很簡單。唯一不同之處是使用 $get 函式的位置。您要在進行呼叫之前停用,並在成功和失敗回呼中重新啟用,如 [圖 3] 所示。

Figure 3 在指令碼服務呼叫中停用 UI 項目
function startCall()
{
$get("Button1").disabled = true;
...
Samples.RemoteService.GetTime(param1, param2,
onCompleted, onFailed);
}
function onCompleted(results)
{
// Process response
...
$get("Button1").disabled = false;
...
}
function onFailed(error)
{
$get("Button1").disabled = false;
...
}
在命名容器中停用 UI 項目
到目前為止所討論的程式碼都能順利運作,但還是不盡完美。萬一您需要停用內嵌在主版頁面或使用者控制項中的 UI 項目呢?
許多 ASP.NET 伺服器控制項都使用命名容器,為子控制項目的識別碼屬性值建立唯一命名空間。這在範本化及互動式資料繫結控制項中尤其重要,像是 GridView 和 Repeater。對每個控制項而言,命名容器是實作 INamingContainer 介面最接近的父控制項。舉例來說,UserControl 類別可做為它所包含的全部控制項的命名容器。與主版頁面相關的頁面上的任何控制項,也是一樣的情況。
在命名容器管轄底下的任一 ASP.NET 控制項,其用戶端識別碼的前面會加上父項目的識別碼。請參考下列程式碼片段:
<form runat="server">
<asp:Button runat="server" ID="Button1" />
...
</form>
按鈕的用戶端識別碼會符合伺服器識別碼,而且永遠是 Button1。但是,如果您在使用者控制項中移動按鈕,或從主版頁面衍生其內容頁面,這個程式撰寫特性便會突然改變。請參考下列使用者控制項:
<%@ Control Language="C#" CodeFile="test.ascx.cs" Inherits="test" %>
<asp:Button runat="server" ID="Button2" Text="Click" />
在 ASP.NET 網頁內嵌使用者控制項之後,發出的 HTML 會指向 Button2:
<input type="submit"
name="UserControl1$Button2"
value="Click"
id="UserControl1_Button2" />
每個內含的控制項識別碼前面,都會加上父項目的識別碼。只要您將自己限制在伺服器端程式設計的部分,這個行為對開發人員而言基本上是透明的。然而,如果您需要從用戶端編寫 Button2 的指令碼,就必須了解命名容器以及它們如何在頁面 DOM 中變更 HTML 項目的用戶端識別碼。
在 [圖 4] 中,您可以看見與 [圖 2] 相同的頁面,不過是以內容頁面的形式內嵌在主版頁面中。現在,Button1 的用戶端識別碼已變成:

Figure 4 在回傳期間停用 UI 的內容頁面
<%@ Page Language="C#" AutoEventWireup="true"
CodeFile="DisableUIEmb.aspx.cs"
MasterPageFile="~/Sample.master" Title="Disable UI Elements (Embedded)"
Inherits="Samples_DisableUIEmb" Theme="ProgAjax1" %>
<asp:Content runat="server" ContentPlaceHolderID="ScriptPlaceHolder">
<script type="text/javascript">
function pageLoad()
{
var manager = Sys.WebForms.PageRequestManager.getInstance();
manager.add_beginRequest(OnBeginRequest);
manager.add_endRequest(OnEndRequest);
}
var lcPostbackElementID;
function OnBeginRequest(sender, args)
{
lcPostbackElementID = args.get_postBackElement().id.toLowerCase();
if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
{
$get("<%= Button1.ClientID %>").disabled = true;
}
}
function OnEndRequest(sender, args)
{
if (lcPostbackElementID === "<%= Button1.ClientID.ToLower() %>")
{
$get("<%= Button1.ClientID %>").disabled = false;
}
}
</script>
</asp:Content>
<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
<div id="pageContent">
<b>Enter some data:</b><br />
<asp:TextBox ID="TextBox1" runat="server" />
<asp:Button ID="Button1" runat="server" Text="Post ..."
onclick="Button1_Click" />
<br /><br />
<asp:UpdateProgress runat="server" ID="UpdateProgress1">
<ProgressTemplate>
<img src="images/loading.gif" />
</ProgressTemplate>
</asp:UpdateProgress>
<asp:UpdatePanel ID="UpdatePanel2" runat="server"
UpdateMode="Conditional">
<ContentTemplate>
<asp:Label runat="server" ID="Label1" />
</ContentTemplate>
<Triggers>
<asp:AsyncPostBackTrigger ControlID="Button1" />
</Triggers>
</asp:UpdatePanel>
</div>
</asp:Content>
ctl00_ContentPlaceHolder1_Button1
若要正確停用 Button1 (或者廣泛來說,若要編寫它的指令碼),您必須知道這個新的識別碼。雖然名稱可以設法得知並在外部 JavaScript 檔案中硬式編碼,不過確切的識別碼只有在執行階段才會知道。[圖 4] 顯示如何使用 ASP 樣式程式碼區塊和伺服器端 ClientID 屬性,在瀏覽器參數型指令碼中發出,來解決命名容器問題。[圖 5] 顯示作用中的範例頁面。
圖 5 使用指令碼來調整 UI 的內容頁面 (按影像可放大)
使用 UpdatePanelAnimation 擴充項
停用的 UI 部分是與回呼相關的,您通常是使用純指令碼來定義它。在大多數的情況下,您只需要讓項目變成灰色;不過,其實您還可以做更多。例如,您可以使介面的部分淡出,然後再淡入。
如果您使用指令碼服務方法,那麼這種進階使用者互動的實作,便完全要由您自己來進行。不過,如果您是使用部分呈現方法並有安裝 AJAX Control Toolkit,就可以仰賴一個很棒的擴充項控制項:UpdatePanelAnimation 擴充項。
擴充項控制項是 ASP.NET 的伺服器控制項,這可為各式各樣現有的伺服器控制項提供額外的行為。當您需要控制器的特殊行為時,可以在擴充項中編寫行為的程式碼,然後在相同頁面中繫結擴充項與原始控制項。假設您要篩選出文字方塊中非數字的字元。您可以開發一個特定的 TextBox 控制項,也可以直接使用舊的 TextBox 控制項並以適當的行為進行擴充。這樣的好處是同樣的行為可以重複用於多個控制項型別,而且不僅如此,多重行為也可以自由結合,使伺服器控制項的特定執行個體更豐富。
UpdatePanelAnimation 擴充項的設計,可以在 ASP.NET AJAX 部分呈現頁面的可更新區域中加入一些動畫。您要以宣告方式建立關聯性,亦即在回傳時頁面所使用的動畫。擴充項可搭配 AJAX Control Toolkit 提供的指令碼型動畫架構一同運作。[圖 6] 顯示在可更新區域呈現動畫的程式碼範例。

Figure 6 使用 UpdatePanelAnimation 擴充項
<asp:UpdatePanel ID="UpdatePanel1" runat="server"
UpdateMode="Conditional">
<ContentTemplate>
<asp:Button ID="Button1" runat="server" Text="Load" ... />
<div id="Panel1">
...
</div>
</ContentTemplate>
<Triggers>
...
</Triggers>
</asp:UpdatePanel>
<act:UpdatePanelAnimationExtender ID="UpdatePanelAnimation1" runat="server"
TargetControlID="UpdatePanel1">
<Animations>
<OnUpdating>
<Sequence>
<EnableAction AnimationTarget="Button1" Enabled="false"/>
<FadeOut AnimationTarget="Panel1" minimumOpacity=".3" />
</Sequence>
</OnUpdating>
<OnUpdated>
<Sequence>
<FadeIn AnimationTarget="Panel1" minimumOpacity=".3" />
<EnableAction AnimationTarget="Button1" Enabled="true" />
</Sequence>
</OnUpdated>
</Animations>
</act:UpdatePanelAnimationExtender>
首先要將擴充項繫結到頁面中的特定 UpdatePanel 控制項,然後再設定動畫。[圖 6] 中的程式碼設定在回傳發生前 (OnUpdating 標記) 的兩個動作順序,以及在回傳完成時 (OnUpdated 標記) 另外設定兩個動作的順序。EnableAction、FadeOut 和 FadeIn 都是 AJAX Control Toolkit 支援的預先定義動畫。FadeOut 和 FadeIn 會使用 CSS 樣式,使套用這兩個動畫的 DOM 子樹狀目錄 (在本範例中為 Panel1) 淡出及淡入。EnableAction 動畫只會停用或啟用指定的目標 DOM 項目。
相較於直接撰寫指令碼,UpdatePanel 動畫擴充項對於宣告式程式設計的支援,可讓您不止是以灰色處理項目而已。不過,擴充項只限於使用部分呈現的頁面。如需詳細資訊,請參閱 AJAX Control Toolkit 首頁,網址是
codeplex.com/AtlasControlToolkit。
停用整頁
如前所述,要停用的 UI 部分取決於使用者按一下的控制項,更廣義的說,就是所需的動作。在部分呈現內容中,一次只能執行一個要求。識別並單獨停用擱置中作業所影響的介面部分,所需執行的工作太多,而且更重要的是,這樣無法防範不想要的滑鼠按鍵動作。稍後我將詳細說明。不過眼前您只要知道,停用整頁就不需要找出要變成灰色的控制項,這個解決方案更簡單有效。
其概念是建立一個與瀏覽器用戶端區域一樣大的 DIV 標記,再使用這個標記來覆蓋頁面的全部內容。這麼一來,任何滑鼠按鍵動作都將被這個 DIV 擷取,而且不會到達基礎 HTML 項目。您甚至可以建立 DIV 與某些樣式的關聯性,藉此在進行作業時讓頁面呈現灰色。[圖 7] 顯示用來完成這項作業的 JavaScript 程式碼。

Figure 7 使用 DIV 覆蓋 UI
<script type="text/javascript">
var _backgroundElement = document.createElement("div");
function pageLoad()
{
var manager = Sys.WebForms.PageRequestManager.getInstance();
manager.add_beginRequest(OnBeginRequest);
manager.add_endRequest(OnEndRequest);
// pageContent is the parent of the new DIV
$get("pageContent").appendChild(_backgroundElement);
}
function OnBeginRequest(sender, args)
{
EnableUI(false);
}
function OnEndRequest(sender, args)
{
EnableUI(true);
}
function EnableUI(state)
{
if (!state)
{
_backgroundElement.style.display = '';
_backgroundElement.style.position = 'fixed';
_backgroundElement.style.left = '0px';
_backgroundElement.style.top = '0px';
var clientBounds = this._getClientBounds();
var clientWidth = clientBounds.width;
var clientHeight = clientBounds.height;
_backgroundElement.style.width =
Math.max(Math.max(document.documentElement.scrollWidth,
document.body.scrollWidth), clientWidth)+'px';
_backgroundElement.style.height =
Math.max(Math.max(document.documentElement.scrollHeight,
document.body.scrollHeight), clientHeight)+'px';
_backgroundElement.style.zIndex = 10000;
_backgroundElement.className = "modalBackground";
}
else
{
_backgroundElement.style.display = 'none';
}
}
function _getClientBounds()
{
var clientWidth;
var clientHeight;
switch(Sys.Browser.agent) {
case Sys.Browser.InternetExplorer:
clientWidth = document.documentElement.clientWidth;
clientHeight = document.documentElement.clientHeight;
break;
case Sys.Browser.Safari:
clientWidth = window.innerWidth;
clientHeight = window.innerHeight;
break;
case Sys.Browser.Opera:
clientWidth = Math.min(window.innerWidth,
document.body.clientWidth);
clientHeight = Math.min(window.innerHeight,
document.body.clientHeight);
break;
default: // Sys.Browser.Firefox, etc.
clientWidth = Math.min(window.innerWidth,
document.documentElement.clientWidth);
clientHeight = Math.min(window.innerHeight,
document.documentElement.clientHeight);
break;
}
return new Sys.UI.Bounds(0, 0, clientWidth, clientHeight);
}
</script>
其中宣告了全域 JavaScript 變數來代表 DIV 標記。一經初始化,DIV 便會加入 DOM 樹狀目錄的特定位置中。在這個案例中,它已定義為包裝整個可視介面之 DIV 標記的子項目。BeginRequest 處理常式會設定並顯示 DIV,進而停用可視介面。EndRequest 處理常式只會隱藏 DIV,從而還原 UI:
function OnBeginRequest(sender, args)
{
EnableUI(false);
}
function OnEndRequest(sender, args)
{
EnableUI(true);
}
DIV 標記固定位於 0,0 位址,寬度與高度則是依照瀏覽器特定的方式動態計算。DIV 的大小會反映出瀏覽器用戶端區域的目前大小。getClientBounds 函式顯示指令碼程式設計範例,其中可應付各種瀏覽器間不同的物件模型。
如您所見,程式碼使用一些內建在 Microsoft® AJAX 用戶端程式庫中的類別。我借用了 ModalPopup 擴充項原始程式碼中的程式碼片段 -- ModalPopup 是 AJAX Control Toolkit 中最酷的擴充項之一。尤其,ModalPopup 擴充項可讓頁面以樣式對使用者顯示內容 -- 也就是,避免使用者與頁面的其餘部分互動。效果就與 window.alert 一樣,但是介面 (所顯示的控制項階層) 可完全由您決定。
[圖 8] 顯示頁面在部分呈現作業期間,使用強制回應 DIV 來停用整個 UI。
圖 8 部分呈現期間停用整頁 (按影像可放大)
一次一個 AJAX 回傳
既然部分呈現可讓您在不修改程式撰寫模型的情況下將 AJAX 功能加入傳統 ASP.NET 2.0 頁面中,因此可以完整保留頁面的伺服器生命週期。對於回傳事件、載入、呈現和檢視狀態都是如此。一般部分呈現呼叫的流量小於傳統 ASP.NET 呼叫,但是檢視狀態大小為常數,而且只能透過頁面特定的程式設計技巧來減少此大小。
藉由部分呈現,ASP.NET 網頁一方面享有 AJAX 網頁不間斷的服務體驗,另一方面又維持傳統的使用者介面。從用戶端來看,部分呈現頁面的行為好像它是單一循序 UI 的一部分。一次只能有一項擱置中作業,雖然從商業觀點來看,可以同時執行兩個作業。
假設有一個頁面會顯示關於客戶的各類資訊。此頁面包含兩個按鈕 - 一個用來下載訂單資訊,另一個用來顯示客戶詳細資料。從純粹商業的觀點來看,使用者大可接續按下兩個按鈕來產生兩項連續的要求,也就是第二個要求在第一個要求完成前開始。這兩項要求需要相同的輸入 (客戶識別碼),而且都是唯讀,這表示沒有干擾後端資料庫狀態之虞。然而,在部分呈現頁面中,這兩個呼叫卻不能同時執行,因為部分呈現是以傳統的 ASP.NET 程式撰寫模型為基礎 -- 每個要求都會傳回包含整頁的已更新檢視狀態的標記。如果您讓兩個要求同時執行,便可能危及頁面檢視狀態的一致性。快速依序傳送的兩個要求,會將相同的檢視狀態傳送給伺服器,但這兩個要求分別會傳回不同的檢視狀態,因為這些檢視狀態會針對處理要求的相關控制項而更新。這表示最後傳回的要求會覆寫第一個已傳回要求的變更。因此,部分呈現 AJAX 網頁仍會根據逐步處理模式運作。
為了避免發生這類問題,提出新的要求時,頁面要求管理員物件會自動刪除擱置中的要求。根據這項行為,在部分呈現頁面中停用整個 UI 是非常重要的。這樣可確保使用者不會在目前要求期間與整個頁面互動。如您所見,這段敘述直接抵觸了 AJAX 的一項重要優點 -- 非同步互動。
高優先順序呼叫
在討論部分呈現時會浮現某些問題,舉例來說,如果為了保持檢視狀態一致性而無法同時執行兩個要求,那麼小組為何不實作一個演算法來合併用戶端上的部分檢視狀態?這項疑問似乎很合理。但是,在某種程度上,由於檢視狀態的內部結構會包含編碼內容、雜湊值和伺服器專屬安全性值,因此最終還是未能採取這種做法。
結合用戶端上的兩個檢視狀態在技術上是可行的,但是這需要大幅改變目前的檢視狀態結構和 ASP.NET 執行階段。此外,將指令碼提供給所有人使用也會引發安全性疑慮,因為他們可以準備特定檢視狀態內容來進行重新執行與跨站台的指令碼處理等攻擊。
另一個常見問題是關於後到先贏原則,當頁面要求管理員在收到新的要求並刪除擱置中的要求時,便會實作這項原則。我發現如果由他人來做出程式撰寫決定,開發人員往往會覺得綁手綁腳的。因此,包括我在內的許多開發人員,都不喜歡 AJAX 回傳強加的後到先贏原則。那麼如果是先到先贏原則,讓目前要求有優先權,並取消後繼的要求,或排入佇列直到第一個要求完成,這樣又如何呢?
ASP.NET AJAX 架構有提供實作此模式的必要工具,稍後您就會看到。但是您真的確定要使用這個選項嗎?事實上,如果您使用部分呈現,就應該準備好讓頁面一次處理一個要求。要完成這項工作最簡單及安全的做法,就是停用整個 UI,藉此防止使用者點選項目。
萬一基於某些原因,您認為無法停用整個使用者介面,那麼了解如何實作先到先贏方法,就可能可以提供您所需的彈性。其中的概念是撰寫指令碼來攔截每個部分呈現要求的開頭 -- 頁面要求管理員的 initializeRequest 事件。在處理常式中,您要先檢查執行階段條件來確定是否真的要取消要求,並據此指示管理員:
function OnInitializeRequest(sender, args)
{
var manager = sender;
if (manager.get_isInAsyncPostBack() &&
args.get_postBackElement().id === "Button1")
{
args.set_cancel(true);
}
}
相同的程式碼也會檢查是否正在處理另一個要求,以及這個要求的優先順序是否較低。如果是的話,您可以在事件資料結構上將 Cancel 屬性設為 True,藉此取消要求。根據前段程式碼,最新的要求已經不見了。但是,您可以撰寫程式碼,為擱置中的要求實作內部佇列。如需高優先順序呼叫的詳細資訊,請造訪
ajax.asp.net/docs/tutorials/ExclusiveAsyncPostback.aspx。
如果您要向 Dino 提出問題或意見,請將郵件寄至 cutting@microsoft.com.
Dino Esposito是 Solid Quality Learning 的輔導老師,也是
Introducing ASP.NET AJAX (Microsoft Press,2007) 一書的作者。他目前居住在義大利,並常在世界各地的產業活動與會議發表演說。您可以透過電子郵件地址
cutting@microsoft.com 與他連絡,或造訪他的部落格,網址為 weblogs.asp.net/despos。