ASP.NET MVC框架(第三部分): 把ViewData从控制器传到视图
【原文地址】ASP.NET MVC Framework (Part 3): Passing ViewData from Controllers to Views
【原文发表日期】 Thursday, December 06, 2007 2:49 AM
【译注】根据Scott Guthrie原文的回复,ASP.NET MVC框架的第一个CTP将于12月7日发布
过去的几个星期内,我一直在写着讨论我们正在开发的新ASP.NET MVC框架的系列贴子。ASP.NET MVC框架是个你可以用来结构化你的ASP.NET web应用,使之拥有清晰的关注分离,方便你单元测试代码和支持TDD流程的可选方法。
这个系列的第一篇建造了一个简单的电子商务产品列表/浏览网站。它讨论了MVC后面的高层次的概念,示范了如何从头创建一个新的ASP.NET MVC项目,实现和测试这个电子商务产品列表功能。系列的第二篇对ASP.NET MVC框架的URL路径选择(routing)架构做了深入探讨,讨论了它的工作原理以及你如何使用它来处理更高级的URL路径选择场景。
在今天的帖子里,我将讨论控制器是如何与视图做交互的,具体来说,我将讨论你可以把数据从控制器传到视图以显示返回到客户端的回复的各种方式。
第一部分的扼要简述
在这个系列的第一部分,我们创建了一个电子商务网站,实现了基本的产品列表/浏览支持。我们是用ASP.NET MVC框架实现这个网站的,这个方法会很自然地将代码结构化为独特的控制器,模型和视图组件。
当浏览器向我们的网站发送一个HTTP请求时,ASP.NET MVC框架将使用它的URL路径选择引擎,把进来的请求映射到一个控制器上的action方法来处理它。在基于MVC的应用中的控制器负责处理进来的请求,处理用户输入和交互,执行基于这些输入和交互的应用逻辑(获取或更新存储在数据库中的模型数据等等)。
到生成返回到客户端的HTML回复的时候,控制器一般是与“视图”组件合作,这些视图组件是以独立于控制器的单独的类或模板的形式实现的,其目的是完全注重于封装显示逻辑。
视图不应该含有任何应用逻辑或数据库访问代码,所有的应用/数据逻辑应该由控制器类来处理。这么划分的动机是帮助强制你的应用/数据逻辑与界面生成代码间的清晰分离。同时这也方便你独立于你的界面显示逻辑来单元测试你的应用/数据逻辑。
视图应该只使用从控制器传过来的特定于视图的数据来生成输出。在ASP.NET MVC框架中,我们称这个特定于视图的数据为“ViewData”。这个博客的其他部分将讨论你可以用来将ViewData从控制器传递给视图来生成显示的一些不同方法。
一个简单的产品列表场景
为帮助说明我们可以用来把ViewData从控制器传递给视图的一些技术,让我们来建造一个简单的产品列表网页:
我们将用一个CategoryID整数来过滤我们想要显示在页面上的产品。注意上面我们是如何把CategoryID嵌在URL中的(例如,Products/Category/2 或 /Products/Category/4 )。
然后,我们的产品列表网页显示了2个不同的动态内容元素。第一个元素是我们要显示的分类的文本名称(例如,Condiments-调味品),第二个元素是一个HTML <ul><li/></ul> 产品名字列表。我在上面的屏幕截图中对这2个元素用红笔画了圈。
在下面,我们将看一下我们可以使用的2个不同的方法来实现ProductsController类,这个类处理进来的请求,获取处理请求所需的数据,然后将这个数据传给一个List视图来显示。我们要研究的第一个方法是用后期绑定的字典对象传递这个数据,第二个方法则使用强类型类的方式来传递这个数据。
方法 1:使用 Controller.ViewData 字典来传递ViewData
Controller基类有个ViewData字典属性,可以用来填充你要传给视图的数据。你使用键/值模式将对象加入 ViewData 字典。
下面是个ProductsController类,其中的Category action方法实现了我们上面的产品列表场景。注意,它是如何使用分类的ID参数来查询该分类的文本名称,以及获取该分类中的产品列表的。它使用 “CategoryName”和“Products”两个键将这两个数据存储在Controller.ViewData 集合中:
然后,我们上面的Category action方法调用 RenderView("List") 来表示它要用哪个模板来做显示。当你象这样调用RenderView时,它会将ViewData字典传给视图,以显示对应的回复。
实现我们的视图
我们将使用居于我们项目的\Views\Products目录下的List.aspx文件来实现我们的List视图。这个 List.aspx 将继承 \Views\Shared 文件夹中的Site.Master母版页中的布局(在你创建一个新的视图网页时,你可以在 VS 2008 中,右击,选择添加新项->MVC视图内容网页来接连一个母版页):
当我们使用MVC视图内容网页模板来创建List.aspx网页时,它不是从通常的 System.Web.UI.Page 类继承而来,而是从System.Web.Mvc.ViewPage 基类继承而来(是现有的Page类的一个子类):
ViewPage基类提供一个ViewData字典属性,我们可以在视图网页里访问由控制器添加的数据对象。然后我们可以取出这些数据对象,使用它们来显示HTML输出,可以用服务器控件的方式,或者用 <%= %> 显示代码的方式。
使用服务器控件来实现我们的视图
下面是一个如何使用现有的<asp:literal> 和 <asp:repeater>服务器控件来实现我们的HTML界面的例子:
我们可以用下面的后台代码类将 ViewData 绑定到这些控件之上(注意我们是如何使用ViewPage的ViewData字典来实现的 ):
注: 因为页面上没有 <form runat="server">,是不会输出 view-state 的。上面的控件也不会自动生成任何ID值,这意味着你对输出的HTML有完全的控制。
使用 <%= %> 代码来实现我们的视图
如果你更喜欢使用行内代码来生成输出的话,你可使用下面的 List.aspx 来实现跟上面完全一样的结果:
注:因为ViewData的类型是含有“objects”的字典,为了对它使用foreach语句,我们需要将ViewData["Products"]的类型转换成 List<Product> 或者 IEnumerable<Product>。我在页面上引用了System.Collections.Generic 和 MyStore.Models 命名空间 以避免输入 List<T> 和 Product 类型的完整名称。
注: 上面使用了“var”关键词,这是VS 2008中新的 C# 和 VB “类型推断”特性的一个例子(在这里阅读我以前的相关贴子)。因为我们将ViewData["Products"] 转换成了 List<Product>,我们在 List.aspx 文件中的 prduct 变量上得到了完整的intellisense:
方法 2:使用强类型类来传递ViewData
除了支持后期绑定的字典方法外,ASP.NET MVC框架还允许你把强类型的ViewData对象从控制器传递给你的视图。使用这个强类型的方法有几个好处:
- 避免使用字符串来查询对象,得到对你的控制器和视图代码的编译时检查
- 避免需要在使用象C#这样的强类型语言中明确转换ViewData对象字典中的值
- 在你的视图网页的标识文件以及后台代码文件中得到你的ViewData对象的自动代码intellisense
- 可以使用代码重构工具来帮助自动化对整个应用和单元测试代码库的改动
下面是一个强类型的ProductsListViewData类,封装了 List.aspx 视图显示我们的产品列表所需的数据,它含有 CategoryName 和 Products 属性(是通过使用新的C#自动属性支持来实现的):
然后我们可以更新我们的 ProductsController 实现来使用这个对象,把一个强类型的ViewData对象传给我们的视图:
注意上面,我们是如何通过 RenderView() 方法的一个额外的参数,把我们的强类型 ProductsListViewData 对象传给View的。
把视图的ViewData字典与强类型的ViewData对象一起使用
前面我们编写的 List.aspx 视图实现会继续和我们更新过的 ProductsController 协作,不需改动代码。这是因为,当把一个强类型的 ViewData 对象传递给继承自 ViewPage 的视图类时,ViewData 字典会自动使用反射对强类型的对象的属性做查询取值。所以我们象下面这样的视图中的代码:
会自动使用反射来从强类型的 ProductsListViewData 对象中获取 CategoryName 属性,这个对象是我们在调用 RenderView 方法时传入的。
使用ViewPage<T>基类来对ViewData强类型化
除了支持基于字典的ViewPage基类外,ASP.NET MVC框架中还发布有基于泛型的 ViewPage<T> 实现。如果你的视图是从 ViewPage<T> 继承而来,这里T表示是控制器传给视图的 ViewData 的类型,那么 ViewData 属性就将是使用了这个T类的强类型属性。
例如,我们可以更新我们的 List.aspx.cs 后台代码类,不是从ViewPage继承来,而是继承自 ViewPage<ProductsListViewData> :
这么做之后,页面上的 ViewData 属性将会从一个字典变成属于 ProductsListViewData 类型。这意味着,我们现在可以不再使用基于字符串的字典来查阅获取数据,而是可以使用强类型的属性了:
然后,我们可以使用服务器控件的方法,或者 <%= %> 显示的方法来生成基于这个ViewData的HTML。
使用服务器控件来实现 ViewPage<T>视图
下面是一个例子,我们可以使用<asp:literal> 和 <asp:repeater>服务器控件来实现我们的HTML界面。这是我们使用继承自 ViewPage 的 List.aspx 网页时所使用的完全一样的标识:
下面是相应的后台代码。注意,因为我们是从 ViewPage<ProductsListViewData> 继承而来的,我们可以直接访问它的属性,而不要对任何东西做类型转换(什么时候我们决定对其中一个属性改名的话,我们还将得到重构工具的支持):
使用 <%= %> 代码实现我们的 ViewPage<T> 视图
如果你更喜欢使用行内代码来生成输出的话,你可以象下面这样在 List.aspx 中达成跟上面一样的结果:
使用 ViewPage<T> 方法,我们现在不再需要对 ViewData 使用字符串查阅了。更重要的是,注意上面,我们不再需要对任何属性做类型转换了,因为它们已经是强类型的。这意味着,我们可以编写 foreach (var product in ViewData.Products) ,而不用对 Products 做类型转换。我们还在循环中的 product 变量上得到了完整的intellisense:
结语
希望本贴子提供了关于控制器如何把数据传递给视图以显示返回到客户端的回复的一些细节。你可以使用后期绑定的字典,或者使用强类型的方式来达成这个目的。
第一次试着建造MVC应用时,你很可能发现把应用控制器的逻辑和生成界面的代码分离开来的概念有点怪。你大概要花上一段专门的时间来多建造些应用,你才会感到习惯,把自己的思路转向到处理一个请求,执行所有的应用逻辑,把建造界面回复所需的 viewdata 包装起来,然后交给单独的一个视图页面去显示的观念上去。 重要事项:如果这个模型对你来说并不感觉舒服,那么别用它,MVC的方法纯粹是可选的,我们并不认为这是每个人都想要用的东西。
但这个划分应用的好处以及其后的目标在于,它允许你独立于你的界面显示代码,来运行和测试你的应用和数据逻辑。这极大地方便你为你的应用开发全面的单元测试,以及在建造应用时使用TDD(测试驱动开发)的流程。在以后的贴子里,我会对此做更深入的讨论,以及讨论你可以用来轻松测试代码的最佳实践。