領先技術:使用 ASP.NET 部分呈現功能進行 AJAX 編程 By Dino Esposito

領先技術
使用 ASP.NET 部分呈現功能進行 AJAX 編程
Dino Esposito

代碼下載位置: CuttingEdge2008_08.exe (470 KB)
在線瀏覽代碼
AJAX 的核心是 XMLHttpRequest 對象。AJAX 提供的用戶體驗機會取決於此對象在多個平臺的大量瀏覽器上的可用性。自 2004 年以來已發生了很多事情,那一年,組件供應商首次開始展示 AJAX 應用程序,但在覈心方面,如果不能使用 XMLHttpRequest 對象執行帶外調用,就不可能有 AJAX。
隨着 AJAX 應用程序複雜性的提高,開發人員逐漸意識到要通過一種經濟有效的方式構建新一代 Web 應用程序,只靠簡單的腳本驅動的帶外調用是不夠的。因此,對功能更加強大的工具集的需求不斷增長,開發人員希望這些工具能夠將 AJAX 功能添加到頁面和應用程序,同時使用與傳統的 ASP.NET 相同的開發模式。因此,簡言之,要構建 AJAX 站點,開發人員不僅需要能夠簡單地調用 XMLHttpRequest 對象來提取數據,能夠手動製作 JavaScript 功能來操作文檔對象模型 (DOM),還需要一些其他功能。
在本月的專欄中,我將介紹一種針對利用 ASP.NET 部分呈現引擎的 AJAX 的實用方法。稍後您將看到,AJAX 需要在應用程序性能和開發人員效率之間尋求平衡。實際的 AJAX 站點並不完全是使用部分呈現或 XMLHttpRequest 對象的手動腳本構建的,它們需要的是功能強大的混合技術,這些技術通常可以通過自定義控件很好地合成。

帶有菜單的頁面
AJAX 的主要功用是最小化要重新加載的完整頁面數目。部分呈現和手動腳本驅動調用都允許您提取服務器端數據,並避免整頁刷新。對於許多內容類型而言,後端服務和 DOM 操作可能已經足夠了。但如果您必須支持導航,會怎樣呢?
傳統的超鏈接破壞了 AJAX 的神奇功能,它會告知瀏覽器請求另一個 URL。結果,當前頁面被凍結,直到下載完新的 HTML 塊。當新數據到達時,該頁面關閉,然後完全重繪瀏覽器的客戶端。
位於母版頁上的頂級鏈接可向用戶指示站點的不同區域,這些鏈接可能確實會像傳統超鏈接一樣實現。在這種情況下,重新加載整個頁面可能是可以接受的,但具體取決於用戶的期望。
每次構建菜單時,都需要選擇如何處理用戶單擊。您可以爲每個菜單項分配一個 URL,也可以只是回發到同一頁面。從 AJAX 角度看,您是在選擇關閉當前頁面、加載全新頁面還是從服務器異步加載某些新內容。
請看以下代碼片段,它顯示了 ASP.NET 菜單控件的片段:
<asp:Menu runat="server" ID="Menu1">
<asp:MenuItem Text="Products" Value="Products">
    <asp:MenuItem Text="By price" NavigateUrl="..." /> 
    <asp:MenuItem Text="All" Value="Products-All" /> 
</asp:MenuItem>
...
</asp:Menu> 
“產品”菜單項的第一個 MenuItem 子元素起到的是 NavigateUrl 屬性的作用。此屬性可獲取或設置單擊菜單項時 URL 導航到的目標位置。第二個 MenuItem 元素起到的是 Value 屬性的作用。Value 屬性用於存儲有關菜單項的其他數據,將被傳遞給 MenuItem 的回發事件。同一級別的每個菜單項都必須擁有唯一的 Value 屬性值。當未指定顯式導航 URL 時,單擊菜單項會導致傳統的回發。在服務器上,您只需處理 MenuItemClick 事件和 Value 屬性的內容。
顯然,如果您選擇使用 Value 屬性的方法,就可以使用 AJAX 技術並避免整頁重新載入。此類型頁面的整體模型是一個單頁界面。
單頁界面減少了重新加載的數量並消除了閃爍,因此可減輕用戶界面的負擔。然而,單頁界面也意味着您的應用程序使用的 URL 區別很小,從而使來自搜索引擎的支持減弱。單頁界面也意味着開發團隊需要編寫的頁面減少,但頁面內容更加豐富。此類方法還可以減少團隊內部的並行操作。

