两周前我发表了介绍将作为ASP.net特性的 MVC框架的技术文章,该框架降低应用程序各部分之间的耦合程度,更有利于单元测试的进行并支持TDD工作流,同时它可以通过应用程序中的URL路径及其中的HTML代码提供更多的控制。
之后我答复了很多朋友关于这篇文章的问题,我想又必须要继续对该框架的使用做进一步的介绍,这篇文章是我本系列文章的第一篇。
一个简单的电子商务前端应用
我将用一个简单的网上商店应用程序来演示ASP.net MVC框架的工作原理。这篇文章将要演示其中产品列表/浏览的应用。
首先我们要建立一个前台显示页面,使得用户在访问网站的/Products/Categories路径时可以浏览产品目录:
当用户点击某一个具体产品,将会进入选择产品的详细信息显示页面/Products/Detail/ProductID。
我们要使用ASP.net MVC 框架实现上述功能。这让我们可以清晰划分应用程序的不同组件,并使单元测试和测试驱动开发模型更容易被集成。
创建一个新的ASP.net MVC 应用程序
包含Visual Studio项目模板的ASP.net MVCFramework使创建web应用程序变得更加方便。选择“文件”->“新建项目”菜单项,并点击“ASP.net MVC Web Application”模板创建一个新的Web应用程序。
当你用该选项创建项目时, Visual Studio默认创建一个解决方案并向其中添加两个项目,第一个项目是实现应用程序的Web应用程序,另一个是用于针对web应用程序编写单元测试的测试项目。
在ASP.net MVC框架中你可以使用任何一种单元测试框架(包括Nunit, MBUnit, MSTest, Xunit等等)。VS2008专业版内置了对MSTest测试项目(在先前的VS2005版本中需要安装Visual Studio Team System SKU)的支持,同时ASP.net MVC框架在VS2008中自动创建了这个项目。
我们会陆续提供适用于Nunit, MBUnit和其他单元测试框架的ASP.net MVC项目模板下载,因此如果你偏向于使用上述这些单元测试工具,你仍然可以通过这种简单的方式创建需要的应用程序以及对应的单元测试项目。
理解项目的目录结构
ASP.net MVC 应用程序默认有3个父级目录
- /Controllers
- /Models
- /Views
正如你所想象的那样,我们建议你将控制类放在/Conrollers目录中,数据实体类放在/Model文件夹中,视图模板放在/Views文件夹中。
虽然ASP.net MVC框架并没有强制要求你使用这种文件结构,但是除非你有更好的理由去使用其他的文件组织方式,我们仍然推荐你用这种默认方式组织你的应用程序。
将URL地址映射到控制类
在多数Web框架中(ASP, PHP, JSP, ASP.net Form等等)URL地址通常映射到磁盘上具体的物理文件,例如,对于路径“/Products.aspx”或“/Products.php”通常在磁盘上都有一个Products.aspx或Products.php文件与之相对应。当一个针对Web应用程序的http请求发送到服务器时,web框架将请求的处理权交由请求的文件,通常这个文件通过使用HTML标记来生成发送到客户端的回应内容。
MVC框架使用另一种不同的方式将URL路径直接映射到服务器端的代码,也就是将URL直接映射到类而不是映射到物理文件。这些被影射到的类被称为“控制类”,他们可以处理服务器请求,处理用户输入和与用户进行交互,执行应用逻辑和数据逻辑等。控制类将会调用一个独立的“视图”组件来产生针对请求的HTML输出。
ASP.net MVC框架包含了一个强大的URL映射引擎,可以处理多种复杂的URL到控制类的关系映射。你可以轻松的设置映射规则,使ASP.net遵循这些规则解析URL路径并调用相应的控制类,也可以使用URL映射引擎自动解析URL路径中的变量并将它们作为参数传递给控制类。我将在这个系列的后续文章中详细介绍URL映射引擎的高级使用。
ASP.NET MVC URL到控制类的默认映射
ASP.net MVC项目预置了一系列URL映射规则让用户不需要进行任何配置就可以开始开发应用程序,当然你也可以依照Visual Studio的ASP.NET MVC 项目模板提供的的Global.asax文件中定义的应用程序类中声明的一系列默认URL名称映射规范进行开发。
默认的命名规范是将一个HTTP请求的URL的起始路径部分(例如:/Products)映射到一个具有UrlPathController名称的类(例如:一个由/Products/开始的URL路径,将被映射到ProductsController类中)。
我们在项目中建立一个“ProductsController”类来实现电子商务的产品浏览功能(你可以点击Visual Studio中的“添加新项目”菜单项,根据模板创建一个控制类)。
我们的ProductsController类从System.Web.MVC.Controller基类中继承而来,但控制类并非必须继承自该积累,只是该基类中包含了很多我们后面要用到的很多有用的辅助方法和功能。
当我们在项目中创建了ProductsController类之后,ASP.NET MVC框架默认便将所有“/Products”引导的URL请求交由该类处理,这将意味着在处理诸如"/Products/Categories", “/Products/List/Beverages",或"/Products/Detail/3"这些我们在后面将要提到前端应用时都将调用这个控制类进行处理。
在下一篇文章里,我们将要继续添加ShoppingCartController(用于管理用户的购物车)和AccountController(用于管理用户的账号和登录注销操作)两个控制类,同样以”/ShoppingCart/”和”Account”开头的URL请求也将分别由这两个类进行处理。
提示:ASP.net MVC 框架并不要求你一定遵循这个命名样式,我们之所以这样做是因为在Visual Studio中创建ASP.net MVC项目时,该默认的URL映射规则被自动添加到了项目的应用程序类中。如果你认为这种规则并不合理,你可以打开应用程序类文件(Global.asax文件)修改规则。在后面的文章里我将介绍这以操作(同时我会展示一些URL引擎提供的强大功能)。
理解控制方法
在创建ProductController类之后我们可以开始添加处理”/Products/”请求的逻辑了。
在文章开头部分定义电子商务的前端用例时,我曾提到我们要实现的三个功能:1)浏览产品目录,2)列出某一目录的所有产品和3)展示某一具体产品的详细信息。我们将要使用下面的SEO(Search Engine Optimize)友好路径来实现每种功能。
URL 格式
|
行为
|
示例
|
/Products/Categories
|
显示所有目录
|
/Products/Categories
|
/Products/List/Category
|
列出某一目录的产品
|
/Products/List/Beverages
|
/Products/Detail/ProductID
|
显示某产品的信息
|
/Products/Detail/34
|
我们可以通过多种方式来实现ProductsController类中对3种URL的处理功能,其中一种方式是通过重写控制基类Controller中的”Execute”方法,在其冲添加if/else/switching逻辑判断传入的URL并决定相应的处理逻辑。
然而,另一种更简单的方式是使用MVC框架为我们提供的内置特性:MVC框架允许我们自定义操作方法(Action Method),同时Controller基类会根据当前应用程序中配置的URL映射规则自动调用相应的操作方法。
例如,我们可以在ProductsController类中添加下面三个控制类操作方法处理我们前面提及的三种功能:
项目创建时默认的URL映射规则规定URL中紧跟控制类名称的子路径是请求的控制方法名称,例如如果我们收到一个/Products/Categories请求,路由规则将把”Categories”作为操作方法名,Categories()方法将用于处理该请求等等。
提示:ASP.net MVC框架并不要求你一定遵循这个规范,如果你使用不同的映射规则,你只需要打开应用程序类并进行修改。
URL的参数到控制类方法的映射
控制基类封装了Request和Response对象以供使用,这些对象与ASP.net中你所熟悉的HttpRequest/HttpResponse对象拥有相同的API接口,但他们之间一个重要的区别是这些对象现在是基于接口的而不是抽象基类(说明:MVC框架中同时包含了System.Web.IhttpRequest和System.Web.IhttpResponse接口)。使用接口的优点是可以方便对控制类进行单元测试,在后面的文章里我将详细阐述这一点。
下面是这个例子介绍了我们如何使用Request获取查询字符串(QueryString)中的ID变量,并传递给ProductController控制类中的Detail操作方法:
ASP.net MVC框架也支持将URL参数映射到操作方法的操作。如果你的操作方法中具有一个参数,MVC框架默认查找请求数据中是否存在具有相同名称的相关HTTP请求变量,如果存在,它将作为函数参数自动传递给操作方法。
例如,利用这种特性,我们可以按照下面更简洁的方式实现Detail方法:
除了从请求的查询字符串和Form变量中实现参数值映射,ASP.net MVC框架同样通过 URL映射规则将参数值集成在URL中(例如:使用/Products/Detail/3代替/Products/Detail?id=3)
MVC 项目的默认URL映射规则是"/[controller]/[action]/[id]",这表明在URL子路径中控制类名称(Controller)和动作方法名称(action)之后的部分将被自动作为方法参数传入控制类操作方法。
这表明我们可以从路径(例如: /Products/Detail/3)中获取Detail方法的ID参数。
利用同样的方式,我们可以通过URL路径向List方法传入产品目录名称参数(例如:/Products/List/Beverages)。为了增强代码的可读性,我修改了映射规则,使用“Category”作为这个方法的参数而不是“id”。
下面是具备了完整的URL映射和参数映射的ProductController类的实现:
注意上面的例子中List方法是如何从URL中的查询字符串(querystring)获取目录参数以及可选的页码参数(我们将利用这个参数在服务器端实现分页显示的逻辑代码)。
MVC框架中的操作方法的可选参数使用nullable类型的参数,因为List方法的页码参数是可选参数(int?代表可以为空),所以MVC框架会根据传入的URL自动为参数赋值。关于nullable类型的使用请阅读我的另一篇相关文章。
创建数据模型对象
现在我们已经完成了ProductContoller类并添加了三个操作方法处理Web请求,我们的下一步是创建其他的类用于访问数据库并获取请求的数据。
在MVC中“模型(model)”是用于维护状态的应用程序组件。在Web应用中,状态通常在数据库中保持(例如我们可以使用Product对象代表产品SQL数据库中Product表的数据)。
ASP.net MVC框架允许使用多种数据访问模式和框架创建和管理模型。你可以使用ADO.net DataSet/DataReader(或任何他们的抽象对象),也可以使用像Nhibernate,LLBLGen, WilsonORMapper, LINQ to SQL/LINQ to Entity之类的数据库实体映射工具(ORM).
在我们的电子商务示例程序中,我们将要使用.NET3.5和VS2008中内置的LINQ to SQL ORM映射工具,在以后的文章里我介绍更多关于LINQ to SQL的知识(包括Part1, Part2, Part3 和Part4)。
在VS项目中的”Model”目录上右单击选择“添加新项目”,添加一个LINQ to SQL模型。在LINQ to SQL ORM设计器中我会定义3个数据对象类对应SQL Server的示例数据库Northwind中的类别(Categories)、产品(Products)和供应商(Suppliers)表(阅读我LINQ to SQL系列文章的Part 2了解如何完成该操作)。
完成LINQ to SQL数据模型类后,我将在模型文件夹Model中添加NorthwindDataContext分布类:
在这个类中我将定义一个封装了一些LINQ表达式的辅助方法,以用于从数据库中获取指定的产品目录类别、获取指定产品目录的所有产品和指定ProductID的产品信息。
These helper methods will make it easy for us to cleanly retrieve the data model objects needed from our ProductsController class (without having to write the LINQ expressions within the Controller class itself):
辅助方法让ProductController类中获取所需要的数据模型的代码更加整洁(不需要在控制类中编写LINQ 表达式):
现在我们已经创建完成了在ProductController功能重所需要的所有数据代码和对象。
完成ProductController类的实现
MVC应用程序中的控制类用于处理请求,与用户交互和执行应用逻辑(创建和更新数据库中的对象模型)。
控制类通常不生成HTML代码,生成HTML代码的工作由“视图(View)”组件完成,视图是与控制类相独立的类和模板。视图主要完成封装显示逻辑,不应该包含任何应用逻辑或数据库访问代码(所有的应用逻辑都应该由控制类进行处理)。
在MVC Web工作流程中,控制类操作方法将处理Web请求,接受传入参数并执行相应的逻辑代码,根据数据库创建或获取对象模型,并选择一个“视图”用于生成相应的UI发送到浏览器。控制类将向视图传入所有需要的数据和参数用于显示数据。
你或许想知道像这样将控制类和视图分离的有点是什么,为什么不把他们放在一个相同的类中?这样做主要的优点是有效的将应用程序和数据逻辑分离,使针对应用程序和数据逻辑的单元测试与UI呈现分离,同时可以使应用程序更容易维护,因为在视图模板中不允许添加应用程序和数据逻辑。
在实现ProductController类中的三个控制方法时,我们使用传入URL路径中的参数从数据库中获取对应的对象模型,然后选择一个“视图”组件用于生成相应的HTML响应。我们使用Contoller基类中的RenderView()方法指明我们希望使用的视图,同时传入视图显示时所需要的数据。
下面是ProductsController类的最终实现:
注意到上面的操作方法代码都很少(每个方法只有两行)。这得益于MVC框架啊为我们提供的URL参数解析逻辑(避免书写很多逻辑代码),同时也因为产品浏览的逻辑功能比较简单(操作方法只有只读的显示功能)
你通常会发现你的一些方法代码非常短小,这表明你对数据逻辑和控制逻辑进行了很好的封装。
对ProductController进行单元测试
或许你会对我们下一步将要进行程序测试而感到奇怪,你或许会问这怎么可能?我们还没有实现视图,我们的应用程序现在甚至没有产生一个HTML标记,MVC吸引人的特性之一就是可以在视图/HTML产生逻辑完成之前独立进行针对控制类和模型逻辑的单元测试。正像你将要在下面看到的,我们甚至可以在创建视图之前进行单元测试。
为了对ProductController类进行单元测试我们在项目默认添加的测试项目中新建了一个ProductControllerTest类:
接下来我们要哦啊定义一个简单的单元测试对ProductController类中的Detail函数进行测试:
The ASP.NET MVC framework has been designed specifically to enable easy unit testing. All core APIs and contracts within the framework are interfaces, and extensibility points are provided to enable easy injection and customization of objects (including the ability to use IOC containers like Windsor, StructureMap, Spring.NET, and ObjectBuilder). Developers will be able to use built-in mock classes, or use any .NET type-mocking framework to simulate their own test versions of MVC related objects.
ASP.net MVC框架转为利于单元测试而设计。框架中所有的核心API和协议都是基于接口的
In the unit test above, you can see an example of how we are injecting a dummy "ViewFactory" implementation on our ProductsController class before calling the Detail() action method. By doing this we are overriding the default ViewFactory that would otherwise handle creating and rendering our View. We can use this test ViewFactory implementation to isolate the testing of just our ProductController's Detail action behavior (and not have to invoke the actual View to-do this). Notice how we can then use the three Assert statements after the Detail() action method is called to verify that the correct behavior occurred within it (specifically that the action retrieved the correct Product object and then passed it to the appropriate View).
在上面的单元测试中,你可以看到我们是如何在调用个ProductsController类的Detail()方法前对“视图工厂(ViewFactory)”的。通过这种方式我们重写了将要处理创建和产生视图的视图工厂。我们可以使用这个视图工厂的
因为在MVC框架中哦我们可以模拟各种对象(包括IhttpRequest和IhttpResponse对象),你不需要在Web Server的环境中进行单元测试,取而代之的是我们可以将ProductController类创建在一个普通的类库项目中并对他进行测试,这可以有效提高单元测试的执行效率,同时简化了其配置和运行过程。
如果我们使用Visual Studio 2008,我们可以轻松的跟踪测试运行的成功/失败状态(该功能已经被集成入VS2008专业版中)。
我想你会发现ASP.net MVC框架式的编写测试更加容易,同时更好地支持了TDD开发流程。
用视图产生UI
我们已经完成并测试了该电子商务程序中产品浏览部分的应用和数据逻辑,现在我们需要实现他的HTML UI界面。
我们将实现当RenderView()方法时被调用是,视图会根据数据ProductController操作方法提供的数据产生相应的界面。
在上面的例子中,RenderView方法的”Categories”参数指明了我们希望用于显示的视图名称,第二个参数是一个目录类的集合,我们将在视图中根据该集合数据产生相应的HTML界面。
ASP.net MVC框架支持多种模板引擎产生UI(包括像Nvelocity, Brail以及自定义的模板引擎),ASP.net MVC框架默认使用ASP.net已经支持的ASPX, MASTER和ASCX。
我们将使用ASP.net内置的视图引擎产生我们的电子商务程序界面。
定义Site.Master文件
因为我们要为站点创建很多页面,我们将要首先为所有的UI定义一个母板页,以便封装常用的HTML布局,我们在、Views\Shared文件夹中创建一个名为”Site.Master”的文件实现该功能。
我们引用一个包含了站点样式的外部CSS样式表文件,并使用母板页定义站点的布局和内容显示区域,在开发时我们可以选择使用所有VS2008设计器中提供的强大功能特性(包括拆分设计视图(HTML split-view designer), CSS编辑 (CSS Authoring), 和嵌入模板支持(Nested Master Pages support)
理解/Views目录结构
当你在Visual Studio中创建ASP.net MVC项目时,它默认会在View根目录下创建一个”Shared”子目录,你可以在这里存放母板页,用户控件和在多个控件中共享的视图。
当创建针对某一个控制类的视图是,ASP.net MVC默认的规则是将它们创建在/Views根文件夹下,子文件夹名称默认与控制类的名称相同。例如,因为我们刚创建的控制类是ProductsController,那么我们默认在\Views\Products子文件夹中存放视图文件。
当我们调用某一控制类中的RenderView(string viewName)方法时,MVC框架在、Views\ControllerName文件夹中自动搜索对应的.aspx或.ascx视图模板文件,如果其中没有对应的视图模板,则将自动转到\Views\Share目录进行搜索。
创建目录视图
我们可以通过在Products目录中通过“添加新项目”菜单项选择”MVC View Page”选项创建ProductsController类的目录视图文件。这将创建一个新的.aspx页面,我们可以设置这个文件与Site.Master母板文件的关联使得他具备站点的一致外观:
在使用MVC模式创建应用程序时,你应当使你的视图代码尽可能简化并完全用于产生视图,而把应用和数据逻辑在控制类中实现。控制类在调用RenderView方法时应向视图中传入用于生成视图的必要数据。例如,下面的ProductController类的Categories方法中我们向视图传入一个Category集合对象:
MVC视图页面默认集成自System.Web.Mvc.ViewPage基类,该积累定义了很多在我们构建UI过程中将要用到的辅助方法和属性,ViewPage的一个属性是ViewData,它提供对控制类从RenderView()方法中传来的数据的访问。
在你的视图中你可以通过迟绑定或强类型的方式访问视图数据(ViewData)。如果你的视图继承自ViewPage类,那么ViewData属性将被设定为迟绑定(late-bound dictionary)类型。如果你的视图继承自泛型ViewPage<T>类,那么ViewData属性将被设定为与控制类传入的参数相同的类型。
例如,我下面的目录视图后台代码继承自ViewPage<T>,同时我指明了T是一个目录对象的集合:
这表明我在访问ProductsController.Categories()方法提供的List<Category>类型的ViewData时具备了完全的类型安全,智能感知和编译时检查的特性:
产生目录视图
或许你还记得这篇文章开始部分的截图,我们希望在目录视图中显示所有的产品目录:
我有如下两种方式在目录视图的实现部分产生HTML代码:1)在.aspx文件中嵌入代码2)在.aspx页面使用服务器控件并在后台代码中实现绑定。
实现方式1:使用嵌入代码
ASP.net页,用户控件和母板页都支持<%%>和<%=%>标记在HTML标签中嵌入代码,我们采用这种技术在目录视图中编写一个”foreach”循环生成HTML格式的目录列表:
VS2008提供了对VB和C#两种语言代码编辑器的完全代码智能感知支持,这意味着我们在访问传入视图的目录对象是将获得智能感知的支持:
VS2008同样提供了对迁入代码的调试支持(允许我们在代码中设置断点):
实现方式2:使用服务器控件
注意上面的代码中,ListView控件是如何封装产生列表值和空列表值(<EmptyDataTemplate>省去了我们编写if/else判断逻辑)两种情况的。我们可以在后台代码中将目录列表绑定到该控件。
重要:在MVC中我们只希望在视图的后台代码中放置显示逻辑(不包括任何其他的业务逻辑),注意上面的大妈中我们唯一的一个逻辑操作时将强类型的目录列表ViewData赋值给ListView控件。我们的ProductController控制类实际承担从数据库中获取目录列表工作而不是视图。
我们的视图模板中的ListView服务器控件将会产生向我们在嵌入代码中编写的一样的HTML标记,因为我们的页面上没有<form runat=”server”>控件,没有ViewStaes视图,ID和其他标记都将排除,只有CSS友好的HTML代码:
HTML.ActionLink方法
或许你已经注意到了在两种方式的代码中都调用了Html.ActionLink方法:
Html对象是ViewPage类及其子类的的辅助属性,ActionLink方法用于产生链接回控制类方法的HTML代码。如果你看到了前一部分的HTML输出图片,你可以看到这个方法输出的HTML代码:
<a href="http://weblogs.asp.net/Products/List/Beverages">Beverages</a>
Html.ActionLink辅助方法的原型如下:
string ActionLink(string text, object values);
第一个参数代表超级链接中间的文字(例如<a>text goes here</a>).第二个参数是一个匿名对象(anonymous object)代表一系列用于产生URL路径的参数(你可以认为他是一种产生字典的有简洁方式)。在后面介绍URL映射引擎的文章中我会进一步介绍它究竟是如何工作的。概括的将,你不但可以使用URL映射系统处理URL路径同时可以生成需要的HTML代码,如果我们有如下的映射规则:
/<controller>/<action>/<category>
我们在ProductController的目录视图中编写下面的代码:
<%= Html.ActionLink("Click Me to See Beverages", new { action="List", category="Beverages" } %>
ActionLink方法将要使用应用程序的URL映射规则将处理你传入的参数并产生输出:
<a href="http://weblogs.asp.net/Products/List/Beverages">Click Me to See Beverages</a>
这使得生成到控制类的URL和AJAX回调变得更加容易,同时意味着你只要修改URL映射规则,就可以让应用程序中所有处理URL请求和产生URL路径的代码发生相应的变更。
重要提示:为了增强可测试性,今天的MVC框架不支持视图中控件的直接回送事件,取而代之的,ASP.net MVC应用程序产生到控制类的超级链接和AJAX回调,然后使用视图(以及其中的其他服务器控件)产生输出。这是的你的视图逻辑更加简洁并专注于控制显示,是应用和数据逻辑与视图分离,你可以更方便的对控制类进行单元测试。后面我将会做详细的介绍。
总结
第一篇文章相当长,旨在对ASP.net MVC框架中各部分之间的关系有一个大致的描述,以及如何利用它创建一个更加普遍并与真实世界相吻合的应用场景,ASP.net MVC第一个预览版本将在几周后发布,你将可以使用他尝试我上面提到的操作。
MVC中的很多概念(尤其是对应用程序各部分分离的思想)或许在很多人看来是很新的,希望这篇文章同时也可以展示我们一直在研究的ASP.net MVC是如何在已有的ASP.net, .NET和Visual Studio基础上分离的。你可以使用.ASPX, . ASCX和.MASTER文件和ASP.net AJAX创建ASP.net MVC视图。ASP.net的非UI特性例如窗体验证,Windows验证,用户权限,角色管理,URL权限设置,缓存,Session, Profile, 运行监控,配置,编译,区域化设置和HttpModules/HttpHanlders仍然被MVC模型支持。
如果你并不喜欢MVC模型或发现他并不符合你的开发习惯,你可以选择不使用它,这完全是一个可选项,并不会取代已有的Web页面控制模型,WebForms和MVC都会被继续被支持和强化功能。你甚至可以创建一个同时兼有WebPage和MVC模型的应用程序。
如果你确实喜欢MVC并希望了解更多的知识,请继续关注后面的文章,我会介绍更多MVC的概念并使用他们完成创建我们的电子商务应用程序以展示MVC的诸多特性。
希望这对你有所帮助,
Scott