摘要 本文介绍了在NET框架下应用Web设计模式改进WebForm程序设计的一些基本方法及要点 关键字 设计模式ASPNETWebFormMVCPage ControllerFront ControllerPage Cache 目录 引言 经典的WebForm架构 设计模式 MVC模式下的WebForm Page Controller模式下的WebForm Front Controller模式下的WebForm Page Cache模式下的WebForm 引言 记得微软刚刚推出ASPNET时给人的震撼是开发Web程序不再是编写传统的网页而像是在构造应用程序因而微软称之为WebForm但是两年后的今天有相当多的开发人员仍然延用写脚本程序的思路构建一个又一个的WebForm而没有发挥出ASPNET的优势就此本文希望通过实例能够启发读者一些新的思路 由于篇幅有限本文不可能通过一个复杂的Web应用来向读者展示结合设计模式的WebForm但是如果仅仅是一个小程序的确没有使用模式的必要为了便于理解希望您能把它想象成是一个大型系统中的小模块(如果代码是大型系统的一部分那么使用模式就变得非常重要) 在本文的末尾给出了所有源程序的下载地址 经典的WebForm架构 首先来看一个简单的应用数据库设计如下图Portal是Subject的父表通过portalId进行一对多关联程序需要根据portalId显示不同的Subject列表 按照我们编写WebForm一般的习惯首先在页面上拖放一个DropDownList一个DataGrid一个Button控件 界面(webFormaspx) 〈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> 然后利用VSNET代码隐藏功能编写的核心代码如下 后置代码(webFormaspxcs) //页面初始化事件 private void Page_Load(object sender SystemEventArgs e) { if ( ! IsPostBack ) { string SQL_SELECT_PORTAL = SELECT * FROM PORTAL; //使用using确保释放数据库连接 //连接字符串存放在WebConfig文件中便于修改 using( SqlConnection conn = new SqlConnection( ConfigurationSettingsAppSettings[ConnectionString] ) ) { SqlDataAdapter dataAdapter = new SqlDataAdapter( SQL_SELECT_PORTAL conn ); DataSet dataSet = new DataSet(); dataAdapterFill( dataSet ); //设置下拉列表的数据源与文本域值域 dropDownListDataSource = dataSet; dropDownListDataTextField = portalName; dropDownListDataValueField = portalId; dropDownListDataBind(); } } } //Button的Click事件 private void button_Click(object sender SystemEventArgs e) { string SQL_SELECT_SUBJECT = SELECT * FROM SUBJECT WHERE portalId = {}; using( SqlConnection conn = new SqlConnection( ConfigurationSettingsAppSettings[ConnectionString] ) ) { //用下拉列表选择的值替换掉SQL语句中的待定字符{} SqlDataAdapter dataAdapter = new SqlDataAdapter( stringFormat( SQL_SELECT_SUBJECT dropDownListSelectedValue ) conn ); DataSet dataSet = new DataSet(); dataAdapterFill( dataSet ); dataGridDataSource = dataSet; dataGridDataBind(); } } 执行结果如图所示程序将根据下拉列表框选择的值绑定DataGrid非常典型的一个WebForm架构体现出ASPNET事件驱动的思想实现了界面与代码的分离但是仔细看看可以从中发现几个问题 对数据库操作的代码重复重复代码是软件开发中绝对的坏味道往往由于某些原因当你修改了一处代码却忘记要更改另外一处相同的代码从而给程序留下了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(SQLHelpercs)封装所有对数据库的操作 private static string SQL_SELECT_PORTAL = SELECT * FROM PORTAL; private static string SQL_SELECT_SUBJECT = SELECT * FROM SUBJECT WHERE portalId = {}; private static string SQL_CONNECTION_STRING = ConfigurationSettingsAppSettings[ConnectionString]; public static DataSet GetPortal() { return GetDataSet( SQL_SELECT_PORTAL ); } public static DataSet GetSubject( string portalId ) { return GetDataSet( stringFormat( 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(); dataAdapterFill( dataSet ); return dataSet; } } Controller(webFormaspxcs)负责转化用户的输入 private void Page_Load(object sender SystemEventArgs e) { if ( ! IsPostBack ) { //调用Model的方法获得数据源 dropDownListDataSource = SQLHelperGetPortal(); dropDownListDataTextField = portalName; dropDownListDataValueField = portalId; dropDownListDataBind(); } } private void button_Click(object sender SystemEventArgs e) { dataGridDataSource = SQLHelperGetSubject( dropDownListSelectedValue ); dataGridDataBind(); } 修改后的代码非常清晰MVC各司其制对任意模块的改写都不会引起其他模块的变更类似于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一般继承自SystemWebUIPage类而Page Controller的实现思想是所有的WebForm继承自定义页面基类如图 利用自定义页面基类我们可以统一的接收页面请求提取所有相关数据调用对Model的所有更新以及向View转发请求轻松实现统一的页面风格而由它所派生的Controller的逻辑将变得更简单更具体 下面看一下Page Controller的具体实现 Page Controller(BasePagecs) public class BasePage : SystemWebUIPage { private string _title; public string Title//页面标题由子类负责指定 { get { return _title; } set { _title = value; } } public DataSet GetPortalDataSource() { return SQLHelperGetPortal(); } public DataSet GetSubjectDataSource( string portalId ) { return SQLHelperGetSubject( portalId ); } protected override void Render( HtmlTextWriter writer ) { writerWrite( 〈html>〈head>〈title> + Title + 〈/title>〈/head>〈body> );//统一的页面头 baseRender( writer );//子页面的输出 writerWrite( @〈a >ASPNET〈/a>〈/body>〈/html> );//统一的页面尾 } } 现在它封装了Model的功能实现了统一的页面标题和页尾子类只须直接调用 修改后的Controller(webFormaspxcs) public class webForm : BasePage//继承页面基类 { private void Page_Load(object sender SystemEventArgs e) { Title = Hello World!;//指定页面标题 if ( ! IsPostBack ) { dropDownListDataSource = GetPortalDataSource();//调用基类的方法 dropDownListDataTextField = portalName; dropDownListDataValueField = portalId; dropDownListDataBind(); } } private void button_Click(object sender SystemEventArgs e) { dataGridDataSource = GetSubjectDataSource( dropDownListSelectedValue ); dataGridDataBind(); } } 从上可以看出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模式的具体实现 首先在WebConfig里定义 〈! 指定对Dummy开头的aspx文件交由Handler处理 > 〈httpHandlers> 〈add verb=* path=/WebPatterns/FrontController/Dummy*aspx type=WebPatternsFrontControllerHandlerWebPatterns/> 〈/httpHandlers> 〈! 指定名为FrontControllerMap的页面映射块交由UrlMap类处理程序将根据key找到对应的url作为最终的执行路径您在这可以定义多个key与url的键值对 > 〈configSections> 〈section name=FrontControllerMap type=WebPatternsFrontControllerUrlMap WebPatterns>〈/section> 〈/configSections> 〈FrontControllerMap> 〈entries> 〈entry key=/WebPatterns/FrontController/DummyWebFormaspx url=/WebPatterns/FrontController/ActWebFormaspx />
〈/entries> 〈/FrontControllerMap> 修改webFormaspxcs private void button_Click( object sender SystemEventArgs e ) { ResponseRedirect( DummyWebFormaspx?requestParm= + dropDownListSelectedValue ); } 当程序执行到这里时将会根据WebConfig里的定义触发类Handler的ProcessRequest事件 Handlercs public class Handler : IHttpHandler { public void ProcessRequest( HttpContext context ) { Command command = CommandFactoryMake( contextRequestParams ); commandExecute( 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 : command = new FirstPortal(); break; case : command = new SecondPortal(); break; default : command = new FirstPortal(); break; } return command; } } public interface Command { void Execute( HttpContext context ); } public abstract class RedirectCommand : Command { //获得WebConfig中定义的key和url键值对UrlMap类详见下载包中的代码 private UrlMap map = UrlMapSoleInstance; protected abstract void OnExecute( HttpContext context ); public void Execute( HttpContext context ) { OnExecute( context ); //根据key和url键值对提交到具体处理的页面 string url = StringFormat( {}?{} mapMap[ contextRequestUrlAbsolutePath ] contextRequestUrlQuery ); contextServerTransfer( url ); } } public class FirstPortal : RedirectCommand { protected override void OnExecute( HttpContext context ) { //在输入参数中加入项portalId以便页面处理 contextItems[portalId] = ; } } public class SecondPortal : RedirectCommand { protected override void OnExecute(HttpContext context) { contextItems[portalId] = ; } } 最后在ActWebFormaspxcs中 dataGridDataSource = GetSubjectDataSource( HttpContextCurrentItems[portalId]ToString() ); dataGridDataBind(); 上面的例子展示了如何通过Front Controller集中和处理所有的请求它使用CommandFactory来确定要执行的具体操作无论执行什么方法和对象Handler只调用Command对象的Execute方法您可以在不修改 Handler的情况下添加额外的命令它允许让用户看不到实际的页面当用户输入一个URL时然后系统将根据nfig文件将它映射到特定的URL这可以让程序员有更大的灵活性还可以获得Page Controller实现中所没有的一个间接操作层 对于相当复杂的Web应用我们才会采用Front Controller模式它通常需要将页面内置的Controller替换为自定义的Handler在Front Controllrer模式下我们甚至可以不需要页面不过由于它本身实现比较复杂可能会给业务逻辑的实现带来一些困扰 以上两个Controller模式都是处理比较复杂的WebForm应用相对于直接处理用户输入的应用来讲复杂度大大提高性能也必然有所降低为此我们最后来看一个可以大幅度提高程序性能的模式Page Cache模式 Page Cache模式下的WebForm 几乎所有的WebForm面临的都是访问很频繁改动却很少的应用对WebForm的访问者来说有相当多的内容是重复的因此我们可以试着把WebForm或者某些相同的内容保存在服务器内存中一段时间以加快程序的响应速度 这个模式实现起来很简单只需在页面上加入 〈%@ OutputCache Duration= VaryByParam=none %> 这表示该页面会在秒以后过期也就是说在这秒以内所有的来访者看到该页面的内容都是一样的但是响应速度大大提高就象静态的HTML页面一样 也许您只是想保存部分的内容而不是想保存整个页面那么我们回到MVC模式中的SQLHelpercs我对它进行了少许修改 public static DataSet GetPortal() { DataSet dataSet; if ( HttpContextCurrentCache[SELECT_PORTAL_CACHE] != null ) { //如果数据存在于缓存中则直接取出 dataSet = ( DataSet ) HttpContextCurrentCache[SELECT_PORTAL_CACHE]; } else { //否则从数据库中取出并插入到缓存中设定绝对过期时间为分钟 dataSet = GetDataSet( SQL_SELECT_PORTAL ); HttpContextCurrentCacheInsert( SELECT_PORTAL_CACHE dataSet null DateTimeNowAddMinutes( ) TimeSpanZero ); } return dataSet; } 在这里把SELECT_PORTAL_CACHE作为Cache的键把GetDataSet( SQL_SELECT_PORTAL )取出的内容作为Cache的值这样除了程序第次调用时会进行数据库操作外在Cache过期时间内都不会进行数据库操作同样大大提高了程序的响应能力 小结 自从NET框架引入设计模式以后在很大程度上提高了其在企业级应用方面的实力可以毫不夸张的说在企业级应用方面NET已经赶上了Java的步伐并大有后来居上之势本文通过一个实例的讲解向读者展示了在NET框架下实现Web设计模式所需的一些基本知识希望能起到一点抛砖引玉的作用 |