5.3 Single Page架構
自從筆者于課堂上教導學員如何使用MasterPage、UpdatePanel等技術來撰寫網頁后,學員常問的問題莫過于樣板設計與異步刷新如何結合,學員們常常問道:“老師,我們的主網頁上右方有一個TreeView,用來切換要使用的功能頁面,功能頁面充分地利用了UpdatePanel控件,所以操作上并不會產生過多的刷新動作,不過現在仍有一個問題,那就是TreeView于切換功能頁面時,仍會因頁面的切換而產生刷新動作,能否連這個都避免掉?”呃!聽到這種問題,筆者也只能笑著說:“頁面切換時的閃爍是網頁程序的必要之惡,要連這點都避免,可不是寫幾百行程序代碼可以解決的,這是系統架構的問題了!”那此問題是否真的無解呢?未必!筆者回答的最后一段話是:這是系統架構的問題,而不是無解,因為只要動動腦筋,想想UpdatePanel、MasterPage,以及UpdatePanel動態加載UserControl等上面提過的技術,便能規劃出一個特殊的網頁程序架構,稱為Single Page架構(圖5-7)。在此架構中,系統中只有一個.aspx,套用了某個MasterPage,其余的功能頁面均以UserControl存在,當切換功能頁面時,事實上是動態加載了一個UserControl至UpdatePanel中,這樣一來不會有頁面切換,自然也就沒有網頁刷新的問題了。
那該如何實現這個架構呢?請照著以下步驟做。
1. 新增一個MasterPage,命名為DefaultFace.master。
2. 放入一個ScriptManager控件。
3. 建立一個單列兩欄的TABLE。
4. 將默認的ContentPlaceHolder1放到第二欄中。
5. 于第一欄放入一個TreeView控件,命名為TreeView1。

