表示層(Presentation Layer)的設計可以給系統客戶最直接的體驗和最十足的信心。正如人與人的相交相識一樣,初次見面的感覺總是永難忘懷的。一件交付給客戶使用的產品,如果在用戶界面(User Interface,UI)上缺乏吸引人的特色,界面不友好,操作不夠體貼,即使這件產品性能非常優異,架構設計合理,業務邏輯都滿足了客戶的需求,卻仍然難以討得客戶的歡心。俗語云:“佛要金裝,人要衣裝”,特別是對於Web應用程序而言,Web網頁就好比人的衣裝,代表着整個系統的身份與臉面,是招徠“顧客”的最大賣點。
“獻醜不如藏拙”,作爲藝術細胞缺乏的我,並不打算在用戶界面的美術設計上大做文章,是以本書略過不提。本章所關注的表示層設計,還是以架構設計的角度,闡述在表示層設計中對模式的應用,ASP.NET控件的設計與運用,同時還包括了對ASP.NET 2.0新特色的介紹。
6.1 MVC模式
表示層設計中最重要的模式是MVC(Model-View-Controller,即模型-視圖-控制器)模式。MVC模式最早是由SmallTalk語言研究團提出的,被廣泛應用在用戶交互應用程序中。Controller根據用戶請求(Response)修改Model的屬性,此時Event(事件)被觸發,所有依賴於Model的View對象會自動更新,並基於Model對象產生一個響應(Response)信息,返回給Controller。Martin Fowler在《企業應用架構模式》一書中,展示了MVC模式應用的全過程,如圖6-1所示:
圖6-1 典型的MVC模式
如果將MVC模式拆解爲三個獨立的部分:Model、View、Controller,我們可以通過GOF設計模式來實現和管理它們之間的關係。在體系架構設計中,業務邏輯層的領域對象以及數據訪問層的數據值對象都屬於MVC模式的Model對象。如果要管理Model與View之間的關係,可以利用Observer模式,View作爲觀察者,一旦Model的屬性值發生變化,就會通知View基於Model的值進行更新。而Controller作爲控制用戶請求/響應的對象,則可以利用Mediator模式,專門負責請求/響應任務之間的調節。而對於View本身,在面向組件設計思想的基礎上,我們通常將它設計爲組件或者控件,這些組件或者控件根據自身特性的不同,共同組成一種類似於遞歸組合的對象結構,因而我們可以利用Composite模式來設計View對象。
然而在.NET平臺下,我們並不需要自己去實現MVC模式。對於View對象而言,ASP.NET已經提供了常用的Web控件,我們也可以通過繼承System.Web.UI.UserControl,自定義用戶控件,並利用ASPX頁面組合Web控件來實現視圖。ASP.NET定義了System.Web.UI.Page類,它相當於MVC模式的Controller對象,可以處理用戶的請求。由於利用了codebehind技術,使得用戶界面的顯示與UI實現邏輯完全分離,也即是說,View對象與Controller對象成爲相對獨立的兩部分,從而有利於代碼的重用性。比較ASP而言,這種編程方式更符合開發人員的編程習慣,同時有利於開發人員與UI設計人員的分工與協作。至於Model對象,則爲業務邏輯層的領域對象。此外,.NET平臺通過ADO.NET提供了DataSet對象,便於與Web控件的數據源綁定。
6.2 Page Controller模式的應用
通觀PetShop的表示層設計,充分利用了ASP.NET的技術特點,通過Web頁面與用戶控件控制和展現視圖,並利用codebehind技術將業務邏輯層的領域對象加入到表示層實現邏輯中,一個典型的Page Controller模式呼之欲出。
Page Controller模式是Martin Fowler在《企業應用架構模式》中最重要的表示層模式之一。在.NET平臺下,Page Controller模式的實現非常簡單,以Products.aspx頁面爲例。首先在aspx頁面中,進行如下的設置:
Aspx頁面繼承自System.Web.UI.Page類。Page類對象通過繼承System.Web.UI.Control類,從而擁有了Web控件的特性,同時它還實現了IHttpHandler接口。作爲ASP.NET處理HTTP Web請求的接口,提供瞭如下的定義:
Level=AspNetHostingPermissionLevel.Minimal),
AspNetHostingPermission(SecurityAction.LinkDemand,
Level=AspNetHostingPermissionLevel.Minimal)]
public interface IHttpHandler
{
void ProcessRequest(HttpContext context);
bool IsReusable { get; }
}
Page類實現了ProcessRequest()方法,通過它可以設置Page對象的Request和Response屬性,從而完成對用戶請求/相應的控制。然後Page類通過從Control類繼承來的Load事件,將View與Model建立關聯,如Products.aspx.cs所示:
{
protected void Page_Load(object sender, EventArgs e)
{
//get page header and title
Page.Title = WebUtility.GetCategoryName(Request.QueryString["categoryId"]);
}
}
事件機制恰好是observer模式的實現,當ASPX頁面的Load事件被激發後,系統通過WebUtility類(在第28章中有對WebUtility類的詳細介紹)的GetCategoryName()方法,獲得Category值,並將其顯示在頁面的Title上。Page對象作爲Controller,就好似一個調停者,用於協調View與Model之間的關係。
由於ASPX頁面中還可以包含Web控件,這些控件對象同樣是作爲View對象,通過Page類型對象完成對它們的控制。例如在CheckOut.aspx頁面中,當用戶發出CheckOut的請求後,作爲System.Web.UI.WebControls.Winzard控件類型的wzdCheckOut,會在整個嚮導過程結束時,觸發FinishButtonClick事件,並在該事件中調用領域對象Order的Insert()方法,如下所示:
protected void wzdCheckOut_FinishButtonClick(object sender, WizardNavigationEventArgs e) {
if (Profile.ShoppingCart.CartItems.Count > 0) {
if (Profile.ShoppingCart.Count > 0) {
// display ordered items
CartListOrdered.Bind(Profile.ShoppingCart.CartItems);
// display total and credit card information
ltlTotalComplete.Text = ltlTotal.Text;
ltlCreditCardComplete.Text = ltlCreditCard.Text;
// create order
OrderInfo order = new OrderInfo(int.MinValue, DateTime.Now, User.Identity.Name, GetCreditCardInfo(), billingForm.Address, shippingForm.Address, Profile.ShoppingCart.Total, Profile.ShoppingCart.GetOrderLineItems(), null);
// insert
Order newOrder = new Order();
newOrder.Insert(order);
// destroy cart
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的Inser()方法將OrderInfo對象插入到數據表中。此外,它還對領域對象ShoppingCart的數據項作出判斷,如果其值等於0,就在頁面中顯示UI提示信息。此時,View的內容決定了Model的值,而Model值反過來又決定了View的顯示內容。
6.3 ASP.NET控件
ASP.NET控件是View對象最重要的組成部分,它充分利用了面向對象的設計思想,通過封裝與繼承構建一個個控件對象,使得用戶在開發Web頁面時,能夠重用這些控件,甚至自定義自己的控件。在第8章中,我已經介紹了.NET Framework中控件的設計思想,通過引入一種“複合方式”的Composite模式實現了控件樹。在ASP.NET控件中,System.Web.UI.Control就是這棵控件樹的根,它定義了所有ASP.NET控件共有的屬性、方法和事件,並負責管理和控制控件的整個執行生命週期。
Control基類並沒有包含UI的特定功能,如果需要提供與UI相關的方法屬性,就需要從System.Web.UI.WebControls.WebControl類派生。該類實際上也是Control類的子類,但它附加了諸如ForeColor、BackColor、Font等屬性。
除此之外,還有一個重要的類是System.Web.UI.UserControl,即用戶控件類,它同樣是Control類的子類。我們可以自定義一些用戶控件派生自UserControl,在Visual Studio的Design環境下,我們可以通過拖動控件的方式將多種類型的控件組合成一個自定義用戶控件,也可以在codebehind方式下,爲自定義用戶控件類添加新的屬性和方法。
整個ASP.NET控件類的層次結構如圖6-2所示:
圖6-2 ASP.NET控件類的層次結構
ASP.NET控件的執行生命週期如表6-1所示:
階段 |
控件需要執行的操作 |
要重寫的方法或事件 |
初始化 |
初始化在傳入 Web 請求生命週期內所需的設置。 |
Init 事件(OnInit 方法) |
加載視圖狀態 |
在此階段結束時,就會自動填充控件的 ViewState 屬性,控件可以重寫 LoadViewState 方法的默認實現,以自定義狀態還原。 |
LoadViewState 方法 |
處理回發數據 |
處理傳入窗體數據,並相應地更新屬性。 注意:只有處理回發數據的控件參與此階段。 |
LoadPostData 方法(如果已實現 IPostBackDataHandler) |
加載 |
執行所有請求共有的操作,如設置數據庫查詢。此時,樹中的服務器控件已創建並初始化、狀態已還原並且窗體控件反映了客戶端的數據。 |
Load 事件(OnLoad 方法) |
發送回發更改通知 |
引發更改事件以響應當前和以前回發之間的狀態更改。 注意:只有引發回發更改事件的控件參與此階段。 |
RaisePostDataChangedEvent 方法(如果已實現 IPostBackDataHandler) |
處理回發事件 |
處理引起回發的客戶端事件,並在服務器上引發相應的事件。 注意:只有處理回發事件的控件參與此階段。 |
RaisePostBackEvent 方法(如果已實現 IPostBackEventHandler) |
預呈現 |
在呈現輸出之前執行任何更新。可以保存在預呈現階段對控件狀態所做的更改,而在呈現階段所對的更改則會丟失。 |
PreRender 事件(OnPreRender 方法) |
保存狀態 |
在此階段後,自動將控件的 ViewState 屬性保持到字符串對象中。此字符串對象被髮送到客戶端並作爲隱藏變量發送回來。爲了提高效率,控件可以重寫 SaveViewState 方法以修改 ViewState 屬性。 |
SaveViewState 方法 |
呈現 |
生成呈現給客戶端的輸出。 |
Render 方法 |
處置 |
執行銷燬控件前的所有最終清理操作。在此階段必須釋放對昂貴資源的引用,如數據庫鏈接。 |
Dispose 方法 |
卸載 |
執行銷燬控件前的所有最終清理操作。控件作者通常在 Dispose 中執行清除,而不處理此事件。 |
UnLoad 事件(On UnLoad 方法) |
表6-1 ASP.NET控件的執行生命週期
在這裏,控件設計利用了Template Method模式,Control基類提供了大部分protected虛方法,留待其子類改寫其方法。以PetShop 4.0爲例,就定義了兩個ASP.NET控件,它們都屬於System.Web.UI.WebControls.WebControl的子類。其中,CustomList控件派生自System.Web.UI.WebControls.DataList,CustomGrid控件則派生自System.Web.UI.WebControls.Repeater。
由於這兩個控件都改變了其父類控件的呈現方式,故而,我們可以通過重寫父類的Render虛方法,完成控件的自定義。例如CustomGrid控件:
//Static constants
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) {
//Check there is some data attached
if (ItemCount == 0) {
writer.Write(emptyText);
return;
}
//Mask the query
string query = Context.Request.Url.Query.Replace(COMMA, AMP);
query = RX.Replace(query, string.Empty);
// Write out the first part of the control, the table header
writer.Write(HTML1);
// Call the inherited method
base.Render(writer);
// Write out a table row closure
writer.Write(HTML2);
//Determin whether next and previous buttons are required
//Previous button?
if (currentPageIndex > 0)
writer.Write(string.Format(LINK_PREV, (currentPageIndex - 1) + query));
//Close the table data tag
writer.Write(HTML3);
//Next button?
if (currentPageIndex < PageCount)
writer.Write(string.Format(LINK_MORE, (currentPageIndex + 1) + query));
//Close the table
writer.Write(HTML4);
}
由於CustomGrid繼承自Repeater控件,因而它同時還繼承了Repeater的DataSource屬性,這是一個虛屬性,它默認的set訪問器屬性如下:
{
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屬性有着不同的設置行爲,因而在定義CustomGrid控件的時候,需要改寫DataSource虛屬性,如下所示:
private int itemCount;
override public object DataSource {
set {
//This try catch block is to avoid issues with the VS.NET designer
//The designer will try and bind a datasource which does not derive from ILIST
try {
dataSource = (IList)value;
ItemCount = dataSource.Count;
}
catch {
dataSource = null;
ItemCount = 0;
}
}
}
當設置的value對象值不爲IList類型時,set訪問器就將捕獲異常,然後將dataSource字段設置爲null。
由於我們改寫了DataSource屬性,因而改寫Repeater類的OnDataBinding()方法也就勢在必行。此外,CustomGrid還提供了分頁的功能,我們也需要實現分頁的相關操作。與DataSource屬性不同,Repeater類的OnDataBinding()方法實際上是繼承和改寫了Control基類的OnDataBinding()虛方法,而我們又在此基礎上改寫了Repeater類的OnDataBinding()方法:
//Work out which items we want to render to the page
int start = CurrentPageIndex * pageSize;
int size = Math.Min(pageSize, ItemCount - start);
IList page = new ArrayList();
//Add the relevant items from the datasource
for (int i = 0; i < size; i++)
page.Add(dataSource[start + i]);
//set the base objects datasource
base.DataSource = page;
base.OnDataBinding(e);
}
此外,CustomGrid控件類還增加了許多屬於自己的屬性和方法,例如PageSize、PageCount屬性以及SetPage()方法等。正是因爲ASP.NET控件引入了Composite模式與Template Method模式,當我們在自定義控件時,就可以通過繼承與改寫的方式來完成控件的設計。自定義ASP.NET控件一方面可以根據系統的需求實現特定的功能,也能夠最大限度地實現對象的重用,既可以減少編碼量,同時也有利於未來對程序的擴展與修改。
在PetShop 4.0中,除了自定義了上述WebControl控件的子控件外,最主要的還是利用了用戶控件。在Controls文件夾下,一共定義了11個用戶控件,內容涵蓋客戶地址信息、信用卡信息、購物車信息、期望列表(Wish List)信息以及導航信息、搜索結果信息等。它們相當於是一些組合控件,除了包含了子控件的方法和屬性外,也定義了一些必要的UI實現邏輯。以ShoppingCartControl用戶控件爲例,它會在該控件被呈現(Render)之前,做一些數據準備工作,獲取購物車數據,並作爲數據源綁定到其下的Repeater控件:
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頁面下,我們可以加入該用戶控件,如下所示:
由於ShoppingCartControl用戶控件已經實現了用於呈現購物車數據的邏輯,那麼在ShoppingCart.aspx.cs中,就可以不用負責這些邏輯,在充分完成對象重用的過程中,同時又達到了職責分離的目的。用戶控件的設計者與頁面設計者可以互不干擾,分頭完成自己的設計。特別是對於頁面設計者而言,他可以是單一的UI設計人員角色,僅需要關注用戶界面是否美觀與友好,對於表示層中對領域對象的調用與操作就可以不必理會,整個頁面的代碼也顯得結構清晰、邏輯清楚,無疑也“乾淨”了不少。