一、引言
许多Web应用都是从数据存储中检索数据并将其显示给用户。在用户更改数据之后,系统再将更新内容存储到数据存储中。因为关键的信息流发生在数据存储和用户界面之间,所以很多应用将数据和用户界面这两部分绑在一起,以减少编码量并提高应用程序性能。但是,这种看起来自然而然的方法有一些大问题。一是,用户界面的更改往往比数据存储系统的更改频繁得多。二是,这种耦合往往会合并其他业务逻辑。那么如何让 Web 应用程序的用户界面功能实现模块化,以便可以轻松地单独修改各个部分呢?面向对象的设计模式是经验的总结,MVC架构可以很好地解决上述问题。
.NET是当今设计和开发各种Web应用的主流平台,MVC架构在J2EE平台上已有成熟的设计方案,而在.NET平台上却少有应用。所以讨论其在Asp.net环境下的应用和实现,仍很有意义。
本文首先论述了MVC架构的原理、优缺点以及它所能为Web应用带来的好处。并结合作者在“成都微软技术中心”实习期间,研发项目的经验。介绍了一种在Asp.net环境下的实现方式。旨在帮助Web设计开发者更好的了解和掌握MVC,合理利用MVC构建优秀的Web应用。虽然本文是在.net环境下的实现,但这并不妨碍你对MVC架构的理解。学习MVC架构,重在学习其思想。
二、MVC介绍
MVC是一种软件开发架构,它包含了很多的设计模式,最为密切是以下三种:Observer (观察者模式), Composite(组合模式)和Strategy(策略模式)。MVC最初是在Smalltalk-80中被用来构建用户界面的。
MVC架构把数据处理,程序输入输出控制及数据显示分离开来,并且描述了不同部件的对象间的通信方式,使得软件可维护性,可扩展性,灵活性以及封装性大大提高。
MVC(Model-View-Controller)把系统的组成分解为M(模型)、V(视图)、C(控制器)三种部件。V(视图)表示数据在屏幕上的显示;C(控制器)提供处理过程控制,它在模型和视图之间起连接作用。控制器本身不输出任何信息和做任何处理,它只负责把用户的请求转成针对Model的操作,和调用相应的视图来显示Model处理后的数据。
三者之间关系如下图2.1:
图2.1 MVC关系图
2、为什么要在Web应用中使用MVC架构
用户界面逻辑的更改往往比业务逻辑频繁,尤其是在基于Web的应用程序中。例如,可能添加新的用户界面页,或者可能完全打乱现有的页面布局。对显示的更改,尽可能地不要影响到数据和业务逻辑。
目前大部分Web应用都是将数据代码和表示混在一起。经验比较丰富的开发者会将数据从表示层分离开来,但这通常不是很容易做到的,它需要精心的计划和不断的尝试。MVC从根本上强制性的将它们分开。尽管构造MVC应用需要一些额外的工作,但它带来的好处是无庸质疑的。
2.1 提高代码重用率
最重要的一点是多个视图能共享一个模型,无论用户想要Flash界面或是 WAP 界面;用一个模型就能处理它们。由于已经将数据和业务规则从表示层分开,所以可以最大化的重用代码。
2.2 提高程序的可维护性
因为模型是自包含的,并且与控制器和视图相分离,所以很容易改变数据层和业务规则[3]。例如,把数据库从MySQL移植到Oracle,或者把基于RDBMS数据源改变到LDAP,只需改变模型即可。一旦正确的实现了模型,不管数据来自哪里,视图都会正确的显示它们。MVC架构的运用,使得程序的三个部件相互对立,大大提高了程序的可维护性。
2.3 有利于团队开发
在开发过程中,可以更好的分工,更好的协作。有利于开发出高质量的软件。良好的项目架构设计,将减少编码工作量 :采用MVC结构 + 代码生成器,是大多数Web应用的理想选择。部分模型(Model)、和存储过程一般可用工具自动生成。控制(Controller)器比较稳定,一般由于架构师(也可能是有经验的人)完成;那么整个项目需要手动编写代码的地方就只有视图(View)了。在这种模式下,个人能力不在特别重要,只要懂点语法基础的人都可以编写,无论项目成员写出什么样的代码,都在项目管理者的可控范围内。即使项目中途换人,也不会有太大问题。在个人能力参差不齐的团队开发中,采用MVC开发是非常理想的。
3 MVC在 Asp.net中的原理及实现
Asp.net提供了很好实现这种模式的类似环境。通过在ASPX页面中开发用户部件或继承母板页MasterPage来实现视图;控制器的功能一般可以放在对应的逻辑功能代码(.cs)中实现;模型通常对应应用系统的业务部分。模型一般包含业务逻辑、业务规则和数据访问层。MVC可和经典的N层结构配合使用。
将用户显示(视图)从动作(控制器)中分离出来,提高了代码的重用性。将数据(模型)从对其操作的动作(控制器)分离出来可以设计一个与后台存储数据无关的系统。就MVC结构的本质而言,它是一种解决耦合系统问题的方法。实现基于MVC的应用需要完成以下步骤,如右图3.1所示:
1、分析当前应用,分解系统功能:
分析当前应用问题,分离出系统的内核功能(Model)、系统的输入输出(View)、系统的输流程控制,行为控制等控制功能(Controller)三大部分。
2、设计和实现模型:
设计模型部件使其封装应用功能、属性。提供访问显示数据的操作,提供控制内部行为的操作以及其他必要的操作接口。这部分的构成与具体的应用问题紧密相关。
3、设计和实现视图:
设计每个视图的显示形式,视图从模型中获取数据,并将数据显示在屏幕上。提供发送用户请求给控制器;提供允许控制器选择视图。
4、设计和实现控制器:
对于每个视图,实现对用户的请求映射到模型。并根据模型处理结果,选择合适的视图显示。在模型状态的影响下,控制器使用特定的方法接受和解释这些事件。控制器的初始化建立起与模型和视图的联系,(这里一般会用观察者模式)并且启动事件处理机制。事件处理机制的具体实现方法依赖于界面的工作平台。
MVC并没有明确的定义,它仅代表一种软件设计思想。所以在不同的应用环境下,可能有不同的实现方式。只有深刻理解其思想,结合实际情况。才能构建合理的应用。下面以“成都市信息化资产管理系统”框架设计为例,介绍MVC构架在Asp.net下的一种实现方式。该框架中并没有使用观察者模式,因为依赖关系(本项目中只有两种视图,列表页面和编辑、查看详情页面。而且将来增加视图的可能性也不大)固定或者几乎固定时,加入一个观察者模式,只会增加系统复杂性。
本项目框架结构包括逻辑结构图3.2和物理结构图3.3两部分。
从逻辑结构图,可以看出对数据库的访问并没有完全用存储过程,这是出于运行效率和开发效率的考虑。
这里的存储过程对每个实体都只包括基本的CRUD四种操作。
3.1 View(视图)
3.1.1原理
视图用于管理信息的显示,它提供用户交互界面。使用多个包含单页面显示的用户部件,复杂的Web页面可以展示来自多个数据源的内容,并且网页人员,美工能独自参与这些Web页面的开发和维护。在Asp.net下,视图的实现很简单。可以像开发WINDOWS界面一样直接在集成开发环境下通过拖动部件来完成页面开发本。每一个页面也可以采用复合视图的形式即:一个页面由多个子视图(用户部件)组成;也可以继承母板页MasterPage。子视图可以是最简单HTML 部件、服务器部件或多个部件嵌套构而成的Web自定义部件或Web页面。 页面都由模板定义,模板定义了页面的布局,用户部件的标签和数目,用户指定一个模板(这里的模板指Html页面、Asp.net页面、用户部件等),.net平台根据这些信息自动创建页面。针对静态的模板内容,如页面上的站点 导航,菜单,友情链接,这些使用缺省的模板内容配置;针对动态的模板内容(主要是业务内容),由于用户的请求不同,只能使用后期绑定,并且针对用户的不同,用户部件的显示内容进行过滤。使用由用户部件根据模板配置组成的组合页面,它增强了可重用性,并简化了站点的布局。在.Asp.net2.0中,可以使用MasterPage来简化视图设计。在MasterPage里设置的Skin(皮肤),会根据不同子视图(继承自MasterPage页)中的Them(主题)。自动选择合适的Skin显示。可以说MasterPage是MVC架构思想的很好体现。
视图部分大致处理流程如下:首先,页面模板定义了页面的布局;页面配置文件定义视图标签的具体内容(用户部件);然后,由页面布局策略类初始化并加载页面;每个用户部件根据它自己的配置进行初始化,加载校验器并设置参数,以及事件的委托等;用户提交后,通过了表示层的校验,用户部件把数据自动提交给业务实体即模型。
这一部分主要定义了WEB页面基类PageBase;页面布局策略类PageLayout,完成页面布局,用于加载用户部件到页面;用户部件基类 UserControlBase即用户控件框架,用于动态加载检验部件,以及实现用户部件的个性化。为了实现WEB应用的灵活性,视图部分也用到了许多配置文件例如:模板配置、页面配置、路径配置、验证配置等。
3.1.2实现
良好的界面架构设计,将减少界面调整时间。在.net下应充分利用Asp.net2.0新特性,自动导航,SiteMap、MasterPage、MemberShip、MultiView、Them、Skin等。在本项目中,每个模块的View,实际上都只有两种,一种是用来显示多条数据的列表页面,一种是用来编辑、和查看详情的页面。由于View种类几乎是固定的,所以不需要加入Observer(观察者)模式。让所有的编辑页都继承自“母板页dialog.master”,所有列表页都继承“母板页Main.master”即可。如图3.4
每个列表页面的动态显示区域仅为ContentPlaceHolder即黄色区域部分,这就保证相同类型页面风格的一致。按照命名规范和便于理解的原则,我们把所有的编辑页后缀都取名为“EditPG.aspx”,所有的列表页后缀都取名为“ListPG.aspx”。对View的改变,可以通过Asp.net2.0的主题(Themes)来实现。本例中编辑和查看详情页面,用的就是相同的视图(View)。如果要添加不同的View,只需添加相应的Master,和完成具体的显示要求。在本项目中,所有的编辑页面一般只需要实现基类(DialogUIBase)提供的如下方法。
//得到数据,并存放在对应的Model中,供View使用
protected override void GetDataFromDB(object keyValue){…}
//用Model中的数据填充编辑或显示界面
protected override void SetEditText(){…}
//重新填写编辑框页面的部件内容, 对部分用户可能重复填写的部件内容不进行赋默 认值操作
protected override void ResetEditText(){…}
//检查用户输入正确性
protected override string CheckUserInput(){…}
//保存用户输入,把用户输入更新到数据库
protected override object SaveEditText(object keyValue){…}
所有的列表View一般也只用实现基类(GridViewUIBase)提供的如下几个虚方法.
//返回子类中使用的GridView,子类必须继承
protected override GridView GetGridView(){…}
// 返回子类中的GridView中复选框列模板中复选框的名称,子类根据有无该模板列进 行选择继承
protected override string GetGridCheckBoxName(){…}
通常无需处理的继承方法
//绑定DataGrid部件事件,
protected override void BindGridEvent()
//绑定除通过GetDataGrid()传入的DataGrid部件以外的部件的客户端事件
protected override void BindControlEvent()
// 返回GridViewList使用的数据源, 子类必须继承
protected override object GetDataSource()
// 多条记录删除,在删除按钮事件中调用
protected override void DelRecords(object keyValuesString)
// 返回以CommandName为key以GridEventPageParam类型参数为内容的hashtable
protected override Hashtable GetDialogParams(){…}
从以上代码中可以很容易发现,无论是列表页面还是编辑页面,都没有和流程相关的东西,这正是MVC所要做的,View中只包含数据的显示,流程完全由基类控制。好处是显而易见的。不同的人写出来的View也具有相同的风格。
3.2 Controller(控制器)
3.2.1原理
Controller控制器是Model与View之间沟通的桥梁,它可以分派用户的请求并选择恰当的视图以用于显示,同时它也可以解释用户的输入并将它们映射为模型层可执行的操作。在.NET中每个aspx对应了一个后端代码aspx.cs,可以通过aspx.cs方便地实现Controller的功能。每个Asp.net页面都有一种机制,将页面中的部件所要调用的方法在一个与其分离的类中实现。这些aspx和ascx文件后端代码继承了System.Ul.Web.Page的类执行控制器功能,它包括了各种初始化和控制函数。当加载aspx页面时将调用Page_ Load事件,当aspx页面从内存中被卸载时将调用Page_UnLoad事件。如果某个部件触发页面以使其被重新加载则将调用Control Event事件。
3.2.2实现
对应所有编辑页面的控制类为DialogUIBase.cs ,该类完成所有编辑页面的流程控制、请求控制 ;对应所有列表页面的控制类为GridViewUIBase.cs ,该类完成所有列表页面的流程控制和请求控制 ;这两个类都位于App_Code文件夹下。
编辑页面基类(DialogUIBase)和列表页面基类(GridViewUIBase)都继承自System.Web.UI.Page,都包含两部分,一是供View子类继承的虚方法,一是对View子类流程控制的方法。
本例中GridViewUIBase中主要包含的方法有:
供子类继承的方法#region 供子类继承的方法
//返回子类中使用的GridView
protected virtual GridView GetGridView(){…}
//返回子类中的GridView中复选框列模板中复选框的名称,子类根据有无该模板列进行选择继承
protected virtual string GetGridCheckBoxName(){…}
// 返回GridView使用的数据源
protected virtual object GetDataSource(){…}
// 删除View中的选择的数据
protected virtual void DelRecords(object keyValuesString){…}
// 返回以CommandName为key以GridEventPageParam类型参数为内容的hashtable
protected virtual Hashtable GetDialogParams(){…}
// 绑定GridView部件客户端事件,通本默认绑定函数绑定的客户端事件,被绑定列的所有行均调用相同的对话框页面,如果要不同的行调用不同的对话框页面则需要重写该函数
protected virtual void BindGridEvent(){…}
//绑定除通过GetGridView()传入的GridView部件以外的部件的客户端Click事件
protected virtual void BindControlEvent(){…}
//按钮事件绑定
public void BindBtnEvent(…){…}
//表格事件绑定
public void BindGridEvent(…){…}
DialogUIBas类和GridViewUIBase类,设计思路完全相同。所以不再举例。从上面当面可以发现在GridViewUIBase中,实现了对View的控制。根据用户的请求的不同,调用不同的Model进行处理。
3.3 Model(模型)
3.3.1原理
Model对象代表了商业规则和商业数据,单个模型代表问题域中的某个对象,或叫做实体。所以模型要封装系统的应用功能和应用属性。提供访问显示数据的操作,提供控制内部行为的操作以及其他必要的操作接口。模型的构成与具体的应用问题紧密相关。通常模型包括数据访问、商务逻辑和商务规则。在Asp.net中,简单的模型可以方便地用自动代码生成工具实现。VS IDE 2003、VS IDE 2005本身就提供了很好的支持,可以从数据库或XML等数据源,轻松的生成强类型的DataSet和DataTable。数据访问层可以使用Application Block块。或Enterprise Library 等开源组件。当然你也可以手动完成这些工作,如果你愿意。
3.3.2实现
在本示例中,业务处理对象和业务实体对象都继承自EntityBase类。EntityBase类又继承自Entity类。Entity类是数据库访问的基类。它主要包含供子类继承的方法(用存储过程完成数据库的CRUD操作)。和供外部类调用的方法(Model完成CRUD操作)。设置两种方式是因为逻辑结构的需要。
供子类继承的主要方法如下:
protected virtual void Init(){…}//执行必要的初始化
protected virtual bool Proc_Insert(){…} //添加
protected virtual bool Proc_Update(object KeyValue) {…} //更新
protected virtual bool Proc_ReadByKeyValue(object KeyValue){…} //检索
protected virtual bool Proc_Delete(object KeyValue){…} //删除
protected virtual DataTable Proc_ReadAll(){…} //检索所有
protected virtual void AfterLoad(){…} //数据库中数据更新模型之前
protected virtual void BeforeSave(){…}//用模型更新数据库之前
供外部调用的主要方法如下:
public DataTable ReadAll(){…} //检索所有
public object Insert(){…} //添加
public object Update(object keyValue){…} //更新
public bool Load(object keyValue){…} //填充Model
public bool Delelte(object keyValue){…} //删除
public void Clear() //清除Model
EntityBase类,只需实现基类(Entity)的四个虚CRUD方法,和定义Model自身相关的属性。由于Model又继承于EntityBase类,所以如果某个Model需要进行额外的操作,可添加到该Model对应的AfterLoad()或BeforeSave()方法中。
3.4 MVC架构的扩展设计
通过在Asp.net中使用MVC模式,可以构建,具有良好扩展性的Web应用。MVC构架可以轻松实现以下功能:
①实现一个模型的多个视图;
②采用多个控制器;
③当模型改变时,所有视图将自动刷新;
④所有的控制器将相互独立工作。
这就是MVC模式的好处,只需在以前的程序上稍作修改或增加新的类,即可轻松增加许多程序功能。以前开发的许多类可以重用,而程序结构根本不再需要改 变,各类之间相互独立,便于团体开发,提高开发效率。下面讨论如何实现一个模型、两个视图和一个控制器的程序。其中模型类及视图类根本不需要改变,与前面的完全一样,这就是面向对象编程的好处。对于控制器中的类,只需要增加另一个视图,并与模型发生关联即可。该模式下视图、控制器、模型三者之间的示意图如图3.5所示。
同样也可以实现其它形式的MVC例如:一个模型、两个视图和两个控制器。从上面可以看出,通过MVC模式实现的应用程序具有极其良好的可扩展性,是Asp.net面向对象编程的未来方向。
4 MVC架构的优点及不足
4.1 MVC的优点
MVC的优点体现在以下几个方面:
(1) 有利于团队开发分工协作和质量控制,降低开发成本。
(2) 可以为一个模型在运行时同时建立和使用多个视图。变化-传播机制可以确保所有相关的视图及时得到模型数据变化,从而使所有关联的视图和控制器做到行为同步。
(3) 视图与控制器的可接插性,允许更换视图和控制器对象,而且可以根据需求动态的打开或关闭、甚至在运行期间进行对象替换。
(4) 模型的可移植性。因为模型是独立于视图的,所以可以把一个模型独立地移植到新的平台工作。需要做的只是在新平台上对视图和控制器进行新的修改。
(5) 潜在的框架结构。可以基于此模型建立应用程序框架,不仅仅是用在设计界面的设计中。
4.2 MVC的缺点
MVC的不足体现在以下几个方面:
(1)增加了系统结构和实现的复杂性。对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
(2)视图对模型数据的访问效率低。视图可能需要多次调用Model才能获得足够的显示数据。
(3)完全理解MVC并不是很容易。使用MVC需要精心的计划,由于它的内部原理比较复杂,所以需要花费一些时间去思考。 同时由于模型和视图要严格的分离,这样也给调试应用程序到来了一定的困难。
结束语
与软件所处理问题的内在模型相比较,用户界面是需要经常发生变化的,采用MVC设计模式可以在满足对界面要求的同时,使软件的计算模型独立于界面的构成。也可以基于此模型建立大型分布式应用程序框架。
MVC并不适合小型甚至中等规模的应用程序,花费大量时间将MVC应用到规模并不是很大的应用程序通常会得不偿失。
MVC是一种软件开发架构。和其它设计模式一样,它不是万能的,也不是一成不变的。要根据具体情况灵活运用。在上面的示例项目中,为了提高运行和开发效率。在Model设计上就提供了两种访问方式。
示例中的MVC采用了集中控制的方式。一个列表控制器GridViewUIBase,对应多个列表视图。一个编辑控制器DialogUIBase对应对个编辑、查看详情视图。对每个模型而言,仅有两种视图,且几乎是固定不变的。所以没有增加Observer(观察者)模式。这样减少了系统的复杂性。本示例最精彩的部分,就是控制器的设计。各视图执行流程完全封装在控制器中。由于视图中不含有任何控制信息,流程信息。所以视图编码人员完全不用了解Http的无状态特性等。对他们而言,开发WebForm和WinForm是一样的。当然这种设计也有它的不足,如果修改某个视图的显示,有可能还要修改相关的控制器。