文章目錄
預設情況下,Page Controller 模式中所描述的概念是在 ASP.NET 中實現的。ASP.NET 頁面架構實現這些概念所採取的方式使得在用戶端上捕獲事件、將其傳輸到伺服器並調用適當方法這一系列操作的基本機制是自動進行的,並且對實現者來說是不可見的。頁面控制器是可擴充的,因為它會在生命週期的特定點上公開各種事件(請參閱此模式後面的"頁面生命週期"),因此,與應用程式具體相關的操作可以在適當的時候運行。
例如,假定使用者正在與包含一個按鈕伺服器控制項的 Web Form頁進行互動(請參閱此模式後面的"簡單頁面樣本")。當使用者單擊按鈕控制項時,一個事件將作為 HTTP 投遞內容傳送到伺服器,在那裡,ASP.NET 頁面架構會解釋投遞的資訊,並將引發的事件與適當的事件處理常式相關聯。架構自動調用該按鈕的適當事件處理常式,作為架構的正常處理的一部分。因此,您不再需要實現此功能。此外,您還可以使用內建控制器,或者,您可以用自己自訂的控制器來代替內建控制器(請參閱 Front Controller)。
頁面生命週期
下面按發生順序列出了頁面生命週期中最常見的各個階段。其中還包括引發的特定事件,以及處理請求時在各個階段可能執行的一些典型操作:
• |
ASP.NET 頁面架構初始化(事件:Init)。這是生命週期的第一個步驟,該步驟將初始化 ASP.NET 運行庫以便為響應請求做好準備。 |
• |
使用者代碼初始化(事件:Load)。您應該執行與應用程式具體相關的常見任務,例如,當頁面控制器引發 Load 事件時開啟資料庫連接。您可以假設:引發 Load 事件後,伺服器控制項已建立並完成初始化、狀態已還原並且表單控制項反映了用戶端的更改。 [Reilly02] |
• |
與應用程式相關的事件處理。在此階段,您應該執行與應用程式相關的處理,以響應控制器引發的事件。 . |
• |
清理(事件:Unload)。該頁面已完成組建,現在可以丟棄。您應該關閉 Load 事件開啟的任何資料庫連接,丟棄任何不再需要的對象。在連線物件被作為記憶體回收後,Microsoft?.NET Framework 將自動關閉資料庫連接。不過,您對何時進行記憶體回收沒有任何控制權。因此,顯式關閉資料庫連接以充分利用資料庫連接池是一個很好的做法。 |
注意:還有幾個頁面處理階段沒有在這裡列出。不過,這些階段不用於大多數頁面處理情況。
簡單頁面樣本
第一個樣本是一個簡單頁面,它接受來自使用者的輸入,然後在螢幕上顯示該輸入。該樣本說明了 ASP.NET 用於實現伺服器控制項的事件驅動模型。
圖 1: 簡單頁面
當使用者鍵入他或她的名字、然後單擊"Click Here"按鈕後,鍵入的名字將直接出現在按鈕下面, 2 所示。
圖 2: 顯示使用者輸入的簡單頁面
在 ASP.NET 網頁中,使用者介面編程分為兩個不同的部分:可視組件(或視圖)和結合了模型和控制器的邏輯。這種劃分將頁面的可視部分(視圖)同與頁面互動的、頁面後面的代碼(模型和控制器)分離開來。
可視元素稱為 Web Form頁。該頁面由包含靜態 HTML 伺服器控制項或 ASP.NET 伺服器控制項(或同時包含這兩種控制項)的檔案構成。在此樣本中,Web Form頁名為 SimplePage.aspx,它由以下程式碼群組成:
<%@ Page language="c#" Codebehind="SimplePage.aspx.cs" AutoEventWireup="false" Inherits="SimplePage" %> <HTML> <body> <form id="Form1" runat="server"> Name:<asp:textbox id="name" runat="server" /> <p /> <asp:button id="MyButton" text="Click Here" OnClick="SubmitBtn_Click" runat="server" /> <p /> <span id="mySpan" runat="server"></span> </form> </body> </HTML>
Web Form頁的邏輯由為了與表單進行互動而建立的代碼構成。編程邏輯放在一個與使用者介面檔案分離的檔案中。此檔案被稱為"程式碼後置"檔案,檔案名稱是 SimplePage.aspx.cs:
using System; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; public class SimplePage : System.Web.UI.Page { protected System.Web.UI.WebControls.TextBox name; protected System.Web.UI.WebControls.Button MyButton; protected System.Web.UI.HtmlControls.HtmlGenericControl mySpan; public void SubmitBtn_Click(Object sender, EventArgs e) { mySpan.InnerHtml = "Hello, " + name.Text + "."; } }
此代碼的用途是通知頁面控制器:當使用者單擊按鈕後,將向伺服器發送一個請求,並執行 SubmitBtn_Click 函數。
此實現顯示了串連到控制器所提供的事件是多麼簡單。它還說明,用這種方式編寫的代碼更易於理解,因為應用程式邏輯沒有與管理事件調度的低級代碼結合起來。
公用外觀樣本
下面的樣本使用頁面控制器的典型實現策略來提供顯示動態內容的橫幅,該橫幅在應用程式的每一頁上顯示已驗證的使用者的電子郵件地址(該地址是從資料庫檢索的)。
網站內的所有頁面對象所繼承的基類中包含了公用實現。圖 3 顯示了網站中的一個網頁。
圖 3: 顯示動態內容的橫幅
網站中的各個頁面負責呈現自己的內容,而基類則負責呈現頭資訊。因為各個頁面是從基類繼承的,所以它們都具有相同的功能。
此實現使用了稱為 Template Method的設計模式。該模式在一個操作中定義了一個演算法的架構,而將一些步驟交給子類完成。Template Method 允許子類重新定義演算法的某些步驟,而不必更改該演算法的結構。 [Gamma95]
將 Template Method 應用於此問題需要將公用代碼從各個頁面移到一個基類中。這樣可以確保公用代碼放在一個地方,並且很容易維護。在此樣本中,基類名為 BasePage 並負責將 Page_Load 方法串連到 Load 事件。與 BasePage 相關的工作(即從資料庫檢索使用者的電子郵件地址和佈建網站名)完成後,Page_Load 函數將調用名為 PageLoadEvent 的方法。子類實現 PageLoadEvent,以執行它們自己的特定 Load 功能。圖 4 顯示了此解決方案的結構。
圖 4: 程式碼後置頁面實現的結構
請求網頁時,ASP.NET 運行庫會觸發 Load 事件,該事件再調用 BasePage 的 Page_Load 方法。BasePage 方法檢索所需資料,然後對所請求的特定頁面調用 PageLoadEvent,以執行任何與頁面相關的所需載入。圖 5 顯示了頁面請求序列。
圖 5: 頁面請求序列
通過以這種方式實現公用功能,頁面不必設定頭資訊,並且還可以很容易地進行整個網站的更改。如果頭資訊呈現和初始化代碼不包含在一個檔案中,則必須對包含與頭資訊有關的代碼的所有檔案變更。
BasePage.cs
基類代碼實現了以下功能:
• |
將 Load 事件串連到 Page_Load 方法,以便進行與請求具體相關的初始化。 |
• |
從請求上下文檢索已驗證的使用者的名字,並使用 DatabaseGateway 類在資料庫中尋找該使用者的記錄。該代碼將 eMail 標籤分配給使用者的電子郵件地址。 |
• |
將網站名分配給 siteName 標籤。 |
• |
調用 PageLoadEvent 方法,可以由衍生類別實現該方法以進行任何與頁面相關的載入。 |
注意:最好將 BasePage 類定義為抽象類別,因為這樣可以強制實現者提供 PageLoadEvent 的實現。不過,在 Microsoft Visual Studio? .NET 中,不可能將此基類定義為抽象類別。相反,該類提供可由衍生類別覆蓋的預設實現。
using System; using System.Web.UI; using System.Web.UI.WebControls; public class BasePage : Page { protected Label eMail; protected Label siteName; virtual protected void PageLoadEvent(object sender, System.EventArgs e) {} protected void Page_Load(object sender, System.EventArgs e) { if(!IsPostBack) { string name = Context.User.Identity.Name; eMail.Text = DatabaseGateway.RetrieveAddress(name); siteName.Text = "Micro-site"; PageLoadEvent(sender, e); } } #region Web Form Designer generated code override protected void OnInit(EventArgs e) { // // // CODEGEN: 此調用是 ASP.NET Web Form設計器所必需的。 // InitializeComponent(); base.OnInit(e); } /// <summary> /// 設計器支援所必需的方法 - 不要使用代碼編輯器修改 /// 此方法的內容。 /// </summary> private void InitializeComponent() { this.Load += new System.EventHandler(this.Page_Load); } #endregion }
BasePage.inc
您不僅必須為頁面後面的邏輯代碼提供公用基類,而且還必須提供用來儲存視圖或 UI 的呈現代碼的公用檔案。該程式碼封裝括在每個 .aspx 頁面中。此 HTML 檔案不是為了用於進行獨立顯示。通過使用公用檔案,您可以在一個地方變更,並將這些更改傳播到包括該檔案的所有網頁。下面的範例程式碼顯示了此樣本的公用檔案,檔案名稱為 BasePage.inc:
<table width="100%" cellspacing="0" cellpadding="0"> <tr> <td align="right" bgcolor="#9c0001" cellspacing="0" cellpadding="0" width="100%" height="20"> <font size="2" color="#ffffff">歡迎: <asp:Label id="eMail" runat="server">username</asp:Label> </font> </td> </tr> <tr> <td align="right" width="100%" bgcolor="#d3c9c7" height="70"> <font size="6" color="#ffffff"> <asp:Label id="siteName" Runat="server">Micro-site Banner</asp:Label> </font> </td> </tr> </table>
DatabaseGateway.cs
該類封裝了這些頁面對資料庫的所有訪問。這是 Table Data Gateway [Fowler03] 的一個例子,它提供了此應用程式中的頁面的模型代碼。
using System; using System.Collections; using System.Data; using System.Data.SqlClient; public class DatabaseGateway { public static string RetrieveAddress(string name) { String address = null; String selectCmd = String.Format("select * from webuser where (id = '{0}')", name); SqlConnection myConnection = new SqlConnection("server=(local);database=webusers;Trusted_Connection=yes"); SqlDataAdapter myCommand = new SqlDataAdapter(selectCmd, myConnection); DataSet ds = new DataSet(); myCommand.Fill(ds,"webuser"); if(ds.Tables["webuser"].Rows.Count == 1) { DataRow row = ds.Tables["webuser"].Rows[0]; address = row["address"].ToString(); } return address; } }
Page1.aspx
下面是如何在頁面中使用公用功能的樣本:
<%@ Page language="c#" Codebehind="Page1.aspx.cs" AutoEventWireup="false" Inherits="Page1" %> <HTML> <HEAD> <title>Page-1</title> </HEAD> <body> <!-- #include virtual="BasePage.inc" --> <form id="Page1" method="post" runat="server"> <h1>Page: <asp:label id="pageNumber" Runat="server">NN</asp:label></h1> </form> </body> </HTML>
該檔案中的以下指令用於載入頭資訊的公用 HTML:
<!-- #include virtual="BasePage.inc" -->
Page1.aspx.cs
程式碼後置類別必須從 BasePage 類繼承,然後實現 PageLoadEvent 方法來進行任何與頁面具體相關的載入。在此樣本中,與頁面具體相關的活動是將數字 1 分配給 pageNumber 標籤。
using System; using System.Web.UI; using System.Web.UI.WebControls; public class Page1 : BasePage { protected System.Web.UI.WebControls.Label pageNumber; protected override void PageLoadEvent(object sender, System.EventArgs e) { pageNumber.Text = "1"; } }
返回頁首
測試考慮事項
對 ASP.NET 運行庫的依賴性使實現的測試變得很困難。不可能對從 System.Web.UI.Page 或環境中包含的其他各種類繼承而來的類進行執行個體化。這樣就不可能對應用程式的各個部分單獨進行單元測試。自動化的測試此實現的唯一方法是,產生 HTTP 要求,然後檢索 HTTP 響應,確定響應是否正確。此方法容易產生錯誤,因為您是將響應的文本與預期的文本進行比較。
返回頁首
結果上下文
內建的 ASP.NET 頁面控制器功能具有以下優缺點:
優點
• |
充分利用架構功能。頁面控制器功能內建在 ASP.NET 中,通過將與應用程式具體相關的動作串連到由控制器公開的事件,可以輕鬆地對它進行擴充。另外,通過使用程式碼後置功能,還可以很容易地將與控制器具體相關的代碼與模型和視圖代碼分離開來。 |
• |
顯式 URL。使用者輸入的 URL 引用了應用程式中的實際網頁。這意味著這些網頁可以作為書籤,並在以後輸入。URL 還傾向於使用更少的參數,以便讓使用者更容易輸入它們。 |
• |
增加了模組性和重用性。"公用外觀"樣本說明了您可以如何對許多頁面重用 BasePage,而不必修改 BasePage 類或 HTML 檔案。 |
缺點
• |
需要更改代碼。正如"公用外觀"樣本中說明的那樣,為了共用公用功能,必須對各個網頁進行修改,以便繼承新定義的基類而不是 System.Web.UI.Page。Intercepting Filter 模式描述了通過更改 Web.config 檔案而不是網頁本身來添加公用功能的機制。 |
• |
使用繼承。"公用外觀"樣本通過使用繼承來讓多個網頁共用實現。學習物件導向編程方法的大多數程式員一開始會喜歡繼承。不過,使用繼承來共用實現常常會導致軟體很難更改。如果基類因條件邏輯而變得複雜,最好引入協助器類或者考慮使用 Front Controller 。 |
• |
難以測試。由於頁面控制器是在 ASP.NET 中實現的,因此很難單獨測試。要提高可測試性,您 |
應該將同樣多的功能從 ASP.NET 專用代碼中分隔到不依賴於 ASP.NET 的類中。這樣,不必啟動 ASP.NET 運行庫就能進行測試。