前几周我发表了一系列文章介绍我们正在研究的ASP.NET MVC框架。ASP.NET MVC框架为你提供了一种新的开发Web应用程序的途径,这种途径可以让应用程序变得更加层次清晰,而且更加有利于对代码进行单元测试和支持TDD(测试驱动开发)开发。
这一些列的第一篇文章创建了一个简单的电子商务产品列表/浏览站点。他涉及到了MVC背后的高层次概念并演示了一个ASP.NET MVC项目从设计到实现的过程和对产品列表功能的测试。该系列的第二篇文章深入介绍了ASP.NET MVC框架的URL映射机制并针对其工作原理和更复杂URL映射的处理进行了深入讨论。
本文将要讨论控制类(英文名称:Controller,以下统一称Controller)如何与视图类(英文名称Views,以下统称Views)进行交互,并专门介绍从Controller到Views传递数据以便给客户端呈现内容的方式。
Part 1 回顾
该系列的第一篇文章我们采用ASP.NET MVC框架创建了一个电子商务站点,实现了基本的产品列表/浏览功能,并把代码自然的划分到不同的Controller,模型(英文名称:Model,以下统一称Model)和View部分。
当浏览器向服务器发送一个HTTP请求时,ASP.NET MVC框架将使用URL映射引擎将该请求映射到controller的操作方法来处理。MVC应用程序中的Controller处理客户端请求,捕获用户输入和进行交互,并执行相应的应用逻辑(获取或更新数据库中的模型数据等)。
当需要向客户端返回HTML回应时,controller针对views进行操作,Views是独立于controller的类或模板,并主要完成显示逻辑的封装。
Views不应该包含任何应用逻辑或数据访问代码,所有的应用/数据逻辑都应该有controller来完成。这样做的好处是让应用/数据逻辑与UI呈现代码有更加清晰的分离,并使得针对应用/数据逻辑的单元测试与UI呈现逻辑相分离。
View应当只根据由Controller针对该View传来的数据生成输出。在ASP.NET MVC框架中,我们把该数据成为视图数据(ViewData)。下面将要介绍将视图数据转换为输出的不同方式。
简单的产品列表场景
让我们常见一个产品列表页来说明我们从Controller向View传递视图数据的技术:
我们将要使用CategoryID整数来过滤出我们希望产生在页面上的产品。注意上面我们是如何把CategoryID作为URL的一部分的(例如: /Products/Category/2或/Products/Category/4)
我们的产品列表页要产生两个独立的动态内容元素,第一部分是我们要显示的目录名称(例如:Condiments),另一部分是一组HTMl <ul><li></li></ul>标记显示产品名称列表。这两部分我都已经在上面的截图中作出红色标记。
下面我们将要分别介绍两种方式,来实现ProductsController类处理请求,获取数据并显示数据的过程。第一种方式我们要使用迟绑定(late-bound)对象,另一种方式我们要使用强类型类(strong-typed)。
方式1:使用Controller.ViewData传递视图数据
Controller基类包含一个名为”ViewData”的属性可以用来存放传递给视图的数据,你可以使用name/value的形式向ViewData中添加对象。
下面的ProductsController类中包含一个Category方法可以实现上面所说的产品列表功能。注意该类是如何使用目录的ID参数来寻找该目录的名称的,以及如何获取该目录中的所有产品。这两部分内容都被存放在Controller.ViewData集合中,并分别使用”CategoryName”和“Products”名称作为主键:
上面的Category方法调用RenderView(“List”)并指明了将要使用哪个视图模板来产生输出,当调用RenderView方法时,Controller将把ViewData传递到View中以便进行显示。
实现View
我们将使用\Views\Products目录下的List.aspx页面文件来实现List视图,该文件将继承\Views\Shared目录下Site.Master母版页的布局(在VS2008中创建新的视图时右单击项目选择”添加新项目”->”MVC视图内容页”选择母版页):
当我们使用MVC 视图内容也模板创建List.aspx页时,并不是从System.Web.UI.Page类继承而来,而是继承自System.Web.Mvc.ViewPage基类(该类是Page类的子类):
ViewPage基类同样为我们提供了ViewData属性,我们可以在View中使用该属性访问由Controller添加的对象,我们可以使用这些数据并通过服务器控件或<%=%>标记产生HTML输出。
使用服务器控件实现View
在下面的例子里我们使用已有的<asp:literal>和<asp:repeater>服务器控件实现HTML 输出:
我们可以采用如下方式通过后台代码类将视图数据绑定到这些控件(注意我们是如何使用视图页的ViewData属性来实现的):
注意:因为页面上没有<form runat=”server”>标记,因此不会生成视图状态(ViewState).上面的控件同样不会自动产生任何ID值,这意味着你将对HTML输出具备完全的控制。
通过<%=%>实现视图
如果你希望使用迁入代码的方式产生输出,你可以用下面的List.aspx页面实现与上面相同的效果:
注意:因为ViewData属性的类型为包含对象(object)的字典,我们需要将ViewData[“Products”]转换成List<Product>类型或Ienumerable<Product>类型来使用foreach语句。在页面上引用System.Collections.Generic和MyStore.Models命名空间避免在List<T>和Product类型中使用完整的类型名称。
注意:上面”var”关键字的使用是在VS2008中使用新的C#和VB”Type inference(字面意思为类型推理)”特性的例子(点击这里阅读相关文章)。因为我们已经将ViewData[“Products”]转换为List<Product>类型,在List.aspx文件中我们将获得product变量的智能感知支持。
方式2:使用强类型(strong-typed)类传递视图数据
除了对迟绑定数据源方式之外,ASP.NET MVC框架同时允许从Controller向View中传递强类型的视图数据对象。使用强类型的方式有如下优点:
- 避免使用字符串来搜索对象,同时对Controller和View代码都可以得到编译时错误检查
- 避免在使用强类型语言(例如C#)时对视图数据进行强制类型转换
- 在视图页的标记和后台代码中可以有代码只能感知支持
- 你可以使用代码反射工具在应用程序和单元测试代码范围内对代码进行自动修改
下面的”ProductsListViewData”强类型类封装了List.aspx视图中用户产品列表的数据,它具有CategoryName和Products属性(通过C#中新的自动属性automatic property支持实现):
修改ProductsController使用该对象将强类型的视图数据传递到视图中:
注意上面我们是向RenderView()方法中传入一个额外的参数将强类型的ProductsListViewData对象传递给视图的。
使用视图的ViewData属性访问强类型的ViewData对象
我们在前面实现的List.aspx视图不需要修改仍可以正常工作。这是因为当强类型的ViewData对象传入继承自ViewPage类的View时,ViewData属性将应用反射机制自动查找该对象的各种属性值,因此我们视图中的代码如下所示:
在RenderView()方法被调用是,上面的代码将会利用反射从我们传入的强类型ProductsListViewData对象中获取CategoryName属性。
对象类型ViewData使用ViewPage<T>基类
为了提供对ViewPage基类的支持,ASP.NET MVC框架同时包含了一个泛型基类ViewPage<T>,如果你的视图继承自ViewPage<T>,其中T代表Controller传入View的ViewData的类型,那么ViewData将被强类型话为指定的类型。
例如,我们可以修改List.aspx.cs文件使其继承自ViewPage<ProductsListViewData>而不是ViewPage基类:
此时该页的ViewData属性将自动定义为ProductsListViewData。这样我们可以使用强类型属性而不是字符串索引的方式获取数据:
然后我们可以使用服务器控件或<%%>的方式生成基于ViewData的HTML输出。
通过服务器控件实现ViewPage<T>
在下面的例子中我们将使用<asp:literal>和<asp:repeater>服务器控件实现HTML UI,与前面继承自ViewPage基类的List.aspx页面相同:
后台代码如下所示。注意因为该类继承自ViewPage<ProductsListViewData>,所以我们可以直接访问属性而不需要任何转换(同时在任何时候我们希望修改属性名称时都可以得到反射工具的支持):
使用<%%>实现ViewPage<T>
如果你希望使用嵌入代码的方式来生成输出,你可以使用下面的代码实现与上面相同的效果:
采用ViewPage<T>的方式我们避免使用字符串来检索视图数据,更重要的是属性成为强类型之后我们就不需要再进行属性的类型转换。这意味着我们可以直接使用foreach (var product in ViewData.Products)语句而不需要转换得到Products。在循环语句product变量将会有完全的智能感知支持:
总结
希望上面的文章已经对Controller向View传递用于呈现的数据的过程有了更加深入的介绍,你可以使用迟绑定的方式或强类型的方式来达到这一目的。
当你第一次创建一个MVC应用程序时,你或许会对将应用控制逻辑与表现代码相互分开的概念感到陌生,你需要一些时间来适应这种接受请求-执行应用逻辑-包装用于显示的视图数据-呈现的处理过程。重要:MVC方式为可选方式,如果你对这种开发模式并不习惯你可以不使用它。
这种方式的优点在于将运行和测试业务逻辑的过程与UI呈现分离,这是的开发单元测试更加简便,同时支持TDD开发模式。后面的文章我将深入介绍这一点,并讨论对代码进行测试的实践。
希望对你有所帮助,
Scott