| 摘要 本文介紹了在.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檔案中便於修改 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模式下的WebForm Page 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設計模式所需的一些基本知識,希望能起到一點拋磚引玉的作用。 |