您要在 ASP.NET 中構建 Web 應用程序,您希望通過使用內置的 Page Controller(頁面控制器)來利用 ASP.Net 的事件驅動特性。
實現策略
默認情況下,Page Controller 模式中所描述的概念是在 ASP.NET 中實現的。ASP.Net 頁面框架實現這些概念所采取的方式使得在客戶端上捕獲事件、將其傳輸到服務器並調用適當方法這一系列操作的基本機制是自動進行的,並且對實現者來說是不可見的。頁面控制器是可擴展的,因為它會在生命周期的特定點上公開各種事件(請參閱此模式後面的"頁面生命周期"),因此,與應用程序具體相關的操作可以在適當的時候運行。
例如,假定用戶正在與包含一個按鈕服務器控件的 Web 窗體頁進行交互(請參閱此模式後面的"簡單頁面示例")。當用戶單擊按鈕控件時,一個事件將作為 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 窗體頁。該頁面由包含靜態 Html 服務器控件或 ASP.Net 服務器控件(或同時包含這兩種控件)的文件構成。在此示例中,Web 窗體頁名為 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 窗體頁的邏輯由為了與窗體進行交互而創建的代碼構成。編程邏輯放在一個與用戶界面文件分離的文件中。此文件被稱為"代碼隱藏"文件,文件名是 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 標簽分配給用戶的電子郵件地址。
將站點名分配給
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 窗體設計器所必需的。
//
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 運行庫就能進行測試。
相關模式
有關詳細信息,請參閱以下相關模式:
Template Method [Gamma95]。BasePage 類和 Page_Load 方法是此模式的示例實現。
Intercepting Filter.
Front Controller.
致謝
[Gamma95] Gamma, Helm, Johnson, and Vlissides. Design Patterns: Elements of Reusable Object-OrIEnted Software. Addison-Wesley, 1995.
[Reilly02] Reilly, Douglas J. Designing Microsoft ASP.Net Applications. Microsoft Press, 2002.
[Fowler03] Fowler, Martin. Patterns of Enterprise Application Architecture. Addison-Wesley, 2003.