摘要
本文介紹了在.Net框架下應用Web設計模式改進WebForm程序設計的一些基本方法及要點。
關鍵字
設計模式,ASP.Net,WebForm,MVC,Page Controller,Front Controller,Page Cache
目錄
引言
經典的WebForm架構
設計模式
MVC模式下的WebForm
Page Controller模式下的WebForm
Front Controller模式下的WebForm
Page Cache模式下的WebForm
引言
記得微軟剛剛推出ASP.NET時,給人的震撼是開發Web程序不再是編寫傳統的網頁,而像是在構造應用程序,因而微軟稱之為WebForm。但是兩年後的今天,有相當多的開發人員仍然延用寫腳本程序的思路構建一個又一個的WebForm,而沒有發揮出ASP.Net的優勢,就此本文希望通過實例能夠啟發讀者一些新的思路。
由於篇幅有限,本文不可能通過一個復雜的Web應用來向讀者展示結合設計模式的WebForm,但是如果僅僅是一個小程序的確沒有使用模式的必要。為了便於理解,希望您能把它想象成是一個大型系統中的小模塊(如果代碼是大型系統的一部分那麼使用模式就變得非常重要)。
在本文的末尾給出了所有源程序的下載地址。
經典的WebForm架構
首先來看一個簡單的應用,數據庫設計如下圖,Portal是Subject的父表,通過portalId進行一對多關聯,程序需要根據portalId顯示不同的Subject列表。
按照我們編寫WebForm一般的習慣,首先在頁面上拖放一個DropDownList、一個DataGrid、一個Button控件:
界面(webForm.ASPx):
〈form id="webForm" method="post" runat="server">
〈asp:DropDownList id="dropDownList" runat="server">〈/ASP:DropDownList>
〈asp:Button id="button" runat="server" Text="Button">〈/ASP:Button>
〈asp:DataGrid id="dataGrid" runat="server">〈/ASP:DataGrid>
〈/form>
然後利用VS.Net代碼隱藏功能編寫的核心代碼如下:
後置代碼(webForm.ASPx.cs):
//頁面初始化事件
private void Page_Load(object sender, System.EventArgs e)
{
if ( ! IsPostBack )
{
string SQL_SELECT_PORTAL = "SELECT * FROM PORTAL";
//使用using確保釋放數據庫連接
//連接字符串存放在Web.Config文件中便於修改r />using( SqlConnection conn = new SqlConnection( ConfigurationSettings.APPSettings["ConnectionString"] ) )
{
SqlDataAdapter dataAdapter = new SqlDataAdapter( SQL_SELECT_PORTAL, conn );
DataSet dataSet = new DataSet();
dataAdapter.Fill( dataSet );
//設置下拉列表的數據源與文本域、值域
dropDownList.DataSource = dataSet;
dropDownList.DataTextFIEld = "portalName";
dropDownList.DataValueFIEld = "portalId";
dropDownList.DataBind();
}
}
}
//Button的Click事件
private void button_Click(object sender, System.EventArgs e)
{
string SQL_SELECT_SUBJECT = "SELECT * FROM SUBJECT WHERE portalId = {0}";
using( SqlConnection conn = new SqlConnection( ConfigurationSettings.APPSettings["ConnectionString"] ) )
{
//用下拉列表選擇的值替換掉SQL語句中的待定字符{0}
SqlDataAdapter dataAdapter = new SqlDataAdapter( string.Format( SQL_SELECT_SUBJECT, dropDownList.SelectedValue ), conn );
DataSet dataSet = new DataSet();
dataAdapter.Fill( dataSet );
dataGrid.DataSource = dataSet;
dataGrid.DataBind();
}
}
執行結果如圖所示,程序將根據下拉列表框選擇的值綁定DataGrid,非常典型的一個WebForm架構,體現出ASP.Net事件驅動的思想,實現了界面與代碼的分離。但是仔細看看可以從中發現幾個問題:
對數據庫操作的代碼重復,重復代碼是軟件開發中絕對的“壞味道”,往往由於某些原因當你修改了一處代碼,卻忘記要更改另外一處相同的代碼,從而給程序留下了Bug的隱患。
後置代碼完全依賴於界面,在WebForm下界面的變化遠遠大於數據存儲結構和訪問的變化,當界面改變時您將不得不修改代碼以適應新的頁面,有可能將會重寫整個後置代碼。
後置代碼不僅處理用戶的輸入而且還負責了數據的處理,如果需求發生變更,比如需要改變數據的處理方式,那麼你將幾乎重寫整個後置代碼。
一個優秀的設計需要每一個模塊,每一種方法只專注於做一件事,這樣的結構才清晰,易修改,畢竟項目的需求總是在不斷變更的,“唯一不變的就是變化本身”,好的程序一定要為變化作出准備,避免“牽一發而動全身”,所以一定要想辦法解決上述問題,下面讓我們來看看設計模式。
設計模式
設計模式描述了一個不斷重復出現的問題以及對該問題的核心解決方案,它是成功的構架、設計及實施方案,是經驗的總結。設計模式的概念最早來自於西方建築學,但最成功的案例首推中國古代的“三十六計”。
MVC模式下的WebForm
MVC模式是一個用於將用戶界面邏輯與業務邏輯分離開來的基礎設計模式,它將數據處理、界面以及用戶的行為控制分為:Model-VIEw-Controller。
Model:負責當前應用的數據獲取與變更及相關的業務邏輯
VIEw:負責顯示信息
Controller:負責收集轉化用戶的輸入
View和Controller都依賴於Model,但是Model既不依賴於View,也不依賴於Controller,這是分離的主要優點之一,這樣Model可以單獨的建立和測試以便於代碼復用,VIEw和Controller只需要Model提供數據,它們不會知道、也不會關心數據是存儲在SQL Server還是Oracle數據庫中或者別的什麼地方。
根據MVC模式的思想,可以將上面例子的後置代碼拆分為Model和Controller,用專門的一個類來處理數據,後置代碼作為Controller僅僅負責轉化用戶的輸入,修改後的代碼為:
Model(SQLHelper.cs):封裝所有對數據庫的操作。
private static string SQL_SELECT_PORTAL = "SELECT * FROM PORTAL";
private static string SQL_SELECT_SUBJECT = "SELECT * FROM SUBJECT WHERE portalId = {0}";
private static string SQL_CONNECTION_STRING = ConfigurationSettings.APPSettings["ConnectionString"];
public static DataSet GetPortal()
{
return GetDataSet( SQL_SELECT_PORTAL );
}
public static DataSet GetSubject( string portalId )
{
return GetDataSet( string.Format( SQL_SELECT_SUBJECT, portalId ) );
}
public static DataSet GetDataSet( string sql )
{
using( SqlConnection conn = new SqlConnection( SQL_CONNECTION_STRING ) )
{
SqlDataAdapter dataAdapter = new SqlDataAdapter( sql, conn );
DataSet dataSet = new DataSet();
dataAdapter.Fill( dataSet );
return dataSet;
}
}
Controller(webForm.ASPx.cs):負責轉化用戶的輸入
private void Page_Load(object sender,System.EventArgs e)
{
if ( ! IsPostBack )
{
//調用Model的方法獲得數據源
dropDownList.DataSource = SQLHelper.GetPortal();
dropDownList.DataTextFIEld = "portalName";
dropDownList.DataValueFIEld = "portalId";
dropDownList.DataBind();
}
}
private void button_Click(object sender, System.EventArgs e)
{
dataGrid.DataSource = SQLHelper.GetSubject( dropDownList.SelectedValue );
dataGrid.DataBind();
}
修改後的代碼非常清晰,M-V-C各司其制,對任意模塊的改寫都不會引起其他模塊的變更,類似於MFC中Doc/VIEw結構。但是如果相同結構的程序很多,而我們又需要做一些統一的控制,如用戶身份的判斷,統一的界面風格等;或者您還希望Controller與Model分離的更徹底,在Controller中不涉及到Model層的代碼。此時僅僅靠MVC模式就顯得有點力不從心,那麼就請看看下面的Page Controller模式。
Page Controller模式下的WebForm
MVC 模式主要關注Model與View之間的分離,而對於Controller的關注較少(在上面的MVC模式中我們僅僅只把Model和Controller分離開,並未對Controller進行更多的處理),但在基於WebForm的應用程序中,VIEw和Controller本來就是分隔的(顯示是在客戶端浏覽器中進行),而Controller是服務器端應用程序;同時不同用戶操作可能會導致不同的Controller策略,應用程序必須根據上一頁面以及用戶觸發的事件來執行不同的操作;還有大多數WebForm都需要統一的界面風格,如果不對此處理將可能產生重復代碼,因此有必要對Controller進行更為仔細的劃分。
Page Controller模式在MVC模式的基礎上使用一個公共的頁基類來統一處理諸如Http請求,界面風格等,如圖:
傳統的WebForm一般繼承自System.Web.UI.Page類,而Page Controller的實現思想是所有的WebForm繼承自定義頁面基類,如圖:
利用自定義頁面基類,我們可以統一的接收頁面請求、提取所有相關數據、調用對Model的所有更新以及向VIEw轉發請求,輕松實現統一的頁面風格,而由它所派生的Controller的邏輯將變得更簡單,更具體。
下面看一下Page Controller的具體實現:
Page Controller(BasePage.cs):
public class BasePage : System.Web.UI.Page
{
private string _title;
public string Title//頁面標題,由子類負責指定
{
get
{
return _title;
}
set
{
_title = value;
}
}
public DataSet GetPortalDataSource()
{
return SQLHelper.GetPortal();
}
public DataSet GetSubjectDataSource( string portalId )
{
return SQLHelper.GetSubject( portalId );
}
protected override void Render( HtmlTextWriter writer )
{
writer.Write( "〈Html>〈head>〈title>" + Title + "〈/title>〈/head>〈body>" );//統一的頁面頭
base.Render( writer );//子頁面的輸出
writer.Write( @"〈a href=""http://www.asp.net"">ASP.Net〈/a>〈/body>〈/Html>" );//統一的頁面尾
}
}
現在它封裝了Model的功能,實現了統一的頁面標題和頁尾,子類只須直接調用:
修改後的Controller(webForm.ASPx.cs):
public class webForm : BasePage//繼承頁面基類
{
private void Page_Load(object sender, System.EventArgs e)
{
Title = "Hello, World!";//指定頁面標題
if ( ! IsPostBack )
{
dropDownList.DataSource = GetPortalDataSource();//調用基類的方法
dropDownList.DataTextFIEld = "portalName";
dropDownList.DataValueFIEld = "portalId";
dropDownList.DataBind();
}
}
private void button_Click(object sender, System.EventArgs e)
{
dataGrid.DataSource = GetSubjectDataSource( dropDownList.SelectedValue );
dataGrid.DataBind();
}
}
從上可以看出BagePage Controller接管了大部分原來Controller的工作,使Controller變得更簡單,更容易修改(為了便於講解我沒有把控件放在BasePage中,但是您完全可以那樣做),但是隨著應用復雜度的上升,用戶需求的變化,我們很容易會將不同的頁面類型分組成不同的基類,造成過深的繼承樹;又例如對於一個購物車程序,需要預定義好頁面路徑;對於向導程序來說路徑是動態的(事先並不知道用戶的選擇)。
面對以上這些應用來說僅僅使用Page Controller還是不夠的,接下來再看看Front Controller模式。
Front Controller模式下的WebFormPage Controller的實現需要在基類中為頁面的公共部分創建代碼,但是隨著時間的推移,需求會發生較大的改變,有時不得不增加非公用的代碼,這樣基類就會不斷增大,您可能會創建更深的繼承層次結構以刪除條件邏輯,這樣一來我們很難對它進行重構,因此需要更進一步對Page Controller進行研究。
Front Controller通過對所有請求的控制並傳輸解決了在Page Controller中存在的分散化處理的問題,它分為Handler和Command樹兩個部分,Handler處理所有公共的邏輯,接收HTTP Post或Get請求以及相關的參數並根據輸入的參數選擇正確的命令對象,然後將控制權傳遞到Command對象,由其完成後面的操作,在這裡我們將使用到Command模式。
Command模式通過將請求本身變成一個對象可向未指定的應用對象提出請求,這個對象可被存儲並像其他的對象一樣被傳遞,此模式的關鍵是一個抽象的Command類,它定義了一個執行操作的接口,最簡單的形式是一個抽象的Execute操作,具體的Command子類將接收者作為其一個實例變量,並實現Execute操作,指定接收者采取的動作,而接收者具有執行該請求所需的具體信息。
因為Front Controller模式要比上面兩個模式復雜一些,我們再來看看例子的類圖:
關於Handler的原理請查閱MSDN,在這就不多講了,我們來看看Front Controller模式的具體實現:
首先在Web.Config裡定義:
〈!-- 指定對Dummy開頭的ASPx文件交由Handler處理 -->
〈httpHandlers>
〈add verb="*" path="/WebPatterns/FrontController/Dummy*.ASPx" type="WebPatterns.FrontController.Handler,WebPatterns"/>
〈/httpHandlers>
〈!-- 指定名為FrontControllerMap的頁面映射塊,交由UrlMap類處理,程序將根據key找到對應的url作為最終的執行路徑,您在這可以定義多個key與url的鍵值對 -->
〈configSections>
〈section name="FrontControllerMap" type="WebPatterns.FrontController.UrlMap, WebPatterns">〈/section>
〈/configSections>
〈FrontControllerMap>
〈entrIEs>
〈entry key="/WebPatterns/FrontController/DummyWebForm.aspx" url="/WebPatterns/FrontController/ActWebForm.ASPx" />
。。。
〈/entrIEs>
〈/FrontControllerMap>
修改webForm.ASPx.cs:
private void button_Click( object sender, System.EventArgs e )
{
Response.Redirect( "DummyWebForm.ASPx?requestParm=" + dropDownList.SelectedValue );
}
當程序執行到這裡時將會根據Web.Config裡的定義觸發類Handler的ProcessRequest事件:
Handler.cs:
public class Handler : IHttpHandler
{
public void ProcessRequest( HttpContext context )
{
Command command = CommandFactory.Make( context.Request.Params );
command.Execute( context );
>}
public bool IsReusable
{
get
{
return true;
}
}
}
而它又會調用類CommandFactory的Make方法來處理接收到的參數並返回一個Command對象,緊接著它又會調用該Command對象的Execute方法把處理後參數提交到具體處理的頁面。
public class CommandFactory
{
public static Command Make( NameValueCollection parms )
{
string requestParm = parms["requestParm"];
Command command = null;
//根據輸入參數得到不同的Command對象
switch ( requestParm )
{
case "1" :
command = new FirstPortal();
break;
case "2" :
command = new SecondPortal();
break;
default :
command = new FirstPortal();
break;
}
return command;
}
}
public interface Command
{
void Execute( HttpContext context );
}
public abstract class RedirectCommand : Command
{
//獲得Web.Config中定義的key和url鍵值對,UrlMap類詳見下載包中的代碼
private UrlMap map = UrlMap.SoleInstance;
protected abstract void OnExecute( HttpContext context );
public void Execute( HttpContext context )
{
OnExecute( context );
//根據key和url鍵值對提交到具體處理的頁面
string url = String.Format( "{0}?{1}", map.Map[ context.Request.Url.AbsolutePath ], context.Request.Url.Query );
context.Server.Transfer( url );
}
}
public class FirstPortal : RedirectCommand
{
protected override void OnExecute( HttpContext context )
{
//在輸入參數中加入項portalId以便頁面處理
context.Items["portalId"] = "1";
}
}
public class SecondPortal : RedirectCommand
{
protected override void OnExecute(HttpContext context)
{
context.Items["portalId"] = "2";
}
}
最後在ActWebForm.ASPx.cs中:
dataGrid.DataSource = GetSubjectDataSource( HttpContext.Current.Items["portalId"].ToString() );
dataGrid.DataBind();
上面的例子展示了如何通過Front Controller集中和處理所有的請求,它使用CommandFactory來確定要執行的具體操作,無論執行什麼方法和對象,Handler只調用Command對象的Execute方法,您可以在不修改 Handler的情況下添加額外的命令。它允許讓用戶看不到實際的頁面,當用戶輸入一個URL時,然後系統將根據web.config文件將它映射到特定的URL,這可以讓程序員有更大的靈活性,還可以獲得Page Controller實現中所沒有的一個間接操作層。
對於相當復雜的Web應用我們才會采用Front Controller模式,它通常需要將頁面內置的Controller替換為自定義的Handler,在Front Controllrer模式下我們甚至可以不需要頁面,不過由於它本身實現比較復雜,可能會給業務邏輯的實現帶來一些困擾。
以上兩個Controller模式都是處理比較復雜的WebForm應用,相對於直接處理用戶輸入的應用來講復雜度大大提高,性能也必然有所降低,為此我們最後來看一個可以大幅度提高程序性能的模式:Page Cache模式。
Page Cache模式下的WebForm幾乎所有的WebForm面臨的都是訪問很頻繁,改動卻很少的應用,對WebForm的訪問者來說有相當多的內容是重復的,因此我們可以試著把WebForm或者某些相同的內容保存在服務器內存中一段時間以加快程序的響應速度。
這個模式實現起來很簡單,只需在頁面上加入:
〈%@ OutputCache Duration="60" VaryByParam="none" %>,
這表示該頁面會在60秒以後過期,也就是說在這60秒以內所有的來訪者看到該頁面的內容都是一樣的,但是響應速度大大提高,就象靜態的Html頁面一樣。
也許您只是想保存部分的內容而不是想保存整個頁面,那麼我們回到MVC模式中的SQLHelper.cs,我對它進行了少許修改:
public static DataSet GetPortal()
{
DataSet dataSet;
if ( HttpContext.Current.Cache["SELECT_PORTAL_CACHE"] != null )
{
//如果數據存在於緩存中則直接取出
dataSet = ( DataSet ) HttpContext.Current.Cache["SELECT_PORTAL_CACHE"];
}
else
{
//否則從數據庫中取出並插入到緩存中,設定絕對過期時間為3分鐘
dataSet = GetDataSet( SQL_SELECT_PORTAL );
HttpContext.Current.Cache.Insert( "SELECT_PORTAL_CACHE", dataSet, null, DateTime.Now.AddMinutes( 3 ), TimeSpan.Zero );
}
return dataSet;
}
在這裡把SELECT_PORTAL_CACHE作為Cache的鍵,把GetDataSet( SQL_SELECT_PORTAL )取出的內容作為Cache的值。這樣除了程序第1次調用時會進行數據庫操作外,在Cache過期時間內都不會進行數據庫操作,同樣大大提高了程序的響應能力。
小結
自從.NET框架引入設計模式以後在很大程度上提高了其在企業級應用方面的實力,可以毫不誇張的說在企業級應用方面.NET已經趕上了Java的步伐並大有後來居上之勢,本文通過一個實例的講解向讀者展示了在.Net框架下實現Web設計模式所需的一些基本知識,希望能起到一點拋磚引玉的作用。