表示層設計能給客戶最直觀體驗與信心。如人際初次見面的感覺難以忘懷,產品若在用戶介面(UI)缺乏吸引力、不友好、操作不貼心,即使性能優異、架構合理、業務邏輯滿足需求,也難討客戶歡心。「佛要金裝,人要衣裝」,對 Web 應用而言,網頁如同人的衣裝,代表系統身份臉面,是招徠顧客最大賣點。
「獻醜不如藏拙」,筆者藝術細胞缺乏,不在 UI 美術設計上著墨,本章以架構設計角度闡述表示層設計中的模式應用、ASP.NET 控件設計運用,並介紹 ASP.NET 2.0 新特性。
表示層設計最重要模式為 MVC(Model-View-Controller,模型-視圖-控制器)。最早由 SmallTalk 語言研究團提出,廣泛應用於用戶交互應用程式。Controller 依用戶請求修改 Model 屬性,觸發 Event,所有依賴 Model 的 View 自動更新並基於 Model 產生響應返回 Controller。Martin Fowler 在《企業應用架構模式》展示完整流程:

將 MVC 拆解為三獨立部分,可透過 GOF 設計模式管理其關係:
.NET 平台下無需自行實現 MVC:
System.Web.UI.UserControl 自定義用戶控件,利用 ASPX 頁面組合 Web 控件實現視圖。System.Web.UI.Page 類相當於 Controller,處理用戶請求。Codebehind 技術使 UI 顯示與實現邏輯完全分離,View 與 Controller 相對獨立,利於代碼重用,更符合開發習慣,有利開發人員與 UI 設計人員分工協作。DataSet 方便與 Web 控件資料源繫結。PetShop 表示層設計充分利用 ASP.NET 技術特點,透過 Web 頁面與用戶控件控制展現視圖,利用 codebehind 將業務邏輯層領域對象加入表示層實現邏輯,形成典型 Page Controller 模式。此為 Martin Fowler《企業應用架構模式》重要表示層模式之一,.NET 下實現極簡單。
以 Products.aspx 為例,ASPX 頁面設定:
<%@ Page AutoEventWireup="true" Language="C#" MasterPageFile="~/MasterPage.master" Title="Products" Inherits="PetShop.Web.Products" CodeFile="~/Products.aspx.cs" %>
ASPX 頁面繼承 System.Web.UI.Page。Page 類繼承 Control 擁有 Web 控件特性,實作 IHttpHandler 介面:
[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal),
AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]
public interface IHttpHandler
{
void ProcessRequest(HttpContext context);
bool IsReusable { get; }
}
Page 實作 ProcessRequest() 設定 Request/Response 屬性完成用戶請求/響應控制。透過從 Control 繼承的 Load 事件建立 View 與 Model 關聯,如 Products.aspx.cs:
public partial class Products : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Page.Title = WebUtility.GetCategoryName(Request.QueryString["categoryId"]);
}
}
事件機制即 Observer 模式實現。ASPX 頁面 Load 事件觸發後,系統透過 WebUtility.GetCategoryName() 取得 Category 值顯示於頁面 Title。Page 作為 Controller 協調 View 與 Model 關係。
ASPX 頁面包含 Web 控件,同樣作為 View 對象,由 Page 類控制。如 CheckOut.aspx 中 Wizard 控件完成向導流程觸發 FinishButtonClick 事件,調用領域對象 Order.Insert():
public partial class CheckOut : System.Web.UI.Page
{
protected void wzdCheckOut_FinishButtonClick(object sender, WizardNavigationEventArgs e)
{
if (Profile.ShoppingCart.CartItems.Count > 0)
{
if (Profile.ShoppingCart.Count > 0)
{
CartListOrdered.Bind(Profile.ShoppingCart.CartItems);
ltlTotalComplete.Text = ltlTotal.Text;
ltlCreditCardComplete.Text = ltlCreditCard.Text;
OrderInfo order = new OrderInfo(int.MinValue, DateTime.Now, User.Identity.Name, GetCreditCardInfo(), billingForm.Address, shippingForm.Address, Profile.ShoppingCart.Total, Profile.ShoppingCart.GetOrderLineItems(), null);
Order newOrder = new Order();
newOrder.Insert(order);
Profile.ShoppingCart.Clear();
Profile.Save();
}
}
else
{
lblMsg.Text = "<p><br>Can not process the order. Your cart is empty.</p><p class=SignUpLabel><a class=linkNewUser href=Default.aspx>Continue shopping</a></p>";
wzdCheckOut.Visible = false;
}
}
}
此代碼典型表達 Model 與 View 關係:獲取控件屬性值作參數傳遞給數據值對象 OrderInfo,創建訂單對象,調用領域對象 Order.Insert() 寫入資料庫。同時對 ShoppingCart 數據判斷,值為 0 時顯示 UI 提示。View 內容決定 Model 值,Model 值反決定 View 顯示內容。
ASP.NET 控件是 View 對象最重要組成,充分利用面向對象設計思想,透過封裝與繼承構建控件對象,使開發 Web 頁面時能重用甚至自定義控件。System.Web.UI.Control 為控件樹根節點,定義所有控件共享屬性、方法、事件,管理控件整個執行生命週期。
Control 基類無 UI 特定功能,需 UI 相關屬性方法者須繼承 System.Web.UI.WebControls.WebControl(Control 子類,附加 ForeColor、BackColor、Font 等屬性)。另一重要類別 System.Web.UI.UserControl(用戶控件類,同為 Control 子類),可自定義用戶控件繼承之,在 VS Design 環境拖放控件組合成自定義控件,或在 codebehind 添加新屬性方法。
ASP.NET 控件類層次結構:

控件執行生命週期:
| 階段 | 控件需執行操作 | 要重寫的方法或事件 |
|---|---|---|
| 初始化 | 初始化 Web 請求生命週期內所需設置 | Init 事件 |
| 載入視圖狀態 | 自動填充 ViewState 屬性,可重寫 LoadViewState 自定義狀態還原 | LoadViewState 方法 |
| 處理回發數據 | 處理傳入表單數據並更新屬性(僅處理回發數據的控件參與) | LoadPostData 方法 |
| 載入 | 執行所有請求共有操作(如設定資料庫查詢),此時控件樹已建立初始化、狀態已還原、表單控件反映客戶端數據 | Load 事件 |
| 發送回發變更通知 | 引發變更事件響應回發間狀態變更(僅引發回發變更事件的控件參與) | RaisePostDataChangedEvent 方法 |
| 處理回發事件 | 處理引起回發的客戶端事件,在伺服器引發對應事件(僅處理回發事件的控件參與) | RaisePostBackEvent 方法 |
| 預呈現 | 呈現輸出前執行更新,預呈現階段對狀態更改會保存,呈現階段更改則丟失 | PreRender 事件 |
| 保存狀態 | 自動將 ViewState 保持為字串發送至客戶端作為隱藏變量,可重寫 SaveViewState 修改 ViewState | SaveViewState 方法 |
| 呈現 | 生成呈現給客戶端的輸出 | Render 方法 |
| 處置 | 銷毀控件前最終清理,必須釋放昂貴資源引用(如資料庫連接) | Dispose 方法 |
| 卸載 | 銷毀控件前最終清理,控件作者通常在 Dispose 執行清除 | UnLoad 事件 |
控件設計利用 Template Method 模式,Control 基類提供大量 protected 虛方法留待子類改寫。PetShop 4.0 定義兩個 ASP.NET 控件,均為 WebControl 子類:CustomList 繼承 DataList,CustomGrid 繼承 Repeater。
兩控件均改變父類呈現方式,故重寫父類 Render 虛方法完成自定義。CustomGrid 範例:
public class CustomGrid : Repeater
{
protected const string HTML1 = "<table cellpadding=0 cellspacing=0><tr><td colspan=2>";
protected const string HTML2 = "</td></tr><tr><td class=paging align=left>";
protected const string HTML3 = "</td><td align=right class=paging>";
protected const string HTML4 = "</td></tr></table>";
private static readonly Regex RX = new Regex(@"^&page=\d+", RegexOptions.Compiled);
private const string LINK_PREV = "<a href=?page={0}>< Previous</a>";
private const string LINK_MORE = "<a href=?page={0}>More ></a>";
private const string KEY_PAGE = "page";
private const string COMMA = "?";
private const string AMP = "&";
override protected void Render(HtmlTextWriter writer)
{
if (ItemCount == 0) { writer.Write(emptyText); return; }
string query = Context.Request.Url.Query.Replace(COMMA, AMP);
query = RX.Replace(query, string.Empty);
writer.Write(HTML1);
base.Render(writer);
writer.Write(HTML2);
if (currentPageIndex > 0) writer.Write(string.Format(LINK_PREV, (currentPageIndex - 1) + query));
writer.Write(HTML3);
if (currentPageIndex < PageCount) writer.Write(string.Format(LINK_MORE, (currentPageIndex + 1) + query));
writer.Write(HTML4);
}
}
CustomGrid 繼承 Repeater 同時繼承其 DataSource 虛屬性。預設 set 存取器:
public virtual object DataSource
{
get { ... }
set
{
if (((value != null) && !(value is IListSource)) && !(value is IEnumerable))
throw new ArgumentException(SR.GetString("Invalid_DataSource_Type", new object[] { this.ID }));
this.dataSource = value;
this.OnDataPropertyChanged();
}
}
CustomGrid 需不同設置行為,故改寫 DataSource:
private IList dataSource;
private int itemCount;
override public object DataSource
{
set
{
try { dataSource = (IList)value; ItemCount = dataSource.Count; }
catch { dataSource = null; ItemCount = 0; }
}
}
改寫 DataSource 後,勢必改寫 Repeater.OnDataBinding()。CustomGrid 另提供分頁功能,實現分頁相關操作。Repeater.OnDataBinding() 實際繼承並改寫 Control.OnDataBinding() 虛方法:
override protected void OnDataBinding(EventArgs e)
{
int start = CurrentPageIndex * pageSize;
int size = Math.Min(pageSize, ItemCount - start);
IList page = new ArrayList();
for (int i = 0; i < size; i++) page.Add(dataSource[start + i]);
base.DataSource = page;
base.OnDataBinding(e);
}
CustomGrid 另增 PageSize、PageCount 屬性與 SetPage() 方法等。ASP.NET 控件引入 Composite 與 Template Method 模式,自定義控件時可透過繼承改寫完成設計,既能按需實現特定功能,又最大限度實現對象重用,減少編碼量,利於未來擴展修改。
PetShop 4.0 除自定義 WebControl 子控件外,主要利用用戶控件。Controls 資料夾定義 11 個用戶控件,涵蓋客戶地址、信用卡、購物車、期望清單、導航、搜尋結果等資訊。它們屬組合控件,除包含子控件屬性方法外,也定義必要 UI 實現邏輯。以 ShoppingCartControl 為例,在呈現前做數據準備,獲取購物車數據繫結至下層 Repeater:
public partial class ShoppingCartControl : System.Web.UI.UserControl
{
protected void Page_PreRender(object sender, EventArgs e)
{
if (!IsPostBack) BindCart();
}
private void BindCart()
{
ICollection<CartItemInfo> cart = Profile.ShoppingCart.CartItems;
if (cart.Count > 0)
{
repShoppingCart.DataSource = cart;
repShoppingCart.DataBind();
PrintTotal();
plhTotal.Visible = true;
}
else
{
repShoppingCart.Visible = false;
plhTotal.Visible = false;
lblMsg.Text = "Your cart is empty.";
}
}
}
在 ShoppingCart 頁面加入該用戶控件:
<PetShopControl:shoppingcartcontrol id="ShoppingCartControl1" runat="server"></PetShopControl:shoppingcartcontrol>
用戶控件已實現呈現購物車數據邏輯,ShoppingCart.aspx.cs 即可不負責此邏輯,在完成對象重用過程中達到職責分離。用戶控件設計者與頁面設計者互不干擾分頭完成設計。對頁面設計者而言,可專注 UI 美觀友好,無需理會表示層對領域對象的調用操作,頁面代碼結構清晰、邏輯清楚。
PetShop 4.0 基於 .NET Framework 2.0 開發,表示層引入多項 ASP.NET 2.0 新特性:Membership、Profile、Master Page、登錄控件等。
Profile 提供用戶個性化服務。ASP.NET 1.x 利用 Session、Cookie 儲存用戶狀態,但 Session 有生存期,Cookie 有安全隱患且客戶端可禁用。Profile 解決此問題:將用戶個性化資訊保存在指定資料庫,預設支援 Access 與 SQL Server,其他資料庫可編寫 ProfileProvider。Profile 為強類型,可為用戶資訊建立屬性。PetShop 4.0 建立 ShoppingCart、WishList、AccountInfo 三屬性。
Profile 需訪問資料庫,故在數據訪問層(DAL)定義類似 Product 等資料表的模組結構。先定義 IProfileDAL 介面模組含 IPetShopProfileProvider 介面:
public interface IPetShopProfileProvider
{
AddressInfo GetAccountInfo(string userName, string appName);
void SetAccountInfo(int uniqueID, AddressInfo addressInfo);
IList<CartItemInfo> GetCartItems(string userName, string appName, bool isShoppingCart);
void SetCartItems(int uniqueID, ICollection<CartItemInfo> cartItems, bool isShoppingCart);
void UpdateActivityDates(string userName, bool activityOnly, string appName);
int GetUniqueID(string userName, bool isAuthenticated, bool ignoreAuthenticationType, string appName);
int CreateProfileForUser(string userName, bool isAuthenticated, string appName);
IList<string> GetInactiveProfiles(int authenticationOption, DateTime userInactiveSinceDate, string appName);
bool DeleteProfile(string userName, string appName);
IList<CustomProfileInfo> GetProfileInfo(int authenticationOption, string usernameToMatch, DateTime userInactiveSinceDate, string appName, out int totalRecords);
}
PetShop 4.0 分別支援 SQL Server 與 Oracle,定義兩個不同 PetShopProfileProvider 類實作介面,置於 SQLProfileDAL 與 OracleProfileDAL 模組。引入工廠模式,定義 ProfileDALFactory,工廠類 DataAccess:
public sealed class DataAccess
{
private static readonly string profilePath = ConfigurationManager.AppSettings["ProfileDAL"];
public static PetShop.IProfileDAL.IPetShopProfileProvider CreatePetShopProfileProvider()
{
string className = profilePath + ".PetShopProfileProvider";
return (PetShop.IProfileDAL.IPetShopProfileProvider)Assembly.Load(profilePath).CreateInstance(className);
}
}
業務邏輯層(BLL)定義 Profile 模組,引用 BLL、IProfileDAL、ProfileDALFactory 組件。定義密封類 PetShopProfileProvider 繼承 System.Web.Profile.ProfileProvider,重寫 Initialize()、GetPropertyValues()、SetPropertyValues()、DeleteProfiles() 等方法,為三屬性提供 Get/Set 方法。具體實現調用工廠類創建的具體類型對象:
private static readonly IPetShopProfileProvider dal = DataAccess.CreatePetShopProfileProvider();
定義後在 web.config 配置:
<profile automaticSaveEnabled="false" defaultProvider="ShoppingCartProvider">
<providers>
<add name="ShoppingCartProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
<add name="WishListProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
<add name="AccountInfoProvider" connectionStringName="SQLProfileConnString" type="PetShop.Profile.PetShopProfileProvider" applicationName=".NET Pet Shop 4.0"/>
</providers>
<properties>
<add name="ShoppingCart" type="PetShop.BLL.Cart" allowAnonymous="true" provider="ShoppingCartProvider"/>
<add name="WishList" type="PetShop.BLL.Cart" allowAnonymous="true" provider="WishListProvider"/>
<add name="AccountInfo" type="PetShop.Model.AddressInfo" allowAnonymous="false" provider="AccountInfoProvider"/>
</properties>
</profile>
Profile 資料存儲資料庫類型由以下配置決定:
<add key="ProfileDAL" value="PetShop.SQLProfileDAL"/>
表示層透過頁面 Profile 屬性訪問用戶個性化屬性。如 ShoppingCart.aspx.cs:
public partial class ShoppingCart : System.Web.UI.Page
{
protected void Page_PreInit(object sender, EventArgs e)
{
if (!IsPostBack)
{
string itemId = Request.QueryString["addItem"];
if (!string.IsNullOrEmpty(itemId))
{
Profile.ShoppingCart.Add(itemId);
Profile.Save();
Response.Redirect("~/ShoppingCart.aspx", true);
}
}
}
}
Profile 屬性值來源:配置 web.config 後啟動應用程式,ASP.NET 依配置創建 ProfileCommon 類實例(繼承 ProfileBase),調用繼承的 GetPropertyValue/SetPropertyValue 檢索設置屬性值,將實例設為頁面 Profile 屬性值。故可透過智能感知獲取 Profile.ShoppingCart,並利用 ProfileCommon.Save() 更新資料源。
PetShop 4.0 未使用 Membership 高級功能,直接讓 Membership 與 ASP.NET 2.0 新增登錄控件綁定。.NET Framework 2.0 內建針對 SQL Server 的 SqlMembershipProvider,故實現 Membership 比 Profile 簡單,僅需為 Oracle 定義 MembershipProvider。PetShop.Membership 模組定義 OracleMembershipProvider 繼承 System.Web.Security.MembershipProvider 抽象類,實現具高參考價值,需自定義 MembershipProvider 時可參考。
OracleMembershipProvider 實現不複雜,主要針對用戶與安全實現相關行為。父類 MembershipProvider 已定義虛方法,需重寫之。主要差異在資料庫訪問:SQL Server 用 aspnet_regsql 建立資料表與預存程序;Oracle 無類似工具,需自建資料表,且無預存程序,直接調用 SQL 語句。以 CreateUser() 為例,SqlMembershipProvider 呼叫預存程序 aspnet_Membership_CreateUser;OracleMembershipProvider 則開啟連接、事務,檢查用戶是否存在、Email 是否重複,執行 InsertUser() 插入數據,最後提交或回滾事務。
配置 web.config:
<membership defaultProvider="SQLMembershipProvider">
<providers>
<add name="SQLMembershipProvider" type="System.Web.Security.SqlMembershipProvider" connectionStringName="SQLMembershipConnString" applicationName=".NET Pet Shop 4.0" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" requiresUniqueEmail="false" passwordFormat="Hashed"/>
</providers>
</membership>
Oracle 版本配置類似。
登錄控件非單一控件,而是 ASP.NET 2.0 新提供一組解決用戶登錄的控件,與 Membership 整合,快速簡便實現登錄處理。包含:Login、LoginView、LoginStatus、LoginName、PasswordRecovery、CreateUserWizard、ChangePassword。
PetShop 4.0 如展示登錄控件用法的完美教程。SignIn.aspx 使用 Login 控件:
<asp:Login ID="Login" runat="server" CreateUserUrl="~/NewUser.aspx" SkinID="Login" FailureText="Login failed. Please try again."></asp:Login>
NewUser.aspx 使用 CreateUserWizard:
<asp:CreateUserWizard ID="CreateUserWizard" runat="server" CreateUserButtonText="Sign Up" InvalidPasswordErrorMessage="Please enter a more secure password." PasswordRegularExpressionErrorMessage="Please enter a more secure password." RequireEmail="False" SkinID="NewUser">
<WizardSteps>
<asp:CreateUserWizardStep ID="CreateUserWizardStep1" runat="server"/>
</WizardSteps>
</asp:CreateUserWizard>
使用登錄控件後無需編寫登錄相關代碼,大幅簡化系統設計實現。
Master Page 為整個 Web 站點統一模板,副檔名 .master,可包含靜態文本、HTML 元素、伺服器控件。以特殊 @Master 指令識別:

<%@ Master Language="C#" CodeFile="MasterPage.master.cs" Inherits="MasterPage" %>
使用 Master Page 可建立統一樣式,集中處理相似頁面的控件與代碼,修改一處即更新所有套用頁。PetShop 4.0 建立 MasterPage.master 包含 header、LoginView、導航選單、內容呈現 HTML 元素:

@Master 指令定義:
<%@ Master Language="C#" AutoEventWireup="true" CodeFile="MasterPage.master.cs" Inherits="PetShop.Web.MasterPage" %>
同樣採用 codebehind,代碼置於 MasterPage.master.cs:
public partial class MasterPage : System.Web.UI.MasterPage
{
private const string HEADER_PREFIX = ".NET Pet Shop :: {0}";
protected void Page_PreRender(object sender, EventArgs e)
{
ltlHeader.Text = Page.Header.Title;
Page.Header.Title = string.Format(HEADER_PREFIX, Page.Header.Title);
}
protected void btnSearch_Click(object sender, EventArgs e)
{
WebUtility.SearchRedirect(txtSearch.Text);
}
}
Master Page 不繼承 Page 而繼承 System.Web.UI.MasterPage(UserControl 子類)。適用指令與 UserControl 相同。每個內容頁須在 @Page 指令 MasterPageFile 屬性引用 Master Page,如 CheckOut 頁:
<%@ Page Language="C#" MasterPageFile="~/MasterPage.master" AutoEventWireup="true" CodeFile="CheckOut.aspx.cs" Inherits="PetShop.Web.CheckOut" Title="Check Out" %>
Master Page 可巢狀嵌套:子 Master Page 用 master 屬性指定父 Master Page。內容頁可視情況指向父或子 Master Page。雖多以宣告方式創建,也可建立類繼承 MasterPage 進行程式化創建(需同時創建 .master 檔)。調用也可用程式化方式,如動態添加 Master Page,重寫內容頁 Page_PreInit():
void Page_PreInit(Object sender, EventArgs e)
{
this.MasterPageFile = "~/NewMaster.master";
}
因 Master Page 在內容頁初始化階段合併(PreInit 階段完成分配)。
ASP.NET 2.0 新特性不限於上述,如 Theme、Wizard 控件等在 PetShop 4.0 大量應用。雖及時補足框架缺失、改進表示層設計,但屬「錦上添花」範疇,對整體表示層設計技術推動有限。
直到 AJAX(Asynchronous JavaScript and XML)出現,局面大為改觀。AJAX 雖帶「舊瓶裝新酒」味道,但誕生即具王者氣象,各大廠紛紛啟動開源 AJAX 專案,微軟推出 ASP.NET AJAX(含伺服端與客戶端組件,整合於 VS),為開發者提供強大 AJAX 應用環境。AJAX 已成 Web 應用主流開發技術。未來走向難預知,但從表示層設計角度,AJAX 帶來全新革命。期待未來 PetShop 5.0 在表示層帶來更多驚喜。