導航 URL
當菜單將用戶引入全新頁面時,實際上您不需要使用“菜單”控件來實現解決方案。理論上,有超鏈接列表就已經足夠了。雖然屆時會有許多 DHTML 和 AJAX 菜單框架可用,但到最後,它們的動畫、圖形和提供的預定義外觀會不同。從功能上講,這些菜單只是超鏈接的集合,這些超鏈接由瀏覽器通過非 AJAX 方式本地處理。
通常,總頁數多達數百的網站可能是針對少數入口頁設置的,這些入口頁隨後會將用戶引入不同的子網站。因此,主頁只需要指向這些子網站的鏈接。在這種情況下,就需要導航,而不必非得用 AJAX。但是如果需要將內容拉至當前顯示的頁面,又該如何呢?您要如何組織此內容才能最大程度地提高應用程序的性能和團隊的工作效率?

異步回發
上個月,我演示瞭如何使用 Windows® Communication Foundation (WCF) 服務將原始數據或 HTML 返回到客戶端。兩種解決方案都各有優缺點。發送原始數據可優化帶寬,但需要您使用 JavaScript 在此客戶端上實現一些 DOM 操作邏輯。發送服務器生成的 HTML 會增加移動的數據量,但它允許您在服務器上維護大多數呈現邏輯。
這兩種方法均屬於所謂的“真正的”AJAX 解決方案類別,在此類別中基於雙層模型 — Web 瀏覽器和服務層明確設計了應用程序。涉及用戶界面的任何狀態和邏輯均由客戶端維護並控制;不需要任何視圖狀態或回發。
若要返回只讀標記,則服務器生成的 HTML 最有用。如果您需要返回靜態數據網格,或返回包含所選客戶或發票的一些相關信息的面板,它將非常適合。如果您需要打開一個充滿控件的交互面板,並且這些控件會觸發事件並需要處理程序,它的吸引力就小得多了。在純 AJAX 方法中,顯示的標記(在客戶端生成或在服務器上生成)必須包含 JavaScript 函數調用。
上個月,我使用服務實現了 HTML 消息模式和瀏覽器端模板模式。但您可能已經注意到,我的示例並非真正具有交互效果。事實證明這兩種方法適用於部分情況,而非全部。我的示例服務返回了股票報價,但最終網格無法分頁。例如,要支持分頁,我必須插入指向 JavaScript 功能的超鏈接,然後確保下載了引用的 JavaScript。

菜單和部分呈現
圖 1 顯示了帶有頂級菜單的示例頁,允許用戶在應用程序功能集中導航。此菜單已使用 ASP.NET 菜單控件創建,如圖 2 所示。您可以看到,所有菜單項都沒有指定 NavigateUrl 屬性,這個屬性會將每個菜單項指向一個物理 URL,這就可能會指向一個完全不同的頁面。指定 Value 屬性後,每當用戶選擇任一菜單項後,都會發生回發,並且 MenuItemClick 事件將被引發至該頁面:
void Menu1_MenuItemClick(object sender, MenuEventArgs e)
{
    LoadContent(e.Item.Value);
}
圖 1 帶有頂級菜單的 ASP.NET AJAX 頁(單擊圖像可查看大圖)
MenuEventArgs 類的功能類似於 Item 屬性,表示單擊的菜單項。MenuItem 對象的 Value 屬性會通知事件處理程序有關單擊項的標識的信息。通過使用本地 LoadContent 功能,此頁可以動態加載所需內容。但是回發如何實現?
ASP.NET 菜單控件完全支持部分呈現。要放棄回發並順利加載新內容,您只需設置 ScriptManager 控件並插入一個 UpdatePanel 控件,如下所示:
<asp:UpdatePanel runat="server" ID="UpdatePanel1" 
          UpdateMode="Conditional">
    <ContentTemplate>    
       ...
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Menu1" 
            EventName="MenuItemClick" />
    </Triggers>
</asp:UpdatePanel>
可更新的內容將綁定到菜單的 MenuItemClick。通過 UpdateMode 屬性,面板也會進行配置以適應特定於控件的更新。這意味着,僅當用戶單擊菜單項時,纔會刷新可更新的區域。
在大多數的 Web 應用程序中,您都是使用菜單將用戶引向站點或頁面的不同功能區域。使用不同的 ASPX 頁面實現特定功能是非常有幫助的,原因有很多。它可以保持代碼的清晰,並且更易於維護和測試。但更重要的是,它允許您將不同功能的實現分配給團隊中的不同開發人員。這就可以並行執行一些開發任務,從而提高生產率。
單頁界面是典型的 AJAX 模式,它建議您在每次觸發事件時都會重排用戶界面的應用程序中,創建一個主頁。單頁界面模型非常適用於最小化回發,但當面對團隊的並行開發任務時,就需要謹慎一些。您應該將其視爲基於插件概念的一個頁面體系結構。換句話說,頁面會使用一個約定的接口定義佔位符,藉助此接口可快速輕鬆地插入動態加載的組件。

