引用原文地址:http://weblogs.asp.net/scottgu/archive/2007/11/13/asp-net-mvc-framework-part-1.aspx
来自Scott Gu的blog中ASP.NET MVC系列,以下是译文
ASP.NET MVC Framework (Part 1)
两周之前我写了一个ASP.NET MVC Framework Overview的blog,上面说我们将很快就把它当为一个可选的特性进行支持。它提供了一个结构化的模型,在程序内部实现了清晰的概念分离,而且能够就你的代码进行更容易的单元测试,并支持测试驱动的开发流程(TDD).它还能够帮助你更好的发布你的程序,比如对URL和最终响应的HTML进行更多的控制。
由于我最近一直在回答那些渴望学习更多的人们的很多问题,既然大家给予如此程度的关注,我觉得我应该去写一些新的BLOG来介绍如何去使用这个framework,这第一篇就是近几周以内我会陆续发布的其中之一。
一个简单的电子商店的应用例子
我将会以一个简单的电子商店的应用程序为例,来说明ASP.NET MVC Framework是如何工作的. 今天我将会实现产品的列表/浏览功能
我们将会构建一个商店的web应用程序,当终端用户在这个网站当中访问/Products/Categories的URL的时候,使得他们能够浏览产品种类的列表:

当用户点击如图所示网页中任意产品种类的超链接的时候, 页面将会以/Products/List/CategoryName的URL迁移到某一个产品种类的页面,以列出这个特定的种类下所有的产品。

当用户点击一个单独产品的时候,浏览器将以/Products/Detail/ProductID的URL迁移到某一个产品的详细信息页面- 以显示你所选择的特定产品更多的细节。

我们将会使用新的ASP.NET MVC Framework 来构建以上全部功能.这将使得我们在应用程序的不同组件之间保持“清晰的概念分离”, 使我们更容易地进行单元测试集成和测试驱动的开发
创建一个新的ASP.NET MVC 应用程序
Visual Studio(下载安装了ASP.NET MVC Framework后)包含ASP.NET MVC Project 的模板,能够让我们方便地创建一个新的web应用程序。
在菜单项目里面选择 File->New Project 然后选择"ASP.NET MVC Web Application" 的模板来创建一个新的web应用程序
Visual Studio 将会默认地为你创建一个新的solution 并向内添加两个 project. 第一个project 是你用来实现你的程序的web project. 第二个是能够让你写单元测试代码的测试project:

使用ASP.NET MVC Framework的时候,你能够使用任意的单元测试框架 (包括 NUnit, MBUnit, MSTest, Xunit和其他). 现在VS 2008 Professional 对 MSTest提供了内建测试project的支持, 当你使用VS 2008时,我们默认的ASP.NET MVC project 模板也会自动地创建这些project中的一个。
我们也将会提供对于NUnit, MBUnit 以及其他单元测试框架支持的相干下载, 所以如果你更愿意使用那些的话你也能够通过一个简单的点击来创建你的程序并且快速集成一个你想要的测试project。
理解项目的文件夹结构
ASP.NET MVC 应用程序会默认地创建一个三层目录的结构:
-
/Controllers
-
/Models
-
/Views
就像你所猜到的,我们推荐你把你的控制器类放在/Controllers 目录下, 把你的数据模型类放在/Models目录下, 然后把你的视图模板类放在/Views 目录下.
不过ASP.NET MVC framework 并不会让你总是使用这样的结构,而只是默认的项目模板使用了这种模式,而且我们更推荐它只是由于这种方式能够方便地结构化你的应用程序. 除非你拥有一个足够的理由,那你可以使用一种与此不同的文件布局,否则,我更推荐你使用这种模式.
将你的URL映射到你的控制器类
在大多数的web框架 (ASP, PHP, JSP, ASP.NET WebForms等等)中, 用来访问的URL一般典型地映射你物理存储器上的文件结构,比如说 "/Products.aspx" 或者 "/Products.php" 这样的URL 一般说明你的物理存储器上会有Products.aspx 或者 Products.php 这两个文件来处理这个request。当一个要求访问web应用程序的http request 到达web server的时候, web 框架会执行物理存储器上特定文件的代码,并且这个文件的代码就会全权负责处理这个request. 经常性地,此代码在Products.aspx 或者 Products.php文件内部使用了HTML markup 来帮助生成要返回给客户端的response.
而MVC framework却以另外一种方式来将URL映射到服务器上的代码. 它没有使用将URL映射到实际物理存储器上的文件这一模式,而是直接将URL映射到类. 这些类被称之为"Controllers"(控制器),他们才是接收到来的request,处理用户的输入和互动操作,执行适当的应用程序和在此基础上的数据逻辑的角色, 一个控制器类一般会调用一个单独的"视图"组件,而视图组件正是负责生成实际HTML输出,以响应原本的request.
.
ASP.NET MVC Framework 内置了一个非常强力的URL映射引擎,为你如何映射你的URL到控制器类提供了很大的弹性. 通过使用它,你可以非常方便地建立路由规则, 让ASP.NET在处理接收到的URL请求后能够找出一个特定的控制器去执行. 你还能够让路由引擎自动地转换你在URL中传递的变量.甚至ASP.NET会自动把他们当作声明好的参数传递到你的控制器中去. 我将会在这个系列未来的一篇blog里专门谈一下URL路由引擎的一些深入细节.
ASP.NET MVC中默认的URL到控制器类的路由
默认的ASP.NET MVC project会预先设置了一组URL名字转换规则,让你无需明确地设置任何东西,进而更容易地入门. 当你在Visual Studio 里创建的新的ASP.NET MVC project后,里面生成的Global.asax 文件中会自动声明一套默认的URL名字转换规则,这套规则是以名字为基础的,可以立即供你使用。
根据这套默认的名字转换规则,系统会将接受到http request当中的URL路径 (例如: /Products/)映射到一个名字遵循UrlPathController模式的类(例如: 默认的一个如 /Products/的URL会映射到一个名为ProductsController的类).
为了建立电子商务产品的浏览功能, 我们得往我们得工程里面加入一个名为"ProductsController" 的类 (你可以通过Visual Studio 菜单中的"Add New Item" 来方便地从模板中创建一个控制器类):

