Web设计模式
摘要
本文介紹了在.NET框架下應(yīng)用Web設(shè)計模式改進(jìn)WebForm程序設(shè)計的一些基本方法及要點(diǎn)。
關(guān)鍵字
設(shè)計模式,ASP.NET,WebForm,MVC,Page Controller,Front Controller,Page Cache
目錄
引言
經(jīng)典的WebForm架構(gòu)
設(shè)計模式
MVC模式下的WebForm
Page Controller模式下的WebForm
Front Controller模式下的WebForm
Page Cache模式下的WebForm
引言
記得微軟剛剛推出ASP.NET時,給人的震撼是開發(fā)Web程序不再是編寫傳統(tǒng)的網(wǎng)頁,而像是在構(gòu)造應(yīng)用程序,因而微軟稱之為WebForm。但是兩年后的今天,有相當(dāng)多的開發(fā)人員仍然延用寫腳本程序的思路構(gòu)建一個又一個的WebForm,而沒有發(fā)揮出ASP.NET的優(yōu)勢,就此本文希望通過實例能夠啟發(fā)讀者一些新的思路。
由于篇幅有限,本文不可能通過一個復(fù)雜的Web應(yīng)用來向讀者展示結(jié)合設(shè)計模式的WebForm,但是如果僅僅是一個小程序的確沒有使用模式的必要。為了便于理解,希望您能把它想象成是一個大型系統(tǒng)中的小模塊(如果代碼是大型系統(tǒng)的一部分那么使用模式就變得非常重要)。
在本文的末尾給出了所有源程序的下載地址。
經(jīng)典的WebForm架構(gòu)
首先來看一個簡單的應(yīng)用,數(shù)據(jù)庫設(shè)計如下圖,Portal是Subject的父表,通過portalId進(jìn)行一對多關(guān)聯(lián),程序需要根據(jù)portalId顯示不同的Subject列表。
按照我們編寫WebForm一般的習(xí)慣,首先在頁面上拖放一個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確保釋放數(shù)據(jù)庫連接
//連接字符串存放在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 );
//設(shè)置下拉列表的數(shù)據(jù)源與文本域、值域
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();
}
}
執(zhí)行結(jié)果如圖所示,程序?qū)⒏鶕?jù)下拉列表框選擇的值綁定DataGrid,非常典型的一個WebForm架構(gòu),體現(xiàn)出ASP.NET事件驅(qū)動的思想,實現(xiàn)了界面與代碼的分離。但是仔細(xì)看看可以從中發(fā)現(xiàn)幾個問題:
對數(shù)據(jù)庫操作的代碼重復(fù),重復(fù)代碼是軟件開發(fā)中絕對的“壞味道”,往往由于某些原因當(dāng)你修改了一處代碼,卻忘記要更改另外一處相同的代碼,從而給程序留下了Bug的隱患。
后置代碼完全依賴于界面,在WebForm下界面的變化遠(yuǎn)遠(yuǎn)大于數(shù)據(jù)存儲結(jié)構(gòu)和訪問的變化,當(dāng)界面改變時您將不得不修改代碼以適應(yīng)新的頁面,有可能將會重寫整個后置代碼。
后置代碼不僅處理用戶的輸入而且還負(fù)責(zé)了數(shù)據(jù)的處理,如果需求發(fā)生變更,比如需要改變數(shù)據(jù)的處理方式,那么你將幾乎重寫整個后置代碼。
一個優(yōu)秀的設(shè)計需要每一個模塊,每一種方法只專注于做一件事,這樣的結(jié)構(gòu)才清晰,易修改,畢竟項目的需求總是在不斷變更的,“唯一不變的就是變化本身”,好的程序一定要為變化作出準(zhǔn)備,避免“牽一發(fā)而動全身”,所以一定要想辦法解決上述問題,下面讓我們來看看設(shè)計模式。
設(shè)計模式
設(shè)計模式描述了一個不斷重復(fù)出現(xiàn)的問題以及對該問題的核心解決方案,它是成功的構(gòu)架、設(shè)計及實施方案,是經(jīng)驗的總結(jié)。設(shè)計模式的概念最早來自于西方建筑學(xué),但最成功的案例首推中國古代的“三十六計”。
MVC模式下的WebForm
MVC模式是一個用于將用戶界面邏輯與業(yè)務(wù)邏輯分離開來的基礎(chǔ)設(shè)計模式,它將數(shù)據(jù)處理、界面以及用戶的行為控制分為:Model-View-Controller。
Model:負(fù)責(zé)當(dāng)前應(yīng)用的數(shù)據(jù)獲取與變更及相關(guān)的業(yè)務(wù)邏輯
View:負(fù)責(zé)顯示信息
Controller:負(fù)責(zé)收集轉(zhuǎn)化用戶的輸入
View和Controller都依賴于Model,但是Model既不依賴于View,也不依賴于Controller,這是分離的主要優(yōu)點(diǎn)之一,這樣Model可以單獨(dú)的建立和測試以便于代碼復(fù)用,View和Controller只需要Model提供數(shù)據(jù),它們不會知道、也不會關(guān)心數(shù)據(jù)是存儲在SQL Server還是Oracle數(shù)據(jù)庫中或者別的什么地方。
根據(jù)MVC模式的思想,可以將上面例子的后置代碼拆分為Model和Controller,用專門的一個類來處理數(shù)據(jù),后置代碼作為Controller僅僅負(fù)責(zé)轉(zhuǎn)化用戶的輸入,修改后的代碼為:
Model(SQLHelper.cs):封裝所有對數(shù)據(jù)庫的操作。
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):負(fù)責(zé)轉(zhuǎn)化用戶的輸入
private void Page_Load(object sender, System.EventArgs e)
{
if ( ! IsPostBack )
{
//調(diào)用Model的方法獲得數(shù)據(jù)源
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結(jié)構(gòu)。但是如果相同結(jié)構(gòu)的程序很多,而我們又需要做一些統(tǒng)一的控制,如用戶身份的判斷,統(tǒng)一的界面風(fēng)格等;或者您還希望Controller與Model分離的更徹底,在Controller中不涉及到Model層的代碼。此時僅僅靠MVC模式就顯得有點(diǎn)力不從心,那么就請看看下面的Page Controller模式。
Page Controller模式下的WebForm
MVC 模式主要關(guān)注Model與View之間的分離,而對于Controller的關(guān)注較少(在上面的MVC模式中我們僅僅只把Model和Controller分離開,并未對Controller進(jìn)行更多的處理),但在基于WebForm的應(yīng)用程序中,View和Controller本來就是分隔的(顯示是在客戶端瀏覽器中進(jìn)行),而Controller是服務(wù)器端應(yīng)用程序;同時不同用戶操作可能會導(dǎo)致不同的Controller策略,應(yīng)用程序必須根據(jù)上一頁面以及用戶觸發(fā)的事件來執(zhí)行不同的操作;還有大多數(shù)WebForm都需要統(tǒng)一的界面風(fēng)格,如果不對此處理將可能產(chǎn)生重復(fù)代碼,因此有必要對Controller進(jìn)行更為仔細(xì)的劃分。
Page Controller模式在MVC模式的基礎(chǔ)上使用一個公共的頁基類來統(tǒng)一處理諸如Http請求,界面風(fēng)格等,如圖:
傳統(tǒng)的WebForm一般繼承自System.Web.UI.Page類,而Page Controller的實現(xiàn)思想是所有的WebForm繼承自定義頁面基類,如圖:
利用自定義頁面基類,我們可以統(tǒng)一的接收頁面請求、提取所有相關(guān)數(shù)據(jù)、調(diào)用對Model的所有更新以及向View轉(zhuǎn)發(fā)請求,輕松實現(xiàn)統(tǒng)一的頁面風(fēng)格,而由它所派生的Controller的邏輯將變得更簡單,更具體。
下面看一下Page Controller的具體實現(xiàn):
Page Controller(BasePage.cs):
public class BasePage : System.Web.UI.Page
{
private string _title;
public string Title//頁面標(biāo)題,由子類負(fù)責(zé)指定
{
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>" );//統(tǒng)一的頁面頭
base.Render( writer );//子頁面的輸出
writer.Write( @"〈a href=""http://www.asp.net"">ASP.NET〈/a>〈/body>〈/html>" );//統(tǒng)一的頁面尾
}
}
現(xiàn)在它封裝了Model的功能,實現(xiàn)了統(tǒng)一的頁面標(biāo)題和頁尾,子類只須直接調(diào)用:
修改后的Controller(webForm.aspx.cs):
public class webForm : BasePage//繼承頁面基類
{
private void Page_Load(object sender, System.EventArgs e)
{
Title = "Hello, World!";//指定頁面標(biāo)題
if ( ! IsPostBack )
{
dropDownList.DataSource = GetPortalDataSource();//調(diào)用基類的方法
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中,但是您完全可以那樣做),但是隨著應(yīng)用復(fù)雜度的上升,用戶需求的變化,我們很容易會將不同的頁面類型分組成不同的基類,造成過深的繼承樹;又例如對于一個購物車程序,需要預(yù)定義好頁面路徑;對于向?qū)С绦騺碚f路徑是動態(tài)的(事先并不知道用戶的選擇)。
面對以上這些應(yīng)用來說僅僅使用Page Controller還是不夠的,接下來再看看Front Controller模式。
Front Controller模式下的WebForm
Page Controller的實現(xiàn)需要在基類中為頁面的公共部分創(chuàng)建代碼,但是隨著時間的推移,需求會發(fā)生較大的改變,有時不得不增加非公用的代碼,這樣基類就會不斷增大,您可能會創(chuàng)建更深的繼承層次結(jié)構(gòu)以刪除條件邏輯,這樣一來我們很難對它進(jìn)行重構(gòu),因此需要更進(jìn)一步對Page Controller進(jìn)行研究。
Front Controller通過對所有請求的控制并傳輸解決了在Page Controller中存在的分散化處理的問題,它分為Handler和Command樹兩個部分,Handler處理所有公共的邏輯,接收HTTP Post或Get請求以及相關(guān)的參數(shù)并根據(jù)輸入的參數(shù)選擇正確的命令對象,然后將控制權(quán)傳遞到Command對象,由其完成后面的操作,在這里我們將使用到Command模式。
Command模式通過將請求本身變成一個對象可向未指定的應(yīng)用對象提出請求,這個對象可被存儲并像其他的對象一樣被傳遞,此模式的關(guān)鍵是一個抽象的Command類,它定義了一個執(zhí)行操作的接口,最簡單的形式是一個抽象的Execute操作,具體的Command子類將接收者作為其一個實例變量,并實現(xiàn)Execute操作,指定接收者采取的動作,而接收者具有執(zhí)行該請求所需的具體信息。
因為Front Controller模式要比上面兩個模式復(fù)雜一些,我們再來看看例子的類圖:
關(guān)于Handler的原理請查閱MSDN,在這就不多講了,我們來看看Front Controller模式的具體實現(xiàn):
首先在Web.Config里定義:
〈!-- 指定對Dummy開頭的aspx文件交由Handler處理 -->
〈httpHandlers>
〈add verb="*" path="/WebPatterns/FrontController/Dummy*.aspx" type="WebPatterns.FrontController.Handler,WebPatterns"/>
〈/httpHandlers>
〈!-- 指定名為FrontControllerMap的頁面映射塊,交由UrlMap類處理,程序?qū)⒏鶕?jù)key找到對應(yīng)的url作為最終的執(zhí)行路徑,您在這可以定義多個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 );
}
當(dāng)程序執(zhí)行到這里時將會根據(jù)Web.Config里的定義觸發(fā)類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;
}
}
}
而它又會調(diào)用類CommandFactory的Make方法來處理接收到的參數(shù)并返回一個Command對象,緊接著它又會調(diào)用該Command對象的Execute方法把處理后參數(shù)提交到具體處理的頁面。
public class CommandFactory
{
public static Command Make( NameValueCollection parms )
{
string requestParm = parms["requestParm"];
Command command = null;
//根據(jù)輸入?yún)?shù)得到不同的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 );
//根據(jù)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 )
{
//在輸入?yún)?shù)中加入項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來確定要執(zhí)行的具體操作,無論執(zhí)行什么方法和對象,Handler只調(diào)用Command對象的Execute方法,您可以在不修改 Handler的情況下添加額外的命令。它允許讓用戶看不到實際的頁面,當(dāng)用戶輸入一個URL時,然后系統(tǒng)將根據(jù)web.config文件將它映射到特定的URL,這可以讓程序員有更大的靈活性,還可以獲得Page Controller實現(xiàn)中所沒有的一個間接操作層。
對于相當(dāng)復(fù)雜的Web應(yīng)用我們才會采用Front Controller模式,它通常需要將頁面內(nèi)置的Controller替換為自定義的Handler,在Front Controllrer模式下我們甚至可以不需要頁面,不過由于它本身實現(xiàn)比較復(fù)雜,可能會給業(yè)務(wù)邏輯的實現(xiàn)帶來一些困擾。
以上兩個Controller模式都是處理比較復(fù)雜的WebForm應(yīng)用,相對于直接處理用戶輸入的應(yīng)用來講復(fù)雜度大大提高,性能也必然有所降低,為此我們最后來看一個可以大幅度提高程序性能的模式:Page Cache模式。
Page Cache模式下的WebForm
幾乎所有的WebForm面臨的都是訪問很頻繁,改動卻很少的應(yīng)用,對WebForm的訪問者來說有相當(dāng)多的內(nèi)容是重復(fù)的,因此我們可以試著把WebForm或者某些相同的內(nèi)容保存在服務(wù)器內(nèi)存中一段時間以加快程序的響應(yīng)速度。
這個模式實現(xiàn)起來很簡單,只需在頁面上加入:
〈%@ OutputCache Duration="60" VaryByParam="none" %>,
這表示該頁面會在60秒以后過期,也就是說在這60秒以內(nèi)所有的來訪者看到該頁面的內(nèi)容都是一樣的,但是響應(yīng)速度大大提高,就象靜態(tài)的HTML頁面一樣。
也許您只是想保存部分的內(nèi)容而不是想保存整個頁面,那么我們回到MVC模式中的SQLHelper.cs,我對它進(jìn)行了少許修改:
public static DataSet GetPortal()
{
DataSet dataSet;
if ( HttpContext.Current.Cache["SELECT_PORTAL_CACHE"] != null )
{
//如果數(shù)據(jù)存在于緩存中則直接取出
dataSet = ( DataSet ) HttpContext.Current.Cache["SELECT_PORTAL_CACHE"];
}
else
{
//否則從數(shù)據(jù)庫中取出并插入到緩存中,設(shè)定絕對過期時間為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 )取出的內(nèi)容作為Cache的值。這樣除了程序第1次調(diào)用時會進(jìn)行數(shù)據(jù)庫操作外,在Cache過期時間內(nèi)都不會進(jìn)行數(shù)據(jù)庫操作,同樣大大提高了程序的響應(yīng)能力。?
本文介紹了在.NET框架下應(yīng)用Web設(shè)計模式改進(jìn)WebForm程序設(shè)計的一些基本方法及要點(diǎn)。
關(guān)鍵字
設(shè)計模式,ASP.NET,WebForm,MVC,Page Controller,Front Controller,Page Cache
目錄
引言
經(jīng)典的WebForm架構(gòu)
設(shè)計模式
MVC模式下的WebForm
Page Controller模式下的WebForm
Front Controller模式下的WebForm
Page Cache模式下的WebForm
引言
記得微軟剛剛推出ASP.NET時,給人的震撼是開發(fā)Web程序不再是編寫傳統(tǒng)的網(wǎng)頁,而像是在構(gòu)造應(yīng)用程序,因而微軟稱之為WebForm。但是兩年后的今天,有相當(dāng)多的開發(fā)人員仍然延用寫腳本程序的思路構(gòu)建一個又一個的WebForm,而沒有發(fā)揮出ASP.NET的優(yōu)勢,就此本文希望通過實例能夠啟發(fā)讀者一些新的思路。
由于篇幅有限,本文不可能通過一個復(fù)雜的Web應(yīng)用來向讀者展示結(jié)合設(shè)計模式的WebForm,但是如果僅僅是一個小程序的確沒有使用模式的必要。為了便于理解,希望您能把它想象成是一個大型系統(tǒng)中的小模塊(如果代碼是大型系統(tǒng)的一部分那么使用模式就變得非常重要)。
在本文的末尾給出了所有源程序的下載地址。
經(jīng)典的WebForm架構(gòu)
首先來看一個簡單的應(yīng)用,數(shù)據(jù)庫設(shè)計如下圖,Portal是Subject的父表,通過portalId進(jìn)行一對多關(guān)聯(lián),程序需要根據(jù)portalId顯示不同的Subject列表。
按照我們編寫WebForm一般的習(xí)慣,首先在頁面上拖放一個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確保釋放數(shù)據(jù)庫連接
//連接字符串存放在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 );
//設(shè)置下拉列表的數(shù)據(jù)源與文本域、值域
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();
}
}
執(zhí)行結(jié)果如圖所示,程序?qū)⒏鶕?jù)下拉列表框選擇的值綁定DataGrid,非常典型的一個WebForm架構(gòu),體現(xiàn)出ASP.NET事件驅(qū)動的思想,實現(xiàn)了界面與代碼的分離。但是仔細(xì)看看可以從中發(fā)現(xiàn)幾個問題:
對數(shù)據(jù)庫操作的代碼重復(fù),重復(fù)代碼是軟件開發(fā)中絕對的“壞味道”,往往由于某些原因當(dāng)你修改了一處代碼,卻忘記要更改另外一處相同的代碼,從而給程序留下了Bug的隱患。
后置代碼完全依賴于界面,在WebForm下界面的變化遠(yuǎn)遠(yuǎn)大于數(shù)據(jù)存儲結(jié)構(gòu)和訪問的變化,當(dāng)界面改變時您將不得不修改代碼以適應(yīng)新的頁面,有可能將會重寫整個后置代碼。
后置代碼不僅處理用戶的輸入而且還負(fù)責(zé)了數(shù)據(jù)的處理,如果需求發(fā)生變更,比如需要改變數(shù)據(jù)的處理方式,那么你將幾乎重寫整個后置代碼。
一個優(yōu)秀的設(shè)計需要每一個模塊,每一種方法只專注于做一件事,這樣的結(jié)構(gòu)才清晰,易修改,畢竟項目的需求總是在不斷變更的,“唯一不變的就是變化本身”,好的程序一定要為變化作出準(zhǔn)備,避免“牽一發(fā)而動全身”,所以一定要想辦法解決上述問題,下面讓我們來看看設(shè)計模式。
設(shè)計模式
設(shè)計模式描述了一個不斷重復(fù)出現(xiàn)的問題以及對該問題的核心解決方案,它是成功的構(gòu)架、設(shè)計及實施方案,是經(jīng)驗的總結(jié)。設(shè)計模式的概念最早來自于西方建筑學(xué),但最成功的案例首推中國古代的“三十六計”。
MVC模式下的WebForm
MVC模式是一個用于將用戶界面邏輯與業(yè)務(wù)邏輯分離開來的基礎(chǔ)設(shè)計模式,它將數(shù)據(jù)處理、界面以及用戶的行為控制分為:Model-View-Controller。
Model:負(fù)責(zé)當(dāng)前應(yīng)用的數(shù)據(jù)獲取與變更及相關(guān)的業(yè)務(wù)邏輯
View:負(fù)責(zé)顯示信息
Controller:負(fù)責(zé)收集轉(zhuǎn)化用戶的輸入
View和Controller都依賴于Model,但是Model既不依賴于View,也不依賴于Controller,這是分離的主要優(yōu)點(diǎn)之一,這樣Model可以單獨(dú)的建立和測試以便于代碼復(fù)用,View和Controller只需要Model提供數(shù)據(jù),它們不會知道、也不會關(guān)心數(shù)據(jù)是存儲在SQL Server還是Oracle數(shù)據(jù)庫中或者別的什么地方。
根據(jù)MVC模式的思想,可以將上面例子的后置代碼拆分為Model和Controller,用專門的一個類來處理數(shù)據(jù),后置代碼作為Controller僅僅負(fù)責(zé)轉(zhuǎn)化用戶的輸入,修改后的代碼為:
Model(SQLHelper.cs):封裝所有對數(shù)據(jù)庫的操作。
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):負(fù)責(zé)轉(zhuǎn)化用戶的輸入
private void Page_Load(object sender, System.EventArgs e)
{
if ( ! IsPostBack )
{
//調(diào)用Model的方法獲得數(shù)據(jù)源
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結(jié)構(gòu)。但是如果相同結(jié)構(gòu)的程序很多,而我們又需要做一些統(tǒng)一的控制,如用戶身份的判斷,統(tǒng)一的界面風(fēng)格等;或者您還希望Controller與Model分離的更徹底,在Controller中不涉及到Model層的代碼。此時僅僅靠MVC模式就顯得有點(diǎn)力不從心,那么就請看看下面的Page Controller模式。
Page Controller模式下的WebForm
MVC 模式主要關(guān)注Model與View之間的分離,而對于Controller的關(guān)注較少(在上面的MVC模式中我們僅僅只把Model和Controller分離開,并未對Controller進(jìn)行更多的處理),但在基于WebForm的應(yīng)用程序中,View和Controller本來就是分隔的(顯示是在客戶端瀏覽器中進(jìn)行),而Controller是服務(wù)器端應(yīng)用程序;同時不同用戶操作可能會導(dǎo)致不同的Controller策略,應(yīng)用程序必須根據(jù)上一頁面以及用戶觸發(fā)的事件來執(zhí)行不同的操作;還有大多數(shù)WebForm都需要統(tǒng)一的界面風(fēng)格,如果不對此處理將可能產(chǎn)生重復(fù)代碼,因此有必要對Controller進(jìn)行更為仔細(xì)的劃分。
Page Controller模式在MVC模式的基礎(chǔ)上使用一個公共的頁基類來統(tǒng)一處理諸如Http請求,界面風(fēng)格等,如圖:
傳統(tǒng)的WebForm一般繼承自System.Web.UI.Page類,而Page Controller的實現(xiàn)思想是所有的WebForm繼承自定義頁面基類,如圖:
利用自定義頁面基類,我們可以統(tǒng)一的接收頁面請求、提取所有相關(guān)數(shù)據(jù)、調(diào)用對Model的所有更新以及向View轉(zhuǎn)發(fā)請求,輕松實現(xiàn)統(tǒng)一的頁面風(fēng)格,而由它所派生的Controller的邏輯將變得更簡單,更具體。
下面看一下Page Controller的具體實現(xiàn):
Page Controller(BasePage.cs):
public class BasePage : System.Web.UI.Page
{
private string _title;
public string Title//頁面標(biāo)題,由子類負(fù)責(zé)指定
{
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>" );//統(tǒng)一的頁面頭
base.Render( writer );//子頁面的輸出
writer.Write( @"〈a href=""http://www.asp.net"">ASP.NET〈/a>〈/body>〈/html>" );//統(tǒng)一的頁面尾
}
}
現(xiàn)在它封裝了Model的功能,實現(xiàn)了統(tǒng)一的頁面標(biāo)題和頁尾,子類只須直接調(diào)用:
修改后的Controller(webForm.aspx.cs):
public class webForm : BasePage//繼承頁面基類
{
private void Page_Load(object sender, System.EventArgs e)
{
Title = "Hello, World!";//指定頁面標(biāo)題
if ( ! IsPostBack )
{
dropDownList.DataSource = GetPortalDataSource();//調(diào)用基類的方法
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中,但是您完全可以那樣做),但是隨著應(yīng)用復(fù)雜度的上升,用戶需求的變化,我們很容易會將不同的頁面類型分組成不同的基類,造成過深的繼承樹;又例如對于一個購物車程序,需要預(yù)定義好頁面路徑;對于向?qū)С绦騺碚f路徑是動態(tài)的(事先并不知道用戶的選擇)。
面對以上這些應(yīng)用來說僅僅使用Page Controller還是不夠的,接下來再看看Front Controller模式。
Front Controller模式下的WebForm
Page Controller的實現(xiàn)需要在基類中為頁面的公共部分創(chuàng)建代碼,但是隨著時間的推移,需求會發(fā)生較大的改變,有時不得不增加非公用的代碼,這樣基類就會不斷增大,您可能會創(chuàng)建更深的繼承層次結(jié)構(gòu)以刪除條件邏輯,這樣一來我們很難對它進(jìn)行重構(gòu),因此需要更進(jìn)一步對Page Controller進(jìn)行研究。
Front Controller通過對所有請求的控制并傳輸解決了在Page Controller中存在的分散化處理的問題,它分為Handler和Command樹兩個部分,Handler處理所有公共的邏輯,接收HTTP Post或Get請求以及相關(guān)的參數(shù)并根據(jù)輸入的參數(shù)選擇正確的命令對象,然后將控制權(quán)傳遞到Command對象,由其完成后面的操作,在這里我們將使用到Command模式。
Command模式通過將請求本身變成一個對象可向未指定的應(yīng)用對象提出請求,這個對象可被存儲并像其他的對象一樣被傳遞,此模式的關(guān)鍵是一個抽象的Command類,它定義了一個執(zhí)行操作的接口,最簡單的形式是一個抽象的Execute操作,具體的Command子類將接收者作為其一個實例變量,并實現(xiàn)Execute操作,指定接收者采取的動作,而接收者具有執(zhí)行該請求所需的具體信息。
因為Front Controller模式要比上面兩個模式復(fù)雜一些,我們再來看看例子的類圖:
關(guān)于Handler的原理請查閱MSDN,在這就不多講了,我們來看看Front Controller模式的具體實現(xiàn):
首先在Web.Config里定義:
〈!-- 指定對Dummy開頭的aspx文件交由Handler處理 -->
〈httpHandlers>
〈add verb="*" path="/WebPatterns/FrontController/Dummy*.aspx" type="WebPatterns.FrontController.Handler,WebPatterns"/>
〈/httpHandlers>
〈!-- 指定名為FrontControllerMap的頁面映射塊,交由UrlMap類處理,程序?qū)⒏鶕?jù)key找到對應(yīng)的url作為最終的執(zhí)行路徑,您在這可以定義多個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 );
}
當(dāng)程序執(zhí)行到這里時將會根據(jù)Web.Config里的定義觸發(fā)類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;
}
}
}
而它又會調(diào)用類CommandFactory的Make方法來處理接收到的參數(shù)并返回一個Command對象,緊接著它又會調(diào)用該Command對象的Execute方法把處理后參數(shù)提交到具體處理的頁面。
public class CommandFactory
{
public static Command Make( NameValueCollection parms )
{
string requestParm = parms["requestParm"];
Command command = null;
//根據(jù)輸入?yún)?shù)得到不同的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 );
//根據(jù)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 )
{
//在輸入?yún)?shù)中加入項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來確定要執(zhí)行的具體操作,無論執(zhí)行什么方法和對象,Handler只調(diào)用Command對象的Execute方法,您可以在不修改 Handler的情況下添加額外的命令。它允許讓用戶看不到實際的頁面,當(dāng)用戶輸入一個URL時,然后系統(tǒng)將根據(jù)web.config文件將它映射到特定的URL,這可以讓程序員有更大的靈活性,還可以獲得Page Controller實現(xiàn)中所沒有的一個間接操作層。
對于相當(dāng)復(fù)雜的Web應(yīng)用我們才會采用Front Controller模式,它通常需要將頁面內(nèi)置的Controller替換為自定義的Handler,在Front Controllrer模式下我們甚至可以不需要頁面,不過由于它本身實現(xiàn)比較復(fù)雜,可能會給業(yè)務(wù)邏輯的實現(xiàn)帶來一些困擾。
以上兩個Controller模式都是處理比較復(fù)雜的WebForm應(yīng)用,相對于直接處理用戶輸入的應(yīng)用來講復(fù)雜度大大提高,性能也必然有所降低,為此我們最后來看一個可以大幅度提高程序性能的模式:Page Cache模式。
Page Cache模式下的WebForm
幾乎所有的WebForm面臨的都是訪問很頻繁,改動卻很少的應(yīng)用,對WebForm的訪問者來說有相當(dāng)多的內(nèi)容是重復(fù)的,因此我們可以試著把WebForm或者某些相同的內(nèi)容保存在服務(wù)器內(nèi)存中一段時間以加快程序的響應(yīng)速度。
這個模式實現(xiàn)起來很簡單,只需在頁面上加入:
〈%@ OutputCache Duration="60" VaryByParam="none" %>,
這表示該頁面會在60秒以后過期,也就是說在這60秒以內(nèi)所有的來訪者看到該頁面的內(nèi)容都是一樣的,但是響應(yīng)速度大大提高,就象靜態(tài)的HTML頁面一樣。
也許您只是想保存部分的內(nèi)容而不是想保存整個頁面,那么我們回到MVC模式中的SQLHelper.cs,我對它進(jìn)行了少許修改:
public static DataSet GetPortal()
{
DataSet dataSet;
if ( HttpContext.Current.Cache["SELECT_PORTAL_CACHE"] != null )
{
//如果數(shù)據(jù)存在于緩存中則直接取出
dataSet = ( DataSet ) HttpContext.Current.Cache["SELECT_PORTAL_CACHE"];
}
else
{
//否則從數(shù)據(jù)庫中取出并插入到緩存中,設(shè)定絕對過期時間為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 )取出的內(nèi)容作為Cache的值。這樣除了程序第1次調(diào)用時會進(jìn)行數(shù)據(jù)庫操作外,在Cache過期時間內(nèi)都不會進(jìn)行數(shù)據(jù)庫操作,同樣大大提高了程序的響應(yīng)能力。?
轉(zhuǎn)載于:https://www.cnblogs.com/tommyli/archive/2008/01/04/1025513.html
總結(jié)
- 上一篇: 在水晶报表中实现任意选择指定字段显示-模
- 下一篇: 批量去除链接虚框