提供佔位符
在 ASP.NET 中,實現此面向頁面體系結構的插件的最簡單方法,就是使用 PlaceHolder 控件和 ASCX 用戶控件定義按需組件。定義可更新區域的代碼如下:
<asp:UpdatePanel runat="server" ID="UpdatePanel1" 
    UpdateMode="Conditional">
    <ContentTemplate>    
        <asp:PlaceHolder runat="server" ID="PlaceHolder1"
            EnableViewState="false" />
    </ContentTemplate>
    <Triggers>
        <asp:AsyncPostBackTrigger ControlID="Menu1" 
            EventName="MenuItemClick" />
    </Triggers>
</asp:UpdatePanel>
您將每個菜單項與一個 ASCX 用戶控件關聯,並在每次單擊項目時將其加載到佔位符中。圖 3 顯示了上述 LoadContent 函數的源代碼,您從 MenuItemClick 事件處理程序中調用該函數。
Page 類上的 LoadControl 方法採用 ASP.NET 用戶控件的 URL,並將它作爲由 UserControl 派生的類的實例加載到內存中。如果控件加載正確,此方法會將其添加到 PlaceHolder 的 Controls 集合。請注意,ASP.NET PlaceHolder 控件不輸出任何標記,因此不必擔心使用它會擾亂您的用戶界面標記。
在包含動態和交互內容的 AJAX 站點上下文中使用 ASCX 用戶控件不會減少您同時開發多個內容塊的機會 — ASCX 控件是一種 ASP.NET 頁,可以獨立於站點的其餘部分開發。

動態加載的控件
在 ASP.NET 中,需要稍微注意一下動態加載的控件,因爲它們的行爲會跨過回發。假設您創建了一個包含可分頁客戶網格的控件,然後將這個用戶控件加載到佔位符中(請參見圖 4)。在您單擊底部的一個超鏈接導航到新頁面之前,一切工作正常。單擊後,此頁將部分刷新,但是用戶控件的全部內容都會消失。爲什麼呢?
圖 4 動態加載到頁面的交互式控件(單擊圖像可查看大圖)
在 ASP.NET 中,每個頁面請求都被視爲獨立於此前或此後的任何其他請求。因此要創建該頁面類的全新實例,爲每個傳入請求服務。此新頁面包含在 ASPX 源代碼中靜態引用的所有服務器控件的全新實例。
但動態添加的控件會怎樣呢?只有頁面中的代碼使用一些永久機制跟蹤動態控件時,頁面纔會知道它們。頁面視圖狀態是此類型信息的優良存儲媒體。在圖 3 中,我將 TrackUserControl 屬性添加到了頁面類,以提醒當前顯示的是哪個用戶控件。
在 Page_Load 事件中,您使用 TrackUserControl 屬性的內容指明必須手動重新加載哪個用戶控件,才能完全還原頁面中上次包含的控件集合:
void Page_Load(object sender, EventArgs e)
{
    if (!IsPostingFromMenu() && IsPostBack)
        ReloadContent();
}
注意,回發的原因可能是用戶觸發了顯示的用戶控件中的某個操作,也可能是用戶單擊了其他菜單項。Page_Load 中的 if 語句可確定用戶是否從菜單回發。如果是,則不會引發任何操作,因爲必須加載一些新內容;否則,將重新加載跟蹤的用戶控件,以便其處理回發事件:
void ReloadContent()
{ 
    UserControl uc = null;
    try
    {
        uc = this.LoadControl(TrackedUserControl) as UserControl;
    }
    catch
    {
    }
    if (uc != null)
        Placeholder1.Controls.Add(uc);
}
動態添加的用戶控件的視圖狀態會如何呢?已經向其傳遞 HTML 元素的數據又會如何呢?當爲用戶提供頁面時,靜態添加的控件和動態添加的控件之間沒什麼差異。當提交表單時,用戶輸入的任何內容(例如,輸入字段中的文本)都將自動打包到 HTTP 請求。因此,在服務器上,動態加載的控件使用的任何信息都是可用的。然而,您應該知道,當觸發頁面的 Init 事件時,此類控件尚不可用。這是因爲頁面的控件樹只使用靜態引用的控件填充。
直到該頁面的作者向樹中添加任何動態引用的控件。不過,頁面作者可能需要從永久存儲媒體中讀取有關所添加控件的信息。如果此信息存儲在會話或緩存中,則可以通過 Init 事件處理程序安全地訪問它。如果該信息存儲在視圖狀態袋中,就必須等待 Load 事件。事實上,在 ASP.NET 頁面生命週期中,視圖狀態是在 Init 和 Load 事件之間進行解壓縮和處理的。
在此階段,還會檢查已發佈的所有數據並將其映射到現有控件。但是專門用於動態添加的控件的數據又如何呢?基本上,已發佈的數據是在 Page__Load 事件之前或之後處理的。在第一輪中未找到任何匹配控件的數據將緩存,並在 Load 事件之後再次處理。
在 Load 事件中,開發人員應該檢查上次動態添加的是哪些控件,並在頁面控件樹中重新加載它們。恰好足夠,Controls 集合的 Add 方法可使用在視圖狀態下找到的控件的任何數據來自動更新該控件的狀態。