我们的ProductsController 类将会继承System.Web.MVC.Controller 这个基类. 当然从这个基类当中派生并不是必需的- 但是它包含了一套有用的辅助方法和功能能让我们在后面的工作中充分利用:

一旦我们在我们的project里面定义了这个 ProductsController 类, ASP.NET MVC framework 将会默认地使用它来处理所有的接收到的以"/Products/" 开头的URL. 这意味着它会自动调用这个控制器类来处理诸如"/Products/Categories", "/Products/List/Beverages", 以及 "/Products/Detail/3" 这些我们将会在我们电子商店应用程序中使用到的URL.
在一篇以后的blog 当中我们也会添加一个ShoppingCartController (来使得终端用户能够管理他们的购物车) 和一个 AccountController (使得终端用户能够在网站创建一个新的会员帐户并且登陆或者登出). 一旦我们把这些新的控制器类加入到我们的project中, 以 /ShoppingCart/ 和 /Account/ 开头的URL将会自动路由到他们来进行处理。
注意: ASP.NET MVC framework 并不要求你总是使用这套名字转换的模式. 我们的程序当中使用它的唯一理由只是它是一套在我们使用Visual Studio来创建ASP.NET MVC Project后就会默认加入到我们project中的映射规则. 如果你不喜欢这套规则,或者你想自定义一个不同的URL映射模式,你可以去ASP.NET Application Class (Global.asax)文件当中修改它. 我将会在以后的一篇blog里面告诉你怎么去做 (当我们向你展示一下URL映射引擎的cool点的时候).
理解控制器的Action方法
现在我们就在我们的project里面创建了一个ProductsController类,我们现在可以开始在应用程序当中添加逻辑来处理接收到的"/Products/" 这个URL了.
就像我们这篇blog开端说的那样,我们打算在这个电子商店网站中实现三种功能: 1) 浏览所有的产品种类, 2) 列出某个特定种类当中的所有产品, 还有3) 显示一个特定产品的详细信息. 我们将会使用以下的URL来代表这些功能:
|
URL格式
|
行为
|
URL 样例
|
|
/Products/Categories
|
浏览所有的产品种类
|
/Products/Categories
|
|
/Products/List/Category
|
列出某个特定种类当中的所有产品
|
/Products/List/Beverages
|
|
/Products/Detail/ProductID
|
显示一个特定产品的详细信息
|
/Products/Detail/34
|
我们有两种方法,在我们的ProductsController类里面编码去处理这些接收到的URL需求. 一种方法是通过重写控制器基类里面的 "Execute" 方法然后手动的编写if/else/switching 逻辑结构来分析访问的URL需求然后选定相应的逻辑代码去处理它(类似于javaservlet的做法).
不过还有一种更加简单的方法就是使用MVC Framework内建的特性,我们可以在我们的控制器中定义"action方法" , 然后让控制器基类根据URL路由规则自动地执行相应的action方法.
举个例子, 我们可以在我们的ProductsController类当中添加以下三个控制器action方法(controller action methods)来处理我们上面说的电子商店功能:

在新的project建立的时候就已经预先设定好的URL路由规则将会把URL的子路径按顺序分割,并将分割后的各个字符串分别当作客户端要求访问的控制器名字和action名字来对待. 所以如果我们收到了一个/Products/Categories的URL Request, 路由规则将会把"Categories" 当作action的名字, 然后将会执行Categories()方法来处理这个request. 如果我们收到一个/Products/Detail/5的URL request , 路由规则将会把"Detail" 当作action的名字, 然后执行Detail()方法来处理这个request.
注意: ASP.NET MVC framework 不要求你总是使用这种action命名转换模式. 如果你想使用不同的URL映射模式 , 到ASP.NET Application Class (in Global.asax)文件里面去修改就可以了.
将URL参数映射到控制器的Action方法
有好几种方法可以在控制器类的action方法里面访问通过URL传递进来的参数.
控制器基类暴露了一组可供使用的Request和Response object. 这些object拥有和传统ASP.NET中你所熟悉的HttpRequest/HttpResponse object相同的API结构. 但是一个重要的不同就是现在这些object都是基于接口的而不是基于密封类的(特殊地: MVC framework 集成了新的System.Web.IHttpRequest 和 System.Web.IHttpResponse接口). 使用接口的好处在于非常方便地可以伪造他们- 这能够使控制器类的单元测试更加方便. 我将会在以后blog当中深入地介绍这些.
下面是一个我们在ProductsController类的Detail action方法中如何手动使用Request API去取得ID参数的例子:

ASP.NET MVC framework还支持自动地把URL传递进来的参数映射到action方法. 在默认情况下,如果你的action方法声明中存在参数, MVC framework 将会在接收到的request数据中查找是否有一个相应的HTTP request值拥有相同的名字. 如果有, 它会自动的将这个值作为参数传给相应的action方法.
例如, 我们可以重写Detail action方法来充分利用这种支持的好处,如下:

除了可以从request的querystring/form 集合中映射参数值外,ASP.NET MVC framework 还允许你改变MVC URL路由映射的内部结构在核心URL内部嵌入你的参数值(比如: 你可以使用/Products/Detail/3来取代/Products/Detail?id=3 ).
当你新建一个MVC项目时候默认的路由映射规则是以"/[controller]/[action]/[id]"格式声明的. 这意味着如果URL中按顺序除去控制器名字和action方法名字之后还有URL子路径的时候,这个子路径将会被默认的当作名为"id" 的参数对待- 这同样意味这它能够作为方法所声明的参数传入我们控制器的action方法.
这也意味着我们现在可以让我们自己的Detail 方法自动从URL取得ID参数 (e.g: /Products/Detail/3):

我可以在listaction方法中使用我所熟悉的方式,这样的话我就能够把种类的名字作为URL的一部分传递进来 (例如: /Products/List/Beverages). 出于让代码可读性更高的目的, 我将会在这个action中使用"category"来代替"id".
下面是ProductsController类的一个实现完整URL路由和参数映射支持的版本:

注意上面List action 是如何把category 参数作为URL的一部分, 然后将一个可选的页面索引作为querystring 的(我们在实现服务器端的分页并使用这个值来表示当前request要求显示哪一页的category数据).
我们MVC framework 中可选的参数将会在控制器action方法中使用nullable类型的参数声明. 因为我们List action方法中页面参数是nullable int类型的(这就是"int?"的含义), MVC framework将在URL当中存在这一参数的时候将它传进来,而不存在的时候就会传一个null进来. 你可以看看我另外一个blog去学习如何使用nullable类型(here).
建立我们的数据模型
我们现在已经拥有了一个ProductsController类以及三个action方法来准备处理接收到的web request. 我们的下一步就是建立一些能够帮助我们和数据库打交道的类来接收需要的数据.
在MVC的世界,模型(models)在程序中是负责保持状态的组件. 而在web应用当中这种状态通常被保存在数据库当中(例如: 我们可以让一个Product object来代表SQL数据库当中名为Products的table).
ASP.NET MVC Framework 使得你可以使用你想要的数据访问方式或者框架来取得和管理你的模型. 如果你想使用 ADO.NET DataSets/DataReaders (或者是建立在他们之上的抽象),OK. 如果你更喜欢使用一种ORM框架,比如NHibernate, LLBLGen, WilsonORMapper, LINQ to SQL/LINQ to Entities ,也没有问题.
为了我们这个电子商店的例子程序我准备使用在.NET 3.5和VS 2008中集成的LINQ to SQL ORM框架. 你可以在下面的blog里面去深入学习 LINQ to SQL :tutorial series ,Part1, Part2, Part3 和Part4.
我将右击VS中我的MVC工程文件"Models" 子目录,选择"Add New Item"选项,添加一个LINQ to SQL模型. 在LINQ to SQL ORM designer当中我定义了三个数据模型类分别映射SQL Server Northwind sample database 当中的Categories, Products, 和 Suppliers 这三个table( Part 2 of my LINQ to SQL series):

一旦我们定义了我们的LINQ to SQL 数据模型类, 我将会在我的Models 目录下添加一个新的NorthwindDataContext的部分类:

在这个部分类中我将会定义一些辅助方法来封装一些LINQ表达式,我们用这些表达式从数据库当中取得产品种类列表,某个特定种类当中的所有产品,以及一个特定产品的详细信息:

这些辅助方法能够使我们方便得取得ProductsController所需要的数据模型object (不用在控制器类内部写LINQ表达式了):

我们现在拥有了ProductsController类功能实现所需要的所有数据code/object.
完成我们的ProductsController类
在一个MVC架构为基础的应用程序中控制器负责处理接收到的request和用户的输入以及互动操作, 并在他们的基础上执行适当的程序逻辑(取得和更新存储在数据库里面的模型数据等等).
控制器一般不为一个request生成特定的HTMLresponse. 生成HTMLresponse的任务是由程序内部的“视图”组件所负责的- 它以与控制器分离的类/模板的方式实现. 视图组件试图将全部焦点集中在封装表现层逻辑上, 并且不包含任何程序逻辑以及从数据库中取得数据的代码(所有的程序逻辑应该在控制器中处理).
在一个典型的MVC web工作流当中, 控制器action方法将会处理接收到的web request, 使用传递进来的参数去执行适当的程序逻辑代码, 取得或者更新由数据库生成的数据模型object, 然后选择一个相应的视图将其作为UIresponse返回给客户端的浏览器. 作为返回视图整体工作的一部分, 控制器会显式地将生成视图response所需要的数据和变量传入视图类:

你可能会问- 像这样将控制器和视图分离的好处是什么? 为什么不将他们放在一个相同的类里面呢? 其主要动机是将你生成UI的代码和你的应用程序/数据逻辑分离.以更便于进行单元测试. 这还能使得你的应用程序更加容易维护- 因为这会使得你在视图模板里添加偶尔的应用程序/数据逻辑变得更加困难.
当实现我们ProductsController控制器内三个action方法的时候, 我们将会使用传入的URL参数来从数据库中取得所需的模型, 然后选择一个视图组件来发送一个相应的HTMLresponse. 我们将会使用控制器基类里面的RenderView() 方法来发送我们想使用的视图, 我们也可以显式地传递我们在发送视图响应时候所需要的数据.
下面是我们ProductsController的最终实现:

注意我们在以上action方法里面所使用代码行数是相当少的 (每个两行). 这部分是由于MVC framework 已经自动为我们处理了URL参数的转换逻辑 (把我们从需要写一大堆代码的困境中解救出来). 另外也是因为从业务的观点来看产品浏览机能也是相对比较简单的(action方法全以只读功能方式显示).
但是在一般情况下,你将会经常发现你的控制器类看起来比较单薄– 意味着控制器方法都是由相对简洁的action方法组成 (少于10行代码). 这通常是一个很好的迹象,能够使得你能够清晰地封装你的数据逻辑.
单元测试我们的ProductsController
你可能会很奇怪我们的下一步居然是测试我们的程序逻辑和功能。你可能会问:这怎么可能?我们还没有实现我们的视图呢,现在我们的应用程序根本不会生成哪怕半点的HTML Tag。好吧,我得告诉你这就是