第5章 表单和HTML辅助方法
本章内容简介:
* 理解表单
* 如何利用HTML辅助方法
* 编辑和输入的辅助方法
* 显示和渲染的辅助方法
顾名思义,HTML辅助方法是用来辅助HTML开发的。这里可能有一个疑问:诸如向文本编辑器中输入HTML元素如此简单的任务,还需要任何帮助吗?输入标签名称是很容易的事情,但是确保HMTL页面链接中的URL指向正确的位置、表单元素拥有可用于模型绑定的合适的名称和值,以及当模型绑定失败时,其他元素能够显示相应的错误提示消息,这些才是使用HMTL的难点。
实现所有这些方面仅靠HTML标记是远远不够的,还需要视图和运行环境之间的协调配合。学习了本章,就可以很容易地实现他们之间的协调。然而,在学习辅助方法之前,首先要学习表单。应用程序中大部分的困难工作都是在表单中完成的,同事表单也是最需要HTML辅助方法的地方。
5.1表单的使用
您可能会疑惑面向专业Web开发人员的图书为什么还要浪费笔墨讲解HTML的form标签,难道它不容易理解吗?
这样做有两个原因:
* form标签时强大的:如果没有form标签,Internet将变成一个枯燥文档的只读存储库。您将不能进行网上搜索,也不能在网上购买任何东西(甚至是这本书)。如果一个邪恶的神偷今晚盗取了每一个网站的form标签,那么文明将于明天午餐时分消失殆尽。
* 许多转向MVC框架的开发人员都已经使用过ASP.NET Web Forms:Web Forms没有完全利用form标签的强大功能(也可以说是Web Form为实现自己的目标才管理和利用form标签的)。所以应该原谅那些忘记form标签功能(例如创建HTTP GET请求的功能)的Web Forms开发人员。
5.1.1 action和method特性
表单是包含输入元素的容器,其中包含按钮、复选框、文本框等元素。表单中的这些输入元素使得用户能够向页面输入信息,并把输入的信息提交给服务器。但是提交给什么服务器呢?这些信息又是如何到达服务器的呢?这些问题的答案就在两个非常重要的form标签特性中,即action和method特性。
action特性用以告知Web浏览器信息发往哪里,所以action就顺理成章地包含一个URL。这里的URL可以是相对的,但当向一个不同的应用程序或服务器发送信息时,它也可以是绝对的。下面的form标签将可以从任何应用程序中向站点www.bing.com的search页面发送一个搜索词(输入元素的名称为q):
<form action="http://www.bing.com/search"> <input name="q"type="text"/> <input type="submit"value="Search!"/></form>
显而易见,上面代码中的form标签没有method特性。当发送信息时,method特性可以告知浏览器是使用HTTP POST还是使用HTTP GET。现在您可能会认为表单默认的方法是HTTP POST。毕竟经常通过提交表单来更新自己的资料,提交信用卡信息来购物和对YouTube上有趣的动物视频发表评论。然而,尽管如此,默认的方法仍是“get”,所以默认情况下表单发送的是HTTP GET请求。
<form action="http://www.bing.com/search"method="get"> <input name="q"type="text"/> <input type="submit"value="Search!"/></form>
当用户使用HTTP GET请求时,浏览器会提取表单中输入元素的name特性值及其相应的value特性值,并将它们放入到查询字符串中。换句话说,上面的表单将把浏览器导航到URL(假设用户正在搜索关键词love)http://www.bing.com/search?q=love。
5.1.2 GET方法还是POST方法
如果不想让浏览器把输入值放入查询字符串中,而是想放入HTTP请求的主体中,就可以给method特性赋值post。
尽管这样也可以成功地向搜索引擎发送POST请求并能看到相应的搜索结果,但是相对而言,使用HTTP GET请求会更好一些。不想POST请求,GET请求的所有参数都在URL中,因此可以为GET请求建立书签。可以在电子邮件或网页中将这些URL作为超链接来使用,除此之外,还可以保留所有的表单输入值。
更重要的是,因为GET方法代表的是幂等操作和只读操作,所以它是做这些工作的最好选择。换而言之,因为GET不(或应该不)会改变服务器上的状态,所以客户端可以向服务器重复地发送GET请求而不会产生负面影响。
另一方面,POST请求可以用来提交信用卡交易信息、向购物车中添加专辑或者修改密码等。POST请求通常情况下会改变服务器上的状态,重复提交POST请求可能会产生不良的后果(比如购物时,由于重复提交两次POST请求,而产生两个订单)。许多浏览器现在都可以帮助用户避免重复提交POST请求(图 5-1 展示了Chrome浏览器在刷新POST请求时的反应)。
图 5-1
通常情况下,在Web应用程序中,GET请求用于读操作,POST请求用于写操作。为音乐付款就使用了POST请求;像接下来将看到的查询音乐的情形就要使用GET请求。
1. 用搜索表单搜索音乐
假设现在想要让音乐商店的顾客可以在音乐商店应用程序的首页搜索音乐。与前面搜索引擎的例子类似,需要一个带有操作和方法的表单。把下面的代码防止HomeController控制器的Index视图中的促销div下面,这样就完成了所需的表单:
<form action="/Home/Search"method="get"> <input name="q"type="text"/> <input type="submit"value="Search"/> </form>
可以对上面的代码进行各种完善,但现在还是按原计划顺序介绍示例。下一步就是在HomeController控制器中实现Search方法。下面的代码块对音乐搜索做了最简单的假定,假设用户总是用专辑名称来搜索音乐:
publicActionResult Search(string q) { var albums = storeDB.Albums .Include("Artist") .Where(a => a.Title.Contains(q) || q == null) .Take(10); return View(albums); }
注意,这里的Search操作希望接收名为q的字符串参数,当q出现时,ASP.NET MVC框架会自动在查询字符串中找到这个值;即便搜索表单发出的是POST请求而非GET请求,搜索引擎也会在提交的表单中找到这个值。
由控制器告知ASP.NET MVC框架渲染视图,现在就可以在Home视图目录下创建Search.cshtml视图来显示搜索结果:
@model IEnumerable<MvcMusicStore.Models.Album> @{ ViewBag.Title = "Search"; } <h2>Results</h2><table> <tr> <th>Artist</th> <th>Title</th> <th>Price</th> </tr> @foreach(var item in Model) { <tr> <td>@item.Artist.Name</td> <td>@item.Title</td> <td>@String.Format("{0:c}", item.Price)</td> </tr> } </table>
假设顾客在搜索输入框中输入搜索关键字“led”,输出的搜索结果将如图 5-2所示。
图 5-2
上面的搜索示例展示了在APS.NET MVC框架中使用HTML表单的简易性。Web浏览器从表单中收集用户输入信息并向MVC应用程序发送一个请求,这里的MVC运行时可以自动地将这些输入值传递给要响应的操作方法的参数。
当然,并非所有的情形都跟搜索表单一样容易。事实上,刚才是将搜索表单简化到了很脆弱的程度。如果刚才的应用程序部署到一个非网站根目录的目录中,或者修改了路由定义,那么刚才手动编写的操作值可能会把用户的浏览器导航到一个网站上并不存在的资源处。请记住,刚才已经把“Home/Search”赋值给了表单的action特性。
<form action="/Home/Search"method="get"> <input name="q"type="text"/> <input type="submit"value="Search"/> </form>
2. 通过计算action特性值来搜索音乐
更好的办法是通过计算action特性的值来搜索音乐。有一个HTML辅助方法可以代劳这个计算,如下所示。
@using (Html.BeginForm("Search", "Home", FormMethod.Get)) { <inputname="q"type="text"/> <inputtype="submit"value="Search"/> }
BeginForm辅助方法询问路由引擎如何找到HomeController控制器的Search操作。在后台它使用RouteTable中Routes属性上名为GetVirtualPath的方法。如果不采用HTML辅助方法,将不得不编写下面的所有代码:
@{ var context = this.ViewContext.RequestContext; var values = newRouteValueDictionary { {"controller", "home"}, {"action", "index"} }; var path = RouteTable.Routes.GetVirtualPath(context, values); } <form action="@path.VirtualPath"method="get"> <input type="text"name="q"/> <input type="submit"value="Search2"/></form>
最后一个例子展示了HTML辅助方法的本质:它们不是夺去了程序员的控制权,而是让他们从大量的编码工作中解脱出来。
5.2 HTML辅助方法
HTML辅助方法是可以通过视图的Html属性调用的方法。相应的也可以通过Url属性调用URL辅助方法,通过Ajax属性调用AJAX辅助方法。所有这些方法都有一个共同的目标:使视图编码变得更容易。
大部分的辅助方法输出HTML标记,尤其是HTML辅助方法都如此。例如,刚才提到的BeginForm辅助方法就是在为搜索表单而构建强壮的form标签,但这并没有太多的编码:
@using (Html.BeginForm("Search", "Home", FormMethod.Get)) { <input type="text"name="q"/> <input type="submit"value="Search"/}
BeginForm辅助方法很可能会输出与前面第一次实现搜索表单时同样的标记。然而,在后台这个辅助方法与路由引擎协调以生成合适的URL,从而使代码在应用程序部署位置发生改变时更富有弹性。
注意BeginForm辅助方法输出的是起始<form>和结束</form>标签。辅助方法在BeginForm的调用期间生成一个起始标签,并且这个调用返回一个实现了IDisposable的对象。当视图中的代码执行到using语句的结束花括号位置时,此时由于隐式调用了Dispose方法,因此辅助方法会生成一个结束</form>标签。这里using的使用使得代码简洁而优雅。如果发现这样不适合自己,也可以使用下面的方法,这个方法的代码看起来前后对称:
@{Html.BeginForm("Search", "Home", FormMethod.Get);} <input type="text"name="q"/> <input type="submit"value="Search"/> @{Html.EndForm();}
乍一看,辅助方法(比如BeginForm)好像使程序员远离了王牌——许多程序员想控制的低级HTML。一旦开始使用辅助方法,就会意识到它们在使您保持高效率的同时还与王牌保持近距离接触。换句话说,就是可以仍然完全控制HTML而不用编写很多代码来处理细节问题。辅助方法不仅能生成尖括号,还能正确的编码特性,构建指向正确资源的合适URL,设置输入元素的名称以简化模型绑定。总之,辅助方法是程序员的好朋友!
5.2.1 自动编码
想任何其他好朋友一样,HTML辅助方法可以帮助您摆脱困境。将在本章中看到的许多辅助方法都可以用来输出模型值。所有这些输出模型值的辅助方法都会在渲染之前对值进行HTML编码。例如,后面将看到的TextArea辅助方法,用来输出HTML元素textarea:
@Html.TextArea("text","hello <br/> world")
TextArea辅助方法中的第二个参数是要渲染的值。上面的例子是向它的值中嵌入一些HTML标记,但TextArea辅助方法将产生下面的标记:
<textarea cols="20"id="text"name="text"rows="2"> hello < br/> world </textarea>
注意输出值是经过HTML编码的。默认的编码可以帮助避免跨站点脚本攻击(Cross Site Scripting,XSS)。在第7章中将更深一步讲解跨站点脚本攻击。
5.2.2 辅助方法的使用
在保护代码的同时,辅助方法也给出了所需程度的控制。为了展示辅助方法的作用,下面列出了BeginForm辅助方法的另外一个重载版本:
@using (Html.BeginForm("Search", "Home", FormMethod.Get, new { target = "_blank" })) { <input type="text"name="q"/> <input type="submit"value="Search"/> }
在这段代码中,向BeginForm方法的htmlAttributes参数传递了一个匿名类型的对象。几乎ASP.NET MVC框架中的每一个HTML辅助方法在它的某个重载版本中都包含一个htmlAttributes参数。有时可以在不同的重载版本中发现htmlAttributes参数的类型是Idictionary<string,object>。辅助方法采用字典条目(在对象参数的情形下,就是对象的属性名称和属性值)并利用这些条目创建辅助方法生成的元素的特性。例如,上面的代码生成了下面的起始form标签:
<form action="/Home/Search/"method="get"target="_blank">
可以看到已经使用htmlAttributes参数设置了target=“_blank”。事实上,可以使用htmlAttributes参数设置许多必要的特性值。一开始可能会觉得一些特性是有问题的。
例如,设置一个元素的class特性就要求在匿名类型对象上有一个名为class的属性,或者值的字典中有一个名为class的键。在字典中有一个“class”的键值不是问题,问题在于对象中带一个名为class的属性。因为class是C#语言中的一个保留关键字,不能用作属性名或标识符,所以必须在class前面加一个@符号作为前缀:
@using (Html.BeginForm("Search", "Home", FormMethod.Get, new { target = "_blank", @class = "editForm", data_validatable = true }))
将会生成如下的HTML代码:
<form action="/Home/Search" class="editForm" data-validatable="true" method="get" target="_blank">
接下来的一节将阐述辅助方法的工作原理以及其他的一些内置辅助方法。
5.2.3 HTML辅助方法的工作原理
每一个Razor视图都继承了各自基类的Html属性。Html属性的类型是System.Web.Mvc.HtmlHelper<T>,这里的T是一个泛型类型的参数,代表传递给视图的模型类型(默认是dynamic)。这个属性提供了一些可以在视图中调用的实例方法,像EnableClientValidation(选择性的开启或关闭视图中的客户端验证)。然而,上一小节中使用的BegionForm方法并不在这些实例方法之中。事实上,框架定义的大多数辅助方法都是扩展方法。
图 5-3
在只能感知窗口中,当在方法名称左边有一个向下的蓝色箭头(如图5-3所示)时,就说明这个方法是一个扩展方法。从图5-3可以看出,AntiForgeryToken是一个实例方法,BeginForm是一个扩展方法。
为了构建HTML辅助方法体系,扩展方法是一种极其美妙的构建方式,这主要有两个原因。首先,在C#中的扩展方法中只有当在其名称空间范围内,才能调用。ASP.NET MVC所有的HtmlHelper扩展方法都在名称空间System.Web.Mvc.Html中(缘于文件View/web.config中使用的一个名称空间条目,默认情况下都是在该名称空间中)。如果不喜欢这些内置的扩展方法。可以构建自己的扩展方法来代替或增强内置的辅助方法。在第14章中将会学习如何构建自定义的辅助方法。
5.2.4 设置专辑编辑表单
如果需要创建一个视图,用来让用户编辑专辑信息,可以从下面的视图代码开始:
@using (Html.BeginForm()) { @Html.ValidationSummary(excludePropertyErrors:true) <fieldset> <legend>Edit Album</legend> <p> <input type="submit" value="Save"/> </p> </fieldset> }
这段代码中有两个辅助方法:Html.BeginForm和Html.ValidationSummary。下面分别对它们进行介绍,首先从Html.BeginForm开始。
1. Html.BegionForm
前面已经使用了BeginForm辅助方法。在上面的代码中,不带参数的BeginForm辅助方法向当前URL发送一个HTTP POST请求,如果视图响应了/StoreManager/Edit/52,那么起始form标签的代码如下所示:
<form action="/StoreManager/Edit/52" method="post">
在这种情形下,HTTP POST将是理想的请求类型,因为这里将要修改服务器上的专辑信息。
2. Html.ValidationSummary
ValidationSummary辅助方法可以用来显示ModelState字典中所有验证错误的无序列表。使用布尔类型参数(值为true)来告知辅助方法排除属性级别的错误。换言之,就是告诉ValidationSummary方法只显示ModelState中与模型本身有关的错误,而去除那些与具体模型属性相关的错误。这里将分开显示属性级别的错误。
假设在控制器操作中的某处有如下用来渲染编辑视图的代码:
ModelState.AddModelError("", "This is all wrong!"); ModelState.AddModelError("Title", "What a terrible name!");
第一个是模型级别的错误,因为代码中没有提供相关错误与特定属性的键。第二个是与Title属性相关联的错误,因此,在视图中的验证摘要区域不会显示这个错误(除非辅助方法中删除参数“Title”或者把方法ValidationSummary的参数值改为false)。在这种情形下,辅助方法将渲染下面的HTML标记:
<div class="validation-summary-errors"> <ul> <li>This is all wrong!</li> </ul> </div>
ValidationSummary辅助方法的其他重载版本可以提供标题文本,并且跟所有辅助方法一样可以设置特定的HTML特性。
如按照惯例,ValidationSummary辅助方法会将CSS类validation-summary-errors和提供的任何特定CSS类一起渲染。默认的ASP.NET MVC项目模板包含一些样式,用于使这些项以红色显示,如果不喜欢这些样式,可以在文件style.css中进行修改。想了解更多信息的话,请参阅第9章。 |
5.2.5 添加输入元素
一旦表单和验证摘要设计完成,就可以在视图中添加一些输入元素让用户来输入专辑信息。下面的代码展示了其中一种方法(刚开始可以只编辑专辑的标题和流派,但是下面的代码处理的是真实音乐商店的Edit操作)
@using(Html.BeginForm()) { @Html.ValidationSummary(excludePropertyErrors: true) <fieldset> <legend>Edit Album</legend> <p> @Html.Label("GenreId") @Html.DropDownList("GenreId", ViewBag.Genres asSelectList) </p> <p> @Html.Label("Title") @Html.TextBox("Title", Model.Title) @Html.ValidationMessage("Title") <input type="submit" value="Save"/> </p> </fieldset> }
新的辅助方法将向用户展示如下界面(如图 5-4所示):
图 5-4
从上述代码中可以看出,在视图中有4个新的辅助方法:Label、DropDownList、TextBox和ValidationMessage。下面首先介绍TextBox辅助方法。
1. Html.TextBox(和Html.TextArea)
TextBox辅助方法渲染type特性为text的input标签。一般用TextBox辅助方法接收用户自由形式的输入。例如,下面形式的调用:
@Html.TextBox("Title", Model.Title)
会生成如下所示的HTML标记:
<input id="Title" name="Title" type="text" value="For Those About To Rock We Salute You"/>
与每一个其他的HTML辅助方法类似,TextBox辅助方法也为个别的HTML特性设置(正如本章前面展示的)提供了重载。TextBox辅助方法的一个兄弟方法就是TextArea辅助方法。下面的代码展示了使用TextArea方法渲染一个能够显示多行文本的<textarea>元素:
@Html.TextArea("text", "hello <br /> world")
上述代码渲染的HTML标记如下:
<textareacols="20"id="text"name="text"rows="2">hello <br /> world</textarea>
再次注意辅助方法如何将值编码为输出形式(所有的辅助方法都对模型值和特性值进行编码)。TextArea辅助方法的其他重载版本可以通过指定显示的行数和列数控制文本区的大小:
@Html.TextArea("text", "hello <br /> world", 10, 80, null)
这行代码将生成如下所示的HTML标记:
<textareacols="80"id="text"name="text"rows="10">hello <br /> world</textarea>
2. Html.Label
Label辅助方法将返回一个<label/>元素,并用String类型的参数决定渲染的文本和for特性值。这个辅助方法的一个重载版本允许独立地设置for特性和要渲染的文本。在上面的代码中,调用Html.Label(“GenreId”)将生成如下所示的HTML标记:
<labelfor="GenreId">Genre</label>
如果以前没有使用过label元素,那么现在可能极想知道这个元素是否有存在的价值。其实,label的作用就是为其他输入元素(比如文本输入元素)显示附加信息,这样可以为用户提供人性化的界面,从而增强应用程序的可访问性。Label的for特性应该包含相关输入元素的ID(在这个例子的HTML标记中,紧跟其后的输入元素是Genre的下拉列表)。呈现的界面可以用label的文本为用户提供有关输入的更好描述。另外一点,如果用户单击label,那么浏览器会把焦点传送给相关的输入控件。这一点对于复选框和单选按钮特别有用,因为这样可以为用户提供更大的单击区域,而不只是复选框和单选框本身。
细心的读者可能已经注意到label渲染的文本不是“GenreId”(传递给辅助方法的字符串),而是“Genre”。在可能的情况下,辅助方法使用任何可用的模型元数据来生成显示内容。下面探讨表单剩余的其他辅助方法,之后再回到这个主题。
3. Html.DropDownList(和Html.ListBox)
DrowpDownList和ListBox辅助方法都返回一个<select />元素。DropDownList允许进行单项选择,而ListBox支持多项选择(通过在要渲染的标记中将multiple特性的值设置为multiple)。
通常情况下,select元素有两个作用:
*展示可选项的列表
*展示字段的当前值
MVC Music Store中的Album类有一个GenreId属性。可以用select元素来显示GenreId属性的值和所有其他可选项。
由于这些辅助方法都需要一些特定的信息,因此当在控制器中使用它们时,还需要做一点设置工作。下拉列表也不例外,它需要一个包含所有可选项的SelectListItem对象集合,其中每一个SelectListItem对象中又包含Text、Value和Selected三个属性。可以根据需要构建自己的SelectListItem对象集合,也可以使用框架中的SelectList或MultiSelectList辅助方法类来构建。这些类可以查看任何类型的IEnumerable对象并将其转换为SelectListItem对象序列。例如,StoreManager控制器中的Edit操作:
publicActionResult Edit(int id) { var album = storeDB.Albums.Single(a => a.AlbumId == id); ViewBag.Genres = storeDB.Genres .OrderBy(g => g.Name) .AsEnumerable() .Select(g => newSelectListItem { Text = g.Name, Value = g.GenreId.ToString(), Selected = album.GenreId == g.GenreId }); return View(album); }
4. Html.ValidationMessage
当ModelState字典中的某一特定字段出现错误时,可以使用ValidationMessage辅助方法来显示相应的错误提示消息。例如,在下面的控制器中,为了说明问题,故意在模型状态中为Tittle属性添加一个错误
publicActionResult Edit(int id, FormCollection collection) { var album = storeDB.Albums.Find(id); ModelState.AddModelError("Title", "Whate a terrible name!"); return View(album); }
在视图中可以用下面这行代码显示错误提示消息:
@Html.ValidationMessage("Title")
执行后生成的HTML标记如下:
<spanclass="field-validation-error"data-valmsg-for="Title" data-valmsg-replace="true">
What a terrible name!
</span>
这条消息只有当键值“Title”在模型状态中出现错误时才会出现。也可以调用@Html.ValidationMessage的一个重写方法来重写视图中的错误提示消息:
@Html.ValidationMessage("Title","Something is wrong with your titile")
上述代码将渲染的HTML形式为:
<spanclass="field-validation-error"data-valmsg-for="Title" data-valmsg-replace="false">
Something is wrong with your titile
</span>
如按照惯例,当出现错误时,这个辅助方法会将CSS类field-validation-error和提供的任何特定CSS类一起渲染。默认的ASP.NET MVC项目模板自带了一些样式,使得能够以红色显示这些项,如果不喜欢,可以在style.css文件中修改这些样式。 |
到目前为止,已经描述了辅助方法的一些共同的特性,如HTML编码和HTML特性设置,除此之外,当谈到处理模型值和模型状态时,所有的表单输入特性还有一些共同的行为。
5.2.6 辅助方法、模型和视图数据
辅助方法提供了对HTML细粒度控制的同时带走了构建UI(要在合适的位置显示控件、标签、错误消息和值)的乏味工作。辅助方法如Html.TextBox和Html.DropDownList(以及所有其他的表单辅助方法)检查ViewData对象以获得用于显示的当前值(在ViewBag对象中的所有值也可以通过ViewData得到)。
现在先不考虑要创建的编辑表单,而是看一个简单的例子。如果想在一个表单中设置专辑的价格,可以使用下面的控制器代码。
publicActionResult Edit(int id) { ViewBag.Price = 10.0; return View(); }
在相应的视图中,可以通过给TextBox辅助方法赋予与ViewBag中的值相同的名称来渲染用来显示价格的文本框:
@Html.TextBox("Price")
TextBox辅助方法将会生成如下HTML标记:
<inputid="Price"name="Price"type="text"value="10"/>
当辅助方法查看ViewData里面的内容时,它们也能看到其中的对象属性。参照下面代码,修改先前的控制器操作:
publicActionResult Edit(int id) { ViewBag.Album = newAlbum { Price = 11 }; return View(); }
在相应的视图中,可以用下面这行代码来显示一个带有专辑价格的文本框:
@Html.TextBox("Album.Price")
现在渲染出的HTML的标记如下所示:
<inputid="Album_Price"name="Album.Price"type="text" value="11"/>
如果在ViewData中没有匹配“Album.Price”的值,那么辅助方法将尝试与第一点之前的那部分名称(Album)匹配的值。换言之,就是找一个Album类型的对象。然后辅助方法估测名称中剩余的部分(Price),并找到相应的值。
注意渲染得到的input元素的id特性值使用下划线代替了点(但name特性依然使用点)。之所以这样做,是因为在id特性中包含点是非法的,因此,运行时用静态属性HtmlHelper.IdAtrributDotReplacement的值代替了点。如果没有有效的id特性,将无法执行带有JavaScript库(如jQuery)的客户端脚本。
TextBox辅助方法依靠强类型视图数据也能很好的工作。例如,下面代码展示的控制器Edit操作:
publicActionResult Edit(int id) { var album = newAlbum { Price = 12.0m }; return View(album); }
现在回到为TextBox辅助方法提供属性名称来显示信息:
@Html.TextBox("Price")
针对上面的代码,辅助方法将生成如下HTML标记:
<inputid="Price"name="Price"type="text"value="12.0"/>
如果想避免自动地查找数据,可以向表单辅助方法提供一个显示的值。有时,显示提供值的方法是必需的。返回到刚才正在构建(用来编辑专辑信息)的表单。请记住,控制器操作代码如下:
publicActionResult Edit(int id) { var album = storeDB.Albums.Single(a => a.AlbumId == id); ViewBag.Genres = newSelectList(storeDB.Genres. OrderBy(g => g.Name), "GenreId", "Name", album.GenreId); return View(album); }
在Album的强类型编辑视图内部,可以使用下面这行代码为专辑标题渲染一个输入元素:
@Html.TextBox("Title", Model.Title)
方法中的第二个参数显示地提供了数据值。为什么呢?原来在这种情形下,音乐商店的专辑编辑视图像许多其他视图一样,也把页面标题放在了ViewBag.Title属性中,因此Title值已经存储在ViewData中。在Edit视图的顶部可以看到这些:
@{
ViewBag.Title = "Edit - " + Model.Title;
}
应用程序的_Layout.cshtml视图检索ViewBag.Title值以设置渲染页面的标题。如果只向调用的TextBox辅助方法传递字符串,那么它将首先在ViewBag中查找并提取出里面的Title值(辅助方法在查找强类型模型对象之前,会首先查看ViewBag)。因此,就在表单中提供了显式的值。
5.2.7 强类型辅助方法
如果不适应使用字符串字面值从视图数据中提取值的话,也可以使用ASP.NET提供的强类型辅助分类方法。使用这个强类型辅助方法,只需要为它传递一个lambda表达式指定要渲染的模型属性。表达式的模型类型必须和为视图指定的模型类型(使用@model指令)一致。作为一个例子,现在使用下面的代码重写前面的专辑编辑表单(假设视图是带有Album模型的强类型视图):
@using (Html.BeginForm()) { @Html.ValidationSummary(excludePropertyErrors: true) <fieldset> <legend>Edit Album</legend> <p> @Html.LabelFor(m => m.GenreId) @Html.DropDownListFor(m => m.GenreId, ViewBag.Genres asSelectList) </p> <p> @Html.TextBoxFor(m => m.Title) @Html.ValidationMessageFor(m => m.Title) </p> <inputtype="submit"value="Save"/> </fieldset> }
注意,这些强类型的辅助方法名称除了有“For”后缀之外,跟先前使用的辅助方法有相同的名称。尽管该代码生成了与先前代码同样的HTML标记,但是用lambda表达式代替字符串还有很多其他的好处,其中包括智能感知和轻松的代码重构(如果在模型中改变一个属性名称,Visual Studio会自动地修改视图中的对应代码)。一般情况下,可以为处理模型数据的每一个辅助方法找到一个与之对应的强类型方法,在第4章中讲到的内置基架将尽可能地使用这些强类型辅助方法。
注意这里没有显式地为Title文本框设置值,这主要是因为lambda表达式向辅助方法提供了足够的信息,使之能够直接读取模型的Title属性来获取需要的值。
5.2.8 辅助方法和模型元数据
辅助方法不仅查看ViewData内部的数据;它们也利用可得到的模型元数据。例如,专辑编辑表单使用Label辅助方法来为流派选择列表显示一个Label元素:
@Html.Label("GenreId")
这个辅助方法生成如下HTML标记:
<labelfor="GenreId">Genre</label>
文本Genre从哪里来的呢?原来它是当辅助方法询问运行时是否有GenreId的可得模型元数据时,运行时从装饰Album模型的DisplayName特性中获取的信息。
[DisplayName("Genre")]
publicint GenreId { get; set; }
在第4章中讲到的数据注解对很多辅助方法都有很大影响,原因在于当辅助方法构建HTML时要用到注解提供的元数据。模板辅助方法可以更深入地利用元数据。
5.2.9 模板辅助方法
ASP.NET MVC中的模板辅助方法利用元数据和模板构建HTML。其中元数据包括关于模型值(它的名称和类型)的信息和(通过数据注解添加的)模型元数据。模板辅助方法有Html.Display和Html.Editor(以及分别与它们对应的强类型方法Html.DisplayFor和Html.EditorFor)。
例如Html.TextBoxFor辅助方法为某个专辑的Title属性生成以下HTML标记:
<inputid="Title"name="Title"type="text"value="For Those About To Rock We Salute You"/>
如果不使用Html.TextBoxFor辅助方法,也可以用EditorFor方法取而代之:
@Html.EditorFor(m => m.Title)
尽管两种方法生成的是同样的HTML标记,但是EditorFor方法可以通过使用数据注解来改变生成的HTML。顾名思义,从辅助方法的名称Editor来看,就知道它比TextBox辅助方法(暗含了特定类型的输入)应用广泛。当使用模板辅助方法时,运行时就可以生成它觉得合适的任何“编辑器”。下面要在Title属性上添加一个DataType注解:
[Required(ErrorMessage="An Album Title is required")]
[StringLength(160)]
[DataType(DataType.MultilineText)]
publicstring Title { get; set; }
添加之后,EditorFor方法生成如下HTML标记:
<textareaclass="text-box multi-line"id="Title"name="Title">
Let There Be Rock
</textarea>
因为是在一般意义上请求一个编辑器,所以EditorFor辅助方法首先查看元数据,然后推断出应该使用的最好的HTML元素是textarea元素(因为元数据指出了Title属性可以容纳多行文本)。当然,尽管一些艺术家推崇对标题的这一限制,但是大部分专辑标题不需要多行输入。
其他的模板辅助方法包括DisplayForModel和EditorForModel,都是为整个模型对象构建HTML的。使用这些辅助方法,可以为一个模型对象添加新属性,并且在不需要对视图做任何修改的情况下立即在UI中查看修改后的效果。
通过编写自定义的显示或编辑模板可以控制一个模板辅助方法的输出(这是第13章的一个主题)。
5.2.10 辅助方法和ModelState
用来显示表单值的所有辅助方法也需要与ModelState交互。要记住,ModelState是模型绑定的副产品,并且存有模型绑定期间检测到的所有验证错误,以及用户提交用来更新模型的原始值。
用来渲染表单字段的辅助方法自动地在ModelState字典中查找它们的当前值。辅助方法使用名称表达式作为键,在ModelState字典中进行查找。如果查找的值已在ModelState中,辅助方法就用ModelState中的值替换视图数据中的当前值。
模型绑定失败之后,ModelState查找表中允许保存“坏”值。例如,如果用户向DateTime属性的编辑器中输入值“abc”,模型绑定将会失败,并且“abc”也会保存在模型状态的相关属性中。为了在用户修改验证错误而重新渲染视图时,“abc”值将依然出现在DateTime编辑器中,可以让用户看到刚才尝试的错误文本并允许他们改正错误。
当ModelState包含某个属性的错误时,与错误相关的表单辅助方法除了显示地渲染任何指定的CSS类之外,还会渲染input-validation-error CSS类。项目模板包含的默认样式表style.css中包含了类input-validation-error的样式。
5.3 其他输入辅助方法
除了到目前已经谈到的输入辅助方法(如TextBox何DropDownList)之外,ASP.NET MVC框架还包含许多其他的辅助方法,从而涵盖了所有的输入控件。
5.3.1 Html.Hidden
Html.Hidden辅助方法用于渲染隐藏的输入元素。例如,下面这行代码:
@Html.Hidden("wizardStep", "1")
将生成如下HTML标记:
<inputid="wizardStep"name="wizardStep"type="hidden"value="1"/>
这个方法的强类型版本是Html.HiddenFor。假设模型有一个WizardStep属性,可以像下面这样使用它:
@Html.HiddenFor(m => m.WizardStep)
5.3.2 Html.Password
Html.Password辅助方法用于渲染密码字段。它除了不保留提交的值和使用密码掩码之外,基本上与TextBox辅助方法一样。下面的代码:
@Html.Password("UserPassword")
将生成:<inputid="UserPassword"name="UserPassword"type="password" value=""/>
正如期望的那样,Html.Password的强类型方法是Html.PasswordFor。下面的代码展示了如何使用它来显示UserPassword属性:
@Html.PasswordFor(m => m.UserPassword)
5.3.3 Html.RadioButton
单选按钮一般都组合在一起使用,为用户的单项选择提供一组可选项。例如,如果想让用户从一个特定的颜色列表中选择一种颜色,就可以使用多个单选按钮来表示这些颜色选项。对于同一组的单选按钮,可以给每一个按钮起相同的名称。最后提交表单时,只有被选中的单选按钮会被发送到服务器。
下面代码展示了使用Html.RadioButton辅助方法渲染一个简单的单选按钮:
@Html.RadioButton("color", "red")
@Html.RadioButton("color", "blue", true)
@Html.RadioButton("color", "green")
生成的HTML标记如下:
<inputid="color"name="color"type="radio"value="red"/>
<inputid="color"name="color"type="radio"value="blue" checked="checked"/>
<inputid="color"name="color"type="radio"value="green"/>
Html.RadioButton有一个强类型的对应方法Html.RadioButtonFor。强类型方法不是用名称和值,而是用表达式来标识包含要渲染属性的对象,当用户选择单选按钮时,后面会跟要提交的值:
@Html.RadioButtonFor(m => m.GenreId, "1") Rock
@Html.RadioButtonFor(m => m.GenreId, "2") Jazz
@Html.RadioButtonFor(m => m.GenreId, "3") Pop
5.3.4 Html.CheckBox
CheckBox辅助方法是唯一一个渲染两个输入元素的辅助方法。以下面的代码为例:
@Html.CheckBox("IsDiscounted")
<inputid="IsDiscounted"name="IsDiscounted"type="checkbox" value="true"/>
<inputname="IsDiscounted"type="hidden"value="false"/>
这行代码生成的HTML标记如下:
看到上面生成的HTML标记,您可能会有一个疑问:CheckBox辅助方法为什么除了checkbox的输入元素之外,还要渲染一个隐藏的输入元素。其实,它渲染两个输入元素的主要原因是,HTML规范中规定浏览器只要提交“开”(即选中的)的复选框的值。在这个例子中,第二个隐藏输入元素就保证了IsDiscounted有一个值会被提交,即便用户没有选择这个复选框。
尽管许多辅助方法专注于构建表单和表单输入元素,但在一般的渲染场合中还是有可用辅助方法的
5.4 渲染辅助方法
渲染辅助方法可以在应用程序中生成指向其他资源的链接,也可以构建被称作分部视图的可重用UI片段。
5.4.1 Html.ActionLink和Html.RouteLink
ActionLink辅助方法渲染一个指向另外一个控制器操作的超链接(锚标签)。跟前面看到的BeginForm辅助方法一样,ActionLink辅助方法在后台使用路由API来生成URL。例如,当使用链接到同一个控制器中的操作来渲染当前视图时,只需要简单地指定操作的名称:
@Html.ActionLink("Link Text", "AnotherAction")
这里假设采用的是默认路由,那么这行代码将生成下面的HTML标记:
<ahref="/Home/AnotherAction">LinkText</a>
当需要一个指向不同控制器的操作的链接时,可以指定控制器名称作为ActionLink方法的第三个参数。例如,要链接到ShoppingCartController控制器的Index操作,可以使用下面的代码:
@Html.ActionLink("Link Text", "Index", "ShoppingCart")
注意上面指定的控制器名称没有Controller后缀,也就是说没有指定控制器的类型名称。但ActionLink方法有足够的关于ASP.NET MVC控制器和操作的特定知识来理解这是一个控制器名称,刚才已经看到,这些辅助方法提供的重载版本允许只指定操作名称或同时指定控制器名称和操作名称。
在很多应用场合中,路由参数的数量会超过ActionLink方法重载版本的处理能力。例如,可能需要在路由中传递一个ID值或应用程序的其他一些特定路由参数。显而易见,内置的ActionLink辅助方法不能提供处理这些情形的重载版本。
但是,可以通过使用其他的ActionLink重载版本来向辅助方法提供所有必须的路由值。其中一个版本允许向其传递一个RouteValueDictionary类型的对象;另外一个版本允许给routeValues参数传递一个对象(通常是匿名类型的)。运行时将查看该对象的属性并使用它们来构建路由值(属性名称就是路由参数的名称,属性值代表路由参数的值)。例如,为构建一个指向ID号为10720的专辑编辑页面的链接,可以使用下面的代码:
@Html.ActionLink("Edit link text", "Edit", "StoreManager", new { id = 10720 }, null)
上述重载方法的最后一个参数是htmlAttributes。在本章前面部分已经讲解了如何使用这个参数设置HTML元素上特性的值。上面代码传递了一个null(实际上没有设置HTML元素上的任何特性值)尽管上面的代码没有设置任何特性,但是为了调用ActionLink的这个重载方法,必须给这个参数传递一个值。
尽管RoutLink辅助方法和ActionLink辅助方法遵循了相同的模式,但是RoutLink可以接收路由名称而不接收控制器名称和操作名称。例如ActionLink展示的第一个例子也可以用下面的代码实现:
@Html.RouteLink("Link Text", new { action = "AnotherAction" })
5.4.2 URL辅助方法
URL辅助方法与HTML的ActionLink和RouteLink辅助方法相似,但它不是以HTML标记的形式返回构建的URL,而是以字符串的形式返回这些URL。对此,有三个辅助方法:
* Action
* Content
* RoutUrl
Action辅助方法与ActionLink非常相似,但是它不返回锚标签。例如,下面的代码将显示浏览商店里所有Jazz专辑的URL(不是链接):
<span>
@Url.Action("Browse", "Store", new { genre = "Jazz"}, null)
</span>
将生成如下HTML标记:
<span>
/Store/Browse?genre=Jazz
</span>
当在第8章中讲解AJAX技术时,将会看到Action方法的另外一种用法。
RouteUrl辅助方法与Action方法遵循同样的模式,但是与RoutLink一样,它接收路由名称,而不是接收控制器名称和操作名称。
Content辅助方法特别有用,因为它可以将应用程序的相对路径转换成绝对路径。在音乐商店的_Layout视图中可以看到Content辅助方法的效果:
<scriptsrc="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
在传递给Content辅助方法的字符串前面使用波浪线作为第一个字符,这样无论应用程序部署在什么位置,辅助方法都可以让其生成指向正确资源的URL(这里可以将波浪线看作应用程序的根目录)。在不加波浪线的情况下,如果挪动应用程序的虚拟目录在目录树中的位置,生成的URL将是无效的。
5.4.3 Html.Partial和Html.Renderpartial
Partial辅助方法用于将分部视图渲染成字符串。通常情况下,分部视图中包含,在多个不同的视图中可以重复使用的标记。Partial方法一共有4个重载版本,如下所示:
publicstaticMvcHtmlString Partial(thisHtmlHelper htmlHelper, string partialViewName); publicstaticMvcHtmlString Partial(thisHtmlHelper htmlHelper, string partialViewName, object model); publicstaticMvcHtmlString Partial(thisHtmlHelper htmlHelper, string partialViewName, ViewDataDictionary viewData); publicstaticMvcHtmlString Partial(thisHtmlHelper htmlHelper, string partialViewName, object model, ViewDataDictionary viewData);
注意这里没必要为视图指定路径和文件扩展名,因为运行时定位分部视图与定位正常视图的逻辑相同。例如下面的代码就是渲染一个名为AlbumDisplay的分部视图。运行时使用所有的可用视图引擎来查找:
@Html.Partial("AlbumDisplay")
RenderPartial辅助方法与Partial非常相似,但RenderPartial不是返回字符串,而是直接响应输出流。出于这个原因,必须把RenderPartial放入代码块中,而不能放在代码表达式中。为了说明这一点,下面两行代码向输出流写入相同的内容:
@{ Html.RenderPartial("AlbumDisplay"); }
@Html.Partial("AlbumDisplay")
这里,应该使用哪一个方法,Partial还是RenderPartial?
一般情况下,因为Partial相对于RenderPartial来说更方便(不必使用花括号将调用封装在代码块中),所以应该选择Partial。然而,RenderPartial有较好的性能,因为它是直接写入响应流的,但这种性能优势需要大量的使用(高的网站流量或在循环中重复调用)才能看出来。
5.4.4 Html.Action和Html.RenderAction
Action和RenderAction类似于Partial和RenderPartial辅助方法。Partial辅助方法通常在单独的文件中应用视图标记来帮助视图渲染视图模型的一部分。另一方面,Action是执行单独的控制器操作并显示结果。Action提供了更多的灵活性和重用性,因为控制器操作可以建立不同的模型,可以利用单独的控制器上下文。
同样,Action和RenderAction之间仅有的不同是:RenderAction可以直接写入响应流(这可以带来微弱的效率增益)。下面是这个方法用法的快速浏览。假设现在使用的是下面的控制器:
publicclassMyController { publicActionResult Index() { return View(); } [ChildActionOnly] publicActionResult Menu() { var menu = GetMenuFromSomewhere(); return PartialView(menu); } }
Menu操作构建一个菜单模型并返回一个带有菜单的分部视图:
@model Menu <ul> @foreach (var item in Model.MenuItem) { <li>@item</li> } </ul>
在Index.cshtml视图中,可以调用Menu操作来显示菜单:
<html> <head> <title>Index with Menu</title> </head> <body> @Html.Action("Menu") <h1>Welcom to the Index View</h1> </body> </html>
注意Menu操作使用了ChildActionOnlyAttribute特性标记。这个特性设置防止了运行时直接通过一个URL来调用Menu操作。相反,只能通过Action或RenderAction方法来调用Menu操作。ChildActionOnlyAttribute特性不是必须的,但通常在进行子操作时推荐使用。
在ASP.NET MVC 3中,在ControllerContext上也有一个称为IsChildAction的新属性。当通过Action或RenderAction方法调用操作时,它的值就为true;当通过一个URL调用时,它的值就为false。ASP.NET MVC运行时的一些操作过滤器不同于子操作,比如AuthorizeAttribute和OutputCacheAttribute。
1. 给RenderAction传递值
因为这些操作复杂方法调用的是操作方法,所以指定目标操作的一些额外值作为参数是可以的。
例如,假设现在想向菜单中添加一些选项。
(1) 定义新类MenuOptions,代码如下:
publicclassMenuOptions { publicint Width { get; set; } publicint Height { get; set; } }
(2) 修改Menu操作,使其可以作为参数接收MenuOption对象:
[ChildActionOnly] [ActionName("CoolMenu")] publicActionResult Menu(MenuOptions options) { return PartialView(options); }
(3) 在视图中可以通过Action调用传进的菜单选项,代码如下所示:
@Html.Action("Menu", new { option = new MenuOption { Width=400, Height=500} })
2. 与ActionName特性结合使用
需要注意的另外一点是,RenderAction方法优先使用ActionName特性值作为要调用操作的名称。如果按照下面的方式注解操作:
[ChildActionOnly] [ActionName("CoolMenu")] publicActionResult Menu(MenuOptions options) { return PartialView(options); }
那么当调用RenderAction方法时,需要确保操作名称是CoolMenu而不是Menu。
5.5 小结
本章首先介绍了如何为Web应用程序构建表单,而后讲解了如何使用ASP.NET MVC框架中带有的与表单和渲染相关HTML辅助方法。这些辅助方法的目标并不是“拿走”开发人员对应用程序标记的控制权。相反,它们的目标是在项目开发过程中保留对标记的完全控制权的同时,提高开发效率。