哪個控件進行了回發?
從外部資源加載其部分內容的頁面需要了解引發回發的原因。在圖 4 所示的示例代碼中,頁面會回發,原因可能是用戶從菜單中選擇了給定元素,也可能是用戶在使用該用戶界面上的其他組件時觸發了回發。
在 ASP.NET 中,沒有哪個屬性能夠告訴您是哪個控件導致的回發。不過,發佈控件的 ID 卻不難找到。如果用戶通過單擊“提交”按鈕進行回發,則該控件的 ID 將在 HTTP 請求中列出。如果在 HTTP 請求的主體中不存在任何按鈕控件,則用戶將通過與“鏈接”按鈕或自動回發控件交互來進行回發。
無論如何,回發源的 ID 都位於 __EVENTTARGET 隱藏字段中。在 ASP.NET AJAX 中,當使用部分呈現時,事情就會簡化,因爲 ScriptManager 控件發佈了一個自定義屬性 AsyncPostBackSourceElementID。以下代碼是生成圖 4 的代碼的摘錄,它展示瞭如何確定用戶是單擊菜單項還是通過另一個控件回發到服務器的:
bool IsPostingFromMenu()
{
    ScriptManager sm = ScriptManager.GetCurrent(this);
    string ctlID = sm.AsyncPostBackSourceElementID;
    Control c = this.FindControl(ctlID);
    if (c == null)
        return false;

    return (c.ID == "Menu1");
}
當用戶重複單擊菜單時,可能會再次加載給定的 ASCX 用戶控件。加載用戶控件意味着要下載控件標記並走完服務器生命週期。然而,不斷重新加載用戶控件可能會對性能產生負面影響,因此您可以使用一項 ASP.NET 功能(如輸出緩存)來幫助提升頁面的整體性能。

緩存用戶控件的輸出內容
要創建向站點提供某些內容的用戶控件,成本可能非常昂貴,因爲這需要一個或多個數據庫調用並可能需要幾百萬個 CPU 循環。既然這樣,那爲什麼還要每秒鐘多次重新生成同樣的用戶控件,尤其是在內容並非在不斷更改的情況下?
比較好的策略是創建一次用戶控件,緩存其輸出併爲其指定最大期限。一旦用戶控件的緩存快照過時,第一個傳入請求就會以標準方式再次服務,再次運行控件的代碼並緩存生成的標記。
ASP.NET 頁面輸出緩存功能允許您緩存頁面和用戶控件響應,這樣無需執行代碼即可滿足後續請求,只要返回緩存的輸出即可。要使用戶控件可緩存,您可以在 ASCX 的源中聲明 @OutputCache 屬性,如以下代碼所示。此代碼片段會將該控件的輸出緩存一分鐘:
<% @OutputCache Duration="60" VaryByParam="None" %>
輸出緩存是一項出色的功能,但是它距離成爲萬靈丹還很遠。它只是一種使應用程序服務於更多頁面並使用戶控件更爲快速的方法。它不一定能夠使應用程序效率更高或更具可擴展性。不過,藉助輸出緩存,您一定可以降低服務器上的工作量,這是因爲頁面和資源都是緩存的下游項。
另外,輸出緩存僅僅是匿名內容的一項可行選擇。請求的緩存內容直接由 IIS 提供服務,並且從不將其傳遞到能夠驗證調用方身份的 ASP.NET 管道。
最後,回發的頁面和用戶控件需要一些額外工作。特別是,您需要指示 ASP.NET 保存輸出的多個副本,這些副本之間會有一個或多個參數不同。爲此,您需要在 @OutputCache 指令中使用 VaryByParam 屬性。

側重實用性
雖然部分呈現經常被認爲是廉價的 AJAX,是爲那些不能運用所需的能源創建“真正的”AJAX 應用程序的人保留的,但它當時是使用 ASP.NET 的開發人員的最佳選擇之一。最後,請記住,AJAX 涉及由多種技術和模式的組合所帶來的許多折衷和有效的 AJAX 結果。

請將您想向 Dino 詢問的問題和提出的意見發送至 [email protected]

Dino Esposito 目前是 IDesign 的架構師,也是《Programming ASP.NET 3.5 Core Reference》的作者。Dino 定居於意大利,經常在世界各地的業內活動中發表演講。您可加入他的博客,網址爲 weblogs.asp.net/despos
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章