圖5-7
6. 于TreeView1中新增兩個Node,分別是Customers及Products,如程序5-8所示。
7. 建立一個新網頁,命名為DefaultFace.aspx,套用DefaultFace.master。
8. 于ContentPlaceHolder1中放入一個UpdatePanel控件,命名為UpdatePanel1,將UpdateMode設為Conditional。
9. 切換至DefaultFace.aspx.cs,鍵入程序5-9的程序代碼。
程序5-8
Samples\5\AdvAjaxDemo\DefaultFace.master <%@ Master Language="C#" AutoEventWireup="true" CodeFile="DefaultFace.master.cs" Inherits="DefaultFace" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Untitled Page</title> </head> <body> <form id="form1" runat="server"> <div> <asp:ScriptManager ID="ScriptManager1" runat="server"> </asp:ScriptManager> <table> <tr> <td valign=top> <asp:TreeView ID="TreeView1" runat="server" ImageSet="BulletedList" ShowExpandCollapse="False"> <ParentNodeStyle Font-Bold="False" /> <HoverNodeStyle Font-Underline="True" ForeColor="#5555DD" /> <SelectedNodeStyle Font-Underline="True" ForeColor="#5555DD" HorizontalPadding="0px" VerticalPadding="0px" /> <Nodes> <asp:TreeNode Text="基本數據" Value="None"> <asp:TreeNode Text="客戶數據" Value="Faces/CustomersControl.ascx"></asp:TreeNode> <asp:TreeNode Text="產品數據" Value="Faces/ProductsControl.ascx"></asp:TreeNode> </asp:TreeNode> </Nodes> <NodeStyle Font-Names="Verdana" Font-Size="8pt" ForeColor="Black" HorizontalPadding="0px" NodeSpacing="0px" VerticalPadding="0px" /> </asp:TreeView> </td> <td valign=top> <asp:contentplaceholder id="ContentPlaceHolder1" runat="server"> </asp:contentplaceholder> </td> </tr> </table> </div> </form> </body> </html>
程序5-9
Samples\5\AdvAjaxDemo\DefaultFace.master.cs using System; using System.Data; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class DefaultFace : System.Web.UI.Page { private void DetectControlLoad() { string loadKey = SearchParams("Dynamic_UserControl_Hidden1"); if (loadKey != null) { string controlLoaded = loadKey; LoadUserControl(controlLoaded); } } private string SearchParams(string paramName) { foreach (string key in Request.Params.Keys) { if (key != null && key.Contains(paramName)) return Request.Params[key]; } return null; } private void LoadUserControl(string path) { UpdatePanel1.ContentTemplateContainer.Controls.Clear(); HtmlInputHidden hidden1 = new HtmlInputHidden(); hidden1.Value = path; Control c = LoadControl(path); if (Cache[path] != null) c.ID = (string)Cache[path]; else { c.ID = "Dynamic_" + Guid.NewGuid().ToString().Replace('-', '_'); Cache[path] = c.ID; } hidden1.ID = "Dynamic_UserControl_Hidden1"; c.Controls.Add(hidden1); UpdatePanel1.ContentTemplateContainer.Controls.Add(c); } private void AttachTrigger() { TreeView tv = (TreeView)Master.FindControl("TreeView1"); AsyncPostbackTrigger trigger = new AsyncPostbackTrigger(); trigger.ControlID = tv.ID; trigger.EventName = "SelectedNodeChanged"; UpdatePanel1.Triggers.Add(trigger); } protected void Page_Init(object sender, EventArgs e) { AttachTrigger(); if (IsPostback && !Request.Params["__EVENTTARGET"].EndsWith( "TreeView1")) { DetectControlLoad(); } } protected void Page_Load(object sender, EventArgs e) { if (IsPostback && ScriptManager.GetCurrent( this).AsyncPostbackSourceElementID.EndsWith("TreeView1")) { TreeView tv = (TreeView)Master.FindControl("TreeView1"); if(tv.SelectedValue != "None") LoadUserControl(tv.SelectedValue); } else if (!IsPostback && Request.QueryString["Page"] != null) LoadUserControl("Faces/"+Request.QueryString["Page"] + ".ascx"); } }
程序5-9頗不容易理解,讓筆者針對其中的幾個關鍵函數做一些說明。首先是AttachTrigger函數,此函數于Page_Init函數中被調用,用途與前例相同,將MasterPage中的TreeView設定為本頁面中UpdatePanel1的Trigger,這樣只要TreeView產生Async-Postback動作,本頁面中的UpdatePanel1就被視為是需要刷新的UpdatePanel。在Page_Init函數中,若狀態為Postback,且觸發Postback的控件不是TreeView1的話,就代表網頁上已經有UserControl被加載了,此時必須進行UserControl的Reload動作,只有這樣才能維持UserControl的正常運作。當狀態處于Postback,且是Async-Postback時,Page_Load會判斷觸發的是否為TreeView1,是的話就代表必須進行UserControl的加載動作,此時將通過TreeView的SelectedValue來取得要加載的UserControl URL,然后進行加載動作。加載動作由LoadUserControl函數來運行,此處會先加載指定的UserControl,接著于該UserControl中插入一個Hidden Field,用途是記錄目前頁面上已載入的UserControl,有這個Hidden Field的存在,DetectControlLoad函數才能決定要重載哪一個UserControl。這里有一個相當特殊的設計,UserControl的ID是結合GUID產生的,不同的UserControl將對應著不同的GUID,此設計是應對當Async-Postback發生時,ASP.NET仍會由ViewState中讀出上次UserControl所存放的值,若不同的UserControl使用同樣的ID,可能會因此造成錯誤。在完成主要頁面的設計后,接著是設計功能頁面,也就是動態加載的UserControl,本例中將使用Northwind數據庫中的Customers、Products數據表為數據源,分別依上節的無刷新編輯模式撰寫CustomersControl.ascx及ProductsControl.ascx,由于這兩個頁面的建立方式與上節大致相同,此處就不再多言,直接看結果,如圖5-8所示。

圖5-8
這個范例不只因UserControl內部使用Async-Postback而不會有網頁閃爍情況發生,連帶著于切換時也因使用了Async-Postback而不發生網頁閃爍。在你對此架構感到滿意,立即想套用前,請先了解一點,這個架構的優點同時也是其缺點,因為沒有頁面的切換動作,功能頁面間的參數傳遞將無法循QueryString方式完成,而必須改由Session、Cache等其他途徑完成。也因為此點,當UserControl中以ScriptManagerProxy來含入外部的JavaScript文件時,這些JavaScript文件會一直存在于瀏覽器的內存中,即使切換到其他UserControl也不會移除,這點在套用此架構時要特別注意。另外,當用戶點擊瀏覽器的重新整理后,此架構不會停留在同一個功能頁面,而是回到最初的頁面。只有當這些問題可以被接受時,套用此架構才會有意義。另外,當需要從URL直接跳往某一頁面時,只需指定Page參數值為UserControl的名稱即可,例如于URL上直接鍵入:http://<your web server>/AdvAjaxDemo/DefaultFace.aspx?Page=CustomersControl,將會直接跳往客戶數據的功能頁面,這在需要以聯結方式跳往特定功能頁面時相當有用。