这里有一个疑问,诸如在文本编辑器中输入 HTML 元素如此简单的任务,也需要任何帮助吗?的确,输入标签名称是很容易的事,但是确保 HTML 页面链接中的 URL 指向正确的位置、表单元素拥有适用于模型绑定的名称和值、以及当模型绑定失败时其他元素能够显示相应的错误提示消息,这些才是 HTML 的繁杂点。
ASP.NET Web Forms 并没有完全利用 form 标签的强大功能。表单中的这些输入元素是如何被提交到服务器的呢?
action 特性告知 Web 浏览器信息发往哪里,所以它顺理成章的包含一条 URL。这里的 URL 是相对的,但当向一个不同的应用程序或服务器发送信息时,它也可以是绝对的。下面的 form 标签可以从任何应用程序向站点 www.bing.com 的 search 页面发送一个搜索词:
<form action="http://www.bing.com/search">
<input name="q" type="text" />
<input type="submit" value="Search" />
</form>
method 特性可以告知浏览器是使用 GET 还是 POST 方式发送请求。上面示例中并没有指定 method 特性值,而从结果可以验证,是以查询字符串方式进行请求,因此 method 默认值为 HTTP GET。
POST 方式也可以把数据提交到服务器,表单值并不会显示在 URL 上,因此 GET 请求可以为结果页面建立书签。通常,应根据实际应用的语意来进行选择,GET 请求应用于读操作(R),而 POST 请求应用于写操作(CUD)。
假设现在音乐商店要实现搜索专辑,代码可能会如下:
<form action="/Home/Search" method="get">
<input name="q" type="text" />
<input type="submit" value="Search" />
</form>
考虑一下,如果把应用程序部署到一个非网站根目录的目录中,或者修改了路由定义,那么手动编写的操作值可能会把用户的浏览器导航到并不存在的资源处。更好的方法是通过计算 action 的值来实现这一 URL,有一个 HTML 辅助方法可以代劳自动完成这个计算:
<!--参数序列:action、Controller、method-->
@using (Html.BeginForm("Search", "Home", FormMethod.Get))
{
<input name="q" type="text" />
<input type="submit" value="Search" />
}
通过视图的 Html 属性可以调用 HTML 辅助方法;通过 Url 属性可以调用 URL 辅助方法;通过 Ajax 属性可以调用 AJAX 辅助方法。所有这些方法的目的都是为了使视图编码变得更容易。
大部分的辅助方法都输出 HTML 标记,尤其是 HTML 辅助方法。例如,前面的示例 Html.BeginForm 可以构建一个强壮的表单标签。在后台,该辅助方法与路由引擎协调工作来生成合适的 URL,从而当应用程序部署位置改变时,代码更富有弹性。
我们知道,using 语句括号中的对象在语句块结束后会自动释放,其实是调用了对象的 Dispose 方法,任何实现了 IDisposeable 接口的对象都能在 using 语句块中得到释放。辅助方法在调用 BeginForm 期间生成了一个其实标签 <form>,并返回了一个实现了 IDisposeable 接口的对象(MvcForm),当执行到 using 语句结束的花括号时,隐式调用了该对象的 Dispose 方法,因此辅助方法会生成一个结束标签 </form>。using 语句使得代码看起来较为优雅,否则你需要这样书写:
@{
Html.BeginForm("Search", "Home", FormMethod.Get);
<input name="q" type="text" />
<input type="submit" value="Search" />
Html.EndForm();
}
本篇所介绍的许多辅助方法都可以用来输出模型值,这些输出模型值的辅助方法都会在渲染前,对值进行 HTML 编码。
@Html.TextArea("text", "hello <br /> world");
输出值是经过 HTML 编码的,默认的编码可以帮助避免跨站点攻击(XSS,Cross Site Scripting)。
<textarea cols="20" id="text" name="text" rows="2">
hello <br /> world
</textarea>
辅助方法也给出了适度的控制,下面是一个 BeginForm 的重载版本:
@using (Html.BeginForm("Search", "Home", FormMethod.Get, new { target = "_blank" }))
{
<input name="q" type="text" />
<input type="submit" value="Search" />
}
这段代码中,第 4 个参数 htmlAttributes 接受一个匿名对象,在 MVC 框架的重载版本中,几乎每一个 HTML 辅助方法都包含 htmlAttributes 参数。
有时也会发现在某些重载版本中,htmlAttributes 参数的类型是 IDictionary<string,object>,辅助方法用字典条目(在对象参数的情形下,就是对象的属性名和属性值),创建辅助方法生成的元素特性。有时,这会有些问题,例如要求匿名对象必须有一个 class 的属性,在字典中有一个“class”的键值不是问题,而对象不行,因为 class 是 C# 语言的关键字,必须加上“@”符号前缀:
@using (Html.BeginForm("Search", "Home", FormMethod.Get,
new { target = "_blank", @class = "editForm" }))
{
<input name="q" type="text" />
<input type="submit" value="Search" />
}
另一个问题是将属性设置为带有连字符的名称,例如像 data-val,带有连字符的 C# 属性名是无效的,不过,所有 HTML 辅助方法在渲染 HTML 时会将属性名中的下划线转换为连字符:
@using (Html.BeginForm("Search", "Home", FormMethod.Get,
new { target = "_blank", @class = "editForm", data_validatable = true }))
{
<input name="q" type="text" />
<input type="submit" value="Search" />
}
将会产生这样的 HTML 代码:
<form action="/Home/Search" class="editForm" data-validatable="True" method="get" target="_blank">
<input name="q" type="text" />
<input type="submit" value="Search" />
</form>
每一个 Razor 视图都继承了它们基类的 Html 属性。Html 属性的类型是 System.Web.Mvc.HtmlHelper<T>,这里的 T 是一个泛型类型的参数,代表传递给视图的模型类型,默认为 dynamic。
这个属性提供了一些可以在视图中调用的实例方法,比如 EnableClientValidition(开启或关闭视图中的客户端验证)。然后,先前介绍过的 BeginForm 方法并不在其中,事实上,框架定义的大多数辅助方法都是扩展方法。
扩展方法是一种极其每秒的构建方式,这主要有 2 个原因。
首先,在 C# 的扩展方法中只有当在它的名称空间范围内,才能调用。ASP.NET MVC 中所有的 HtmlHelper 扩展方法都在名称空间 System.Web.Mvc.Html 中。(缘于文件 Views/web.config 中使用的一个命名空间条目,如果不喜欢这些内置的扩展方法,可以删除这个命名空间,构建自己的方法)
然后,“构建自己的方法”带来了第 2 个好处,我们可以构建自己的扩展方法来代替或增强内置的辅助方法,之后的系列会介绍如何构建自定义辅助方法。
Html.ValidationSummary(true)
ModelState.AddModelError("", "This is all wrong!");// 模型级别错误,因为不关联属性或是空值
ModelState.AddModelError("Title", "What a terrible name!"); // 属性级别错误,这里具体到 Title
用来显示 ModelState 字典中所有验证错误的无序列表,true 用来告知辅助方法排除属性级别的错误。
一些常用的 Html 辅助方法,且各自有一些重载的版本可对标签属性进行详细的设置:
@using (Html.BeginForm())
{
@Html.ValidationSummary(true);
<fieldset>
<legend>Edit Album</legend>
<p>
@Html.Label("GenreId")
@Html.DropDownList("GenreId", ViewBag.Genres as SelectList)
</p>
<p>
@Html.Label("Title")
@Html.TextBox("Title", Model.Title)
@Html.ValidationMessage("Title")
</p>
<input type="submit" value="Save" />
</fieldset>
}
辅助方法提供了对 HTML 细粒度控制的同时还带走了构建 UI(在合适的位置显示控件、标签、错误消息和值)等工作。它会检查 ViewData 对象以获得要显示的当前值。
看一个简单的后台代码及前台源文件,证明辅助方法会查看 ViewData 中的数据,它们也能看到对象属性。
public ActionResult Edit(int? id)
{
ViewBag.Price = 10.0;
return View();
}
@Html.TextBox("Price")
<input id="Price" name="Price" type="text" value="10" />
再看这段代码:
public ActionResult Edit(int? id)
{
ViewBag.Album = new Album { Price = 11 };
return View();
}
@Html.TextBox("Album.Price")
<input id="Album_Price" name="Album.Price" type="text" value="11" />
如果在 ViewData 中没有匹配“Album.Price”这样的键,辅助方法将尝试查找与第一个“.”之前那部分名称匹配的键。换言之,就是查找一个名为 Album 的对象,然后辅助方法估测名称中剩余的部分(Price),并找到相应的值。
注意,input 元素的 id 特性值使用了下划线代替了点,但 name 特性依然使用点。之所以这样做,是因为在 id 特性中包含点是非法的,因此,运行时用静态属性 HtmlHelper.IdAttributeDotReplacement 的值代替了点。如果没有有效的 id 特性,就无法执行带有 JavaScript 库(如jQuery)的客户端脚本。
有时,显式的提供数据是一种比较好的选择,看下面代码:
public ActionResult Edit(int? id)
{
ViewBag.Album = new Album { Title = "Do what?" };
return View();
}
@Html.TextBox("Title")
<input id="Title" name="Title" type="text" value="Edit" />
没有如预料中的输出“Do what?”,原来,这种情况下页面顶部的标题已经使用了 Title 这个键!可使用对象.属性名避免这类问题,或者显式的提供值:
@Html.TextBox("Album.Title")
或者:
public ActionResult Edit(int? id)
{
var album = new Album { Title = "Do what?" };
return View(album);
}
@model MvcMusicStore.Models.Album
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@Html.TextBox("Title", Model.Title)
在大型应用程序中,为了更清晰的确定在哪里使用数据,需要在一些视图数据项前添加前缀,例如,不把主页标题命名为 ViewBag.Title,而是命名为注入 ViewBag.Page_Title,这样就避免了与特定页面的明明冲突。
特别注意:以下这条非强类型辅助方法的代码是无法获取正确的属性值的!这是因为该辅助方法需要在视图数据 ViewData 中查找 key,而 Model.Title 虽然也返回字符串字面值,但本例中的 Title 值为“Caravan”,显然,视图数据中是不具备这样的 key 的!
@Html.Editor(Model.Title)
如果不适应使用字符串字面值从视图数据中提取值的话,也可以使用 ASP.NET MVC 提供的强类型辅助方法。这种强类型辅助方法只需传递一个 lambda 表达式来指定要渲染的模型属性,表达式的模型类型必须和为视图指定的模型类型(@model 指令指定的模型)一致。
@model MvcMusicStore.Models.Album
@{
ViewBag.Page_Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Album</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.AlbumId)
<div class="form-group">
@Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("GenreId", string.Empty)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ArtistId, "ArtistId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("ArtistId", String.Empty)
@Html.ValidationMessageFor(model => model.ArtistId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
</div>
......
}
这些强类型辅助方法和之前的辅助方法除了有“For”后缀之外,名称皆相同。尽管生成了同样的 HTML 标记,但强类型还有许多其他的好处,包括智能感知、编译时检查错误、轻松的代码重构!(如果在模型中更改了属性名,VS 会自动修改视图中的对应代码)。注意,这里并没有显式的提供值,因为 lambda 表达式提供了足够的信息,强类型辅助方法能够正确的读取模型的 Title 属性来从视图数据中获取 Title 的值。
辅助方法也可以查看模型元数据。当询问运行时(runtime)是否有 GenreId 的可用模型元数据时,运行时会从装饰 Album 模型的 DispalyName 特性中获取信息。
@Html.Label("GenreId")
[DisplayName("Genre")]
public virtual int GenreId { get; set; }
ASP.NET MVC 中的模板辅助方法利用元数据和模板构建 HTML。其中元数据包括模型值(它的名称和类型)的信息和(通过数据注解或自定义提供器添加的)模型元数据。模板辅助方法有:
例如,使用 Html.TextBoxFor 辅助方法为某个专辑的 Title 属性生成以下 HTML 标记:
<input id="Title" name="Title" type="text" value="Caravan" />
现在换成 Html.EditorFor 辅助方法,也可以完成同样的工作:
@Html.EditorFor(model => model.Title)
<input class="text-box single-line" id="Title" name="Title" type="text" value="Caravan" />
尽管两种方法生成的是同样的 HTML 标记,但是 EditFor 方法可以通过使用数据注解来改变生成的 HTML。顾名思义,就知道它比 TextBox(指明了是 type = text) 辅助方法应用更为广泛!当使用模板辅助方法时,运行时就可以生成它觉得合适的任何“编辑器(Editor)”
下面在 Title 属性上添加一个 DataType 注解,EditorFor 生成了一个 textarea 文本框,而这些变化是在没有修改视图代码下完成的:
[DisplayName("Genre")]
public int GenreId { get; set; }
<textarea class="text-box multi-line" id="Title" name="Title">Caravan</textarea>
因为,一般意义上请求一个编辑器,EditorFor 会先查看元数据,然后推断出最合适的 HTML 元素。
ASP.NET MVC 还包含许多其他的辅助方法,它们涵盖所有的输入控件:
@Html.Hidden("wizardStep", 1)
@Html.HiddenFor(m => m.WizardStep)
@Html.Password("pwd")
@Html.PasswordFor(u => u.Pwd)
<!-- 单选按钮一般会组合使用 -->
@Html.RadioButton("color", "red")
@Html.RadioButton("color", "blue", true)
@Html.RadioButton("color", "green")
@Html.RadioButtonFor(m => m.GenreId, "1") Rock
@Html.RadioButtonFor(m => m.GenreId, "2") Jazz
@Html.RadioButtonFor(m => m.GenreId, "3") Pop
@Html.CheckBox("IsCar")
@Html.CheckBoxFor(m => m.IsCar)
<!--
CheckBox 方法是唯一一个渲染两个输入元素的辅助方法:
<input id="IsCar" name="IsCar" type="checkbox" value="true" />
<input name="IsCar" type="hidden" value="false" />
主要原因在于,HTML 规范中规定浏览器只提交“选中”的复选框的值,第二个隐藏于就保证了 IsCar
至少有一个值会被提交,即便用户没有选择这个复选框。即若选中,第一个为 checked,IsCar被提交
至服务器;若不选中,复选框虽然不会被提交至服务器,但隐藏于 IsCar 且值为 false 一样会被提交
-->
渲染辅助方法可在应用程序中生成指向其他资源的链接,也可以构建被称为部分视图的可重用 URL 片段。
ActionLink 能够渲染一个超链接,渲染的链接指向另一个控制器操作,它也是使用路由 API 来生成 URL:
<!-- 当链接的操作所在控制器与渲染当前视图的控制器一样时,只需指定操作的名称 -->
@Html.ActionLink("Link Text", "AnotherAction")
<!-- 当需要指向不同控制器操作的链接时,可通过第三个参数指定控制器的名称,不需要 Controller 后缀-->
@Html.ActionLink("Link Text", "AnotherAction", "AnotherController")
URL 辅助方法与 HTML 的 ActionLink、RouteLink 辅助方法类相似,但它不是以 HTML 标记的形式返回构建的 URL。
Action 辅助方法不返回锚标记,下面的代码会显示浏览商店里所有 Jazz 专辑的 URL(不是链接):
<span>
@Url.Action("Browser", "Store", new { Genre = "Jazz" }, null)
</span>
<span>
/Store/Browser?Genre=Jazz
</span>
RouteUrl 辅助方法与 RouteLink 一样,但只接收路由名称,而不接收控制器名称和操作名称。
Content 辅助方法可以把应用程序的相对路径转换成绝对路径。
Partial 辅助方法可以将部分视图渲染成字符串。通常,部分视图中包含多个在不同视图中可重复使用的标记,这是为了 HTML 代码的重用!(你可以当作 Web 组件)。
没必要为视图名指定路径和文件扩展名,因为运行时会使用所有的可用视图引擎来查找,例如:
@Html.Partial("AlbumDisplay")
RenderPartial 辅助方法与 Partial 非常相似,但它并不是返回字符串,而是直接写入响应输出流。出于这个原因,它必须被放置在代码块中,而不能放在代码表达式中。
@{Html.RenderPartial("AlbumDisplay");}
@Html.Partial("AlbumDisplay")
那到底应该使用哪一个?应该选择 Partial,因为它使用起来更方便。尽管后者拥有更好的性能(因为直接写入响应流),但这需要大量使用(高的网站流量或者高数量的循环中)才能提现的出来。
类似于 Partial 和 RenderPartial 辅助方法。 Partial 辅助方法通常在单独的文件中应用视图标记来帮助视图渲染视图模型的一部分。另一方面,Action 执行单独的控制器操作,并显示结果。Action 提供了更多的灵活性和重用性,因为控制器操作可以建立不同的模型,可以利用单独的控制器上下文。
下面是这个方法用法的简单介绍。假设现在使用的是如下的控制器:
public class MyController : Controller
{
public ActionResult Index()
{
return View();
}
[ChildActionOnly] // 该特性用于指示操作方法只应作为子操作进行调用
public ActionResult Menu()
{
var menu = GetMenuFromSomeWhere();
return PartialView(menu);
}
}
Menu操作构建一个菜单模型,并返回一个带有菜单的部分视图:
@model Menu
<ul>
@foreach (var item in Model.MenuItem)
{
<li>@item.Text</li>
}
</ul>
在 Index.cshtml 视图中,可以调用 Menu 操作来显示菜单,这就实现了动态的菜单,菜单选项的增减将不再需要改动任何代码:
<header>
@Html.Action("Menu")
</header>
<h1>Welcome to the Index View</h1>
ChildActionOnly 特性标记可以有效避免运行时直接通过 URL 来调用 Menu 操作,相反,只能通过 Action 或 ActionRender 来调用子操作,虽然这不是必须的,但通常在进行子操作时推荐这样做。
ASP.NET MVC 在 ControllerContext 上添加了一个新属性 IsChildAction,当通过 Action 或 ActionRender 调用操作时,它的值就为 True,通过 URL 访问时它的值就为 False。