【原文地址】ASP.NET MVC Framework (Part 4): Handling Form Edit and Post Scenarios
【原文发表日期】 Sunday, December 09, 2007 4:42 AM
过去的几个星期内,我一直在写着讨论我们正在开发的新ASP.NET MVC框架的系列贴子。ASP.NET MVC框架是个你可以用来结构化你的ASP.NET web应用,使之拥有清晰的关注分离,方便你单元测试代码和支持TDD流程的可选方法。
这个系列的第一篇建造了一个简单的电子商务产品列表/浏览网站。它讨论了MVC后面的高层次的概念,示范了如何从头创建一个新的ASP.NET MVC项目,实现和测试这个电子商务产品列表功能。系列的第二篇对ASP.NET MVC框架的URL路径选择(routing)架构做了深入探讨,讨论了它的工作原理以及你如何使用它来处理更高级的URL路径选择场景。 第三篇讨论了控制器是如何与视图做交互的,特别地讨论了你可以把视图数据从控制器传给视图以显示返回到客户端的回复的各种方法。
在今天的帖子里,我将讨论你可以用MVC框架来处理表单输入和提交场景的各种方法,以及讨论一些你可以用来简化数据编辑场景的HTML辅助方法。点击这里下载我们将在下面为解释这些概念而建造的完整的应用的源代码。
为示范如何在ASP.NET MVC框架中处理表单输入和提交场景的一些基本原则,我们将建造一个简单的产品列表,产品生成,和产品编辑场景。它将拥有三个核心的用户体验:
按类列出的产品列表
通过导航到/Products/Category/[CategoryID] 这样的URL,用户将能看到在某个特定产品分类内的所有产品的列表:
添加新产品
用户将能通过点击上面的“添加新产品”的链接往商店里添加一个新产品。点击之后,会转到/Products/New URL,在这里,系统将提示用户输入要添加的新产品的细节:
在点击Save(保存)之后,产品就会添加到数据库中,然后就会转向返回到产品列表网页。
编辑产品
在产品列表网页上,用户可以点击每个产品旁边的“Edit”(编辑)链接。这会转到/Products/Edit/[ProductID] URL,在这里,用户可以改动产品的细节,然后点击Save按钮,往数据库里更新:
我们将使用SQL Server Northwind样品数据库来存储我们的数据。然后我们将使用.NET 3.5内置的LINQ to SQL对象关系映射器(ORM)来对Product, Category, 和 Supplier对象进行建模,这些对象代表了我们的数据库数据表中的记录行。
一开始,在ASP.NET MVC项目中,右击/Models子目录,选择“添加新项” -> “LINQ to SQL 类”,调出 LINQ to SQL ORM 设计器来对我们的数据对象建模:
然后我们将在项目中创建一个NorthwindDataContext部分类(partial class),向里面添加一些辅助方法。我们定义这些辅助方法有2个原因: 1)避免在我们的Controller类中直接嵌入我们的LINQ查询,2) 将允许我们在将来更容易地改变我们的控制器以使用dependency injection(依赖注入)。
我们将添加的NorthwindDataContext辅助方法是象下面这样的:
想进一步了解LINQ和LINQ to SQL的话,请参阅我这里的LINQ to SQL系列。
我们将使用单一控制器类来实现这三个核心用户浏览体验,我们将称这个控制器类为“ProductsController”(在Controllers子目录上右击,选择“添加新项” -> “MVC 控制器”来创建这个类:
我们的 ProductsController 类将通过实现"Category", "New", 和"Edit" 等action方法来处理象/Products/Category/3, /Products/New, 和/Products/Edit/5这样的URL:
想了解这些URL是如何导向到 ProductsController 类的action方法上的话,请阅读我的ASP.NET MVC系列的第一部分和第二部分。在本文的例子里,我们将使用默认的/[Controller]/[Action]/[Id]路径映射规则,这意味着我们不必配置什么东西,路径导向就会自动发生。
我们控制器的Action方法将使用三个视图网页,用以显示输出。"List.aspx", "New.aspx", 和 "Edit.aspx" 网页将居于 \Views\Products 子目录下,这些网页将基于\Views\Shared目录中的Site.Master母版页上。
我们要实现的网站的第一部分将是产品列表URL (/Products/Category/[CategoryId]) :
我们将使用我们的ProductsController类上的"Category" action方法来实现这个功能。我们将使用LINQ to SQL DataContext类,和我们往其中添加的GetCategoryById辅助方法,来获取一个Category对象,该对象代表了由URL (譬如, /Products/Category/3) 指定的某个特定分类。然后我们将该Category对象传给"List"视图来从中生成回复:
在实现我们的List视图时,我们首先将更新我们网页的后台代码,从ViewPage<Category>继承而来,这样页面的ViewData属性将是从我们的控制器传过来的Category对象的类型(第三部分对此有详细讨论):
然后我们将象下面这样实现List.aspx:
上面的视图在页面上方显示了分类名称,然后显示了分类内的所有产品的项目列表。
在项目列表的每个产品旁边,有个 "Edit" 链接。我们是用在第二部分中讨论过的Html.ActionLink辅助方法来显示这些HTML超链接(譬如,<a href="/Products/Edit/4">Edit</a>)的,在"Edit"链接被点击后,用户将被导向到 "Edit"action方法。然后我们还将使用Html.ActionLink辅助方法在页面底部生成一个<a href="/Products/New">Add New Product</a>链接,在该链接被点击后,用户将被导向到"New" action方法。
当我们访问 /Products/Category/1 URL时,在浏览器中查看源码的话,你会注意到我们的ASP.NET MVC应用输出了非常干净的HTML和URL标识:
现在让我们来实现网站的“添加新产品”表单提交功能,最终我们想要用户在访问/Products/New URL时看到象下面这样的显示:
在ASP.NET MVC框架中,表单输入和编辑场景一般是通过在Controller类上呈示2个Action方法来处理的。第一个Controller Action方法负责发送含有要显示的初始表单的HTML。第二个Controller Action方法则负责处理从浏览器发回的任何表单提交。
例如,对上面的“添加产品”屏幕,我们会选择在ProductsController上的2个不同action中来实现:一个叫"New",另一个叫"Create"。/Products/New URL负责显示一个带有HTML文本框和下拉框控件的空白表单,让用户输入新产品的细节。然后,这个网页上的HTML <form>元素将其action属性设置为 /Products/Create URL。这意味着当用户点击表单提交按钮时,表单的输入将被发送到"Create" action方法上来处理和更新数据库。
下面是我们可以用来实现ProductsController的一个初始实现。
注意上面,在涉及产品生成过程中,我们有2个action方法, - "New" 和 "Create"。 "New" action方法只是简单地向用户显示一个空白表单。"Create" action方法则处理从表单提交过来的值,根据这些值在数据库中生成一个新产品,然后将客户转向到产品的分类列表网页。
发送到客户端的HTML表单,是在由"New" action方法调用的"New.aspx"视图里实现的。这个视图的一个初始实现(每个输入都用了文本框)看上去象下面这样:
注意上面,我们在网页上使用了标准的 HTML <form> 元素,而不是form runat=server。表单的"action"属性被设置为ProductsController上的"Create" action方法。在页面底部的<input type="submit">元素被点击时,提交就会发生,之后,ASP.NET MVC框架就会自动将ProductName, CategoryID, SupplierID 和 UnitPrice值映射为方法参数,传给ProductsController上的 "Create" action方法:
至此,我们运行网站时,就有了最基本的产品输入功能:
我们在前面一节里创建的产品输入屏幕是可行的,但不是很友好。具体来说,它要求用户知道正输入的产品的原始CategoryID和SupplierID成员。我们需要通过显示内含可读名称的HTML下拉框来修正这个问题。
第一步,将修改ProductsController来向视图里传人2个集合,一个内含现有的分类列表,另一个内含产品供应商列表。我们将通过生成一个封装这些列表的强类型的ProductsNewViewData类,然后将它传给视图来达成这个目的(你可以在第三部分中了解有关详情):
然后我们将更新 "New" action 方法来填充这些集合,然后将它们作为ViewData传给 "New" 视图:
然后在我们的视图里,我们可以使用这些集合来生成 HTML <select> 下拉框。
ASP.NET MVC HTML 辅助方法
我们可以用来生成下拉框的一个方法是在HTML里手工生成内含 if/else 语句的 <% %> for-循环。这会给与我们对HTML的完全控制,但HTML会很乱。
一个你可以使用的干净得多的方法是利用ViewPage基类上的"Html"辅助属性。这是个方便对象,呈示了一套HTML辅助界面方法,用于自动化 HTML界面的生成。例如,在本帖子的前面,我们使用了 Html.ActionLink辅助方法来生成 <a href=""> 元素:
HtmlHelper对象(以及我们将在以后的教程里讨论的AjaxHelper对象)是特地设计可以通过使用"扩展方法"(VS 2008中VB和C#的一个新语言特性)来轻松地扩展的。这意味着,任何人都可以为这些对象生成他们自己的自定义辅助方法,共享这些方法,为你所用。
在ASP.NET MVC框架将来的预览版中,我们将提供几十个内置的HTML和AJAX辅助方法。在第一个预览版中,只有"ActionLink"方法是内置于 System.Web.Extensions(目前实现核心ASP.NET MVC框架的程序集)中的。但我们还将有一个单独的 "MVCToolkit" 下载,你可以加到你的项目中,来得到你可以在第一个预览版中使用的的几十个辅助方法。
要安装MVCToolkit HTML辅助方法的话,只要将MVCToolkit.dll程序集添加为你的项目的引用即可:
重新编译你的项目,然后下一次你键入 <%= Html. %> 的话,你将看到许许多多你可以使用的额外界面辅助方法:
为生成HTML <select>下拉框,我们可以使用Html.Select()方法。每个方法都有重载的版本,在视图里有完整的intellisense:
我们可以更新我们的"New"视图,用下面的代码,使用Html.Select选项来显示使用CategoryID/SupplierID属性作为值,CategoryName/SupplierName作为显示文字的下拉框:
这会在运行时为我们生成适当的<select> HTML标识:
在/Products/New屏幕上给用户一个方便的方式来选择产品分类和供应商:
注: 因为我们还是在向服务器提交CategoryID和SupplierID值,所以我们根本不用更新ProductsController的Create Action方法来支持这个新的下拉框界面,这个方法还是工作的。
我们的ProductsController的"Create" Action方法负责处理我们的“添加产品”场景的表单提交。目前它是以action方法参数的方式来处理进来的表单参数的:
这个方法是可行的,但对于涉及大量值的表单,Action方法的签名就会开始变得有点难读。而且,上面将所有进来的参数值设置到新的Product对象上的代码有点长,而且单调。
如果你引用了MVCToolkit程序集,你可以利用在System.Web.Mvc.BindingHelpers命名空间下实现的一个有用的扩展方法,来对此代码作些清理。这个扩展方法叫做“UpdateFrom”,可以用在任何 .NET 对象上。它接受一个字典作为参数,然后,它会对任何匹配该对象的公开属性的键,自动对本身进行属性赋值。
例如,我们可以重写我们上面的Create action方法,来使用UpdateFrom方法,象这样:
注: 如果你因为安全的原因,想要更明确些,只允许某些属性可以更新的话,你还可以向UpdateFrom方法传入一个可以更新的属性名称的字符串数组:
现在让我们来实现网站“编辑产品”的功能。我们最终想要用户在访问/Products/Edit/[ProductID] URL时看到象下面这样的屏幕:
跟上面的“添加新产品”表单提交例子一样,我们将使用2个ProductsController Action方法来实现这个表单编辑交互,我们将称这2个方法为"Edit"和"Update":
"Edit" 会显示产品表单,"Update"会被用来处理表单的提交行动。
我们将通过实现ProductController的Edit action方法来开始启用我们应用的编辑功能。当我们在本贴子的开头创建产品列表网页的时候,我们是这么建造的,Edit action将接受一个作为URL一部分的id参数(譬如,/Products/Edit/5):
我们想要Edit Action方法从数据库中获取适当的产品对象,以及现有的产品供应商和分类集合(这样,我们可以在我们的编辑视图里实现这些东西对应的下拉框)。我们将使用下面的ProductsEditViewData对象来定义一个强类型的视图对象来代表所有这些数据:
然后,我们可以实现我们的Edit action方法来填充这个viewdata对象,在"Edit" 视图中显示:
我们可以使用下述方法来实现Edit.aspx视图网页:
注意我们是如何同时使用上面例子中的Html.TextBox和Html.Select辅助方法来的。这2个方法都是来自MVCToolkit.dll程序集中的扩展方法。
注意Html.Select辅助方法有个重载版本,允许你指定下拉框中的选定值是什么。在下面的代码片断中,我表示我要Category下拉框根据编辑产品目前的CategoryID值自动选择某一项:
最后,注意我们是如何使用Url.Action()辅助方法来设置<form>元素的action属性的:
Url.Action和Html.ActionLink这2个辅助方法都使用了ASP.NET MVC框架的路径选择引擎来生成URL(参阅第二部分以了解URL生成原理的细节)。这意味着,如果我们改变我们网站的编辑功能的路径选择规则的话,我们不需要改动控制器或视图中的任何代码。例如,我们可以将我们的URL做重新映射,换掉/Products/Edit/1,而是使用象/Products/1/Edit这样更具RESTful的URL的话,上面的控制器和视图代码不用做改动,而依旧会工作。
最后一步是实现ProductController类上的"Update" action方法:
跟前面的"Create" action方法一样,我们将利用"UpdateFrom"扩展方法来从请求中自动填充我们的产品对象。但注意,填充的不是一个空对象,我们使用了一个模式,先从数据库中获取老的值,然后对它应用用户做的改动,然后更新到数据库中。
编译完毕之后,我们重新定向到产品列表网页,自动设置 /Products/Category/[CategoryID],以匹配我们正在操作的产品的保存的状态。
希望本帖子提供了在ASP.NET MVC框架中如何处理表单输入和提交场景的一些细节,还提供了你可以如何处理和结构化常见数据输入和编辑场景的一些背景。
点击这里下载一个内含我们在上面建造的完整应用源代码的.ZIP 文件。
在将来的帖子里,我将讨论如何处理表单输入和编辑场景中数据验证和错误复原的情形。我将讨论一些促进快速应用开发的内置的数据和安全支架(scaffolding)。我将讨论你如何在MVC框架中使用ASP.NET AJAX进行启用AJAX的编辑。我还将对如何单元测试控制器和向控制器添加依赖注入做深入的探讨。