ASP.NET MVC3教程
ASP.NET MVC3 电影示例系列:共九篇 [文章列表]
这里的文章是ASP.NET官网的教程,这里做翻译而已,才疏学浅,翻译错误的地方,请指正,原文地址:Examining the Edit Methods and Edit View(C#)
在这节中,你将测试生成的movie控制器行为方法和视图。之后你将添加一个自定义的搜索页面。
运行应用程序,在浏览器的地址栏中的URL后面附加上/Movies来访问Movies的控制器。将鼠标悬停到一个编辑(Edit)连接上查看该链接的URL。
编辑(Edit)链接是Views\Movies\Index.cshtml视图中Html.ActionLink生成的:
@Html.ActionLink("Edit", "Edit", new { id=item.ID })
Html对象是WebViewPage基类通过属性暴露出来的一个帮助器。帮助器的ActionLink方法可以很简单的自动生成链接到控制器中行为方法的HTML超链接。ActionLink方法的第一个参数是要生成链接的文字(例如:<a>Edit Me</a>)。第二个参数是要调用的行为方法名称。最后一个参数是一个生成路由数据的匿名对象(在这里,4是ID)。
前面图中显示的生成链接是http://localhost:xxxxx/Movies/Edit/4。默认的路由使用的URL模式是{controller}/{action}/{id}。因此,ASP.NET将http://localhost:xxxxx/Movies/Edit/4转化为到Movies控制器中Edit行为方法,并带参数id=4的请求。
你也可以通过查询字符串来传递行为方法的参数。比如,URL http://localhost:xxxxx/Movies/Edit?ID=4同样也是将值为4的参数ID传到Movies控制器的Edit行为方法中。
打开Movies控制器。两个Edit行为方法显示如下:
//
// GET: /Movies/Edit/5
public ActionResult Edit(int id)
{
Movie movie = db.Movies.Find(id);
return View(movie);
}
//
// POST: /Movies/Edit/5
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
注意第二个行为方法前面有HttpPost特性。这个特性指明这个重载的Edit方法仅能被POST请求调用。你可以在第一个Edit方法中应用HttpGet特性,但这不是必须的,因为这是默认值。(我们将把那些没有明确指定HttpGet特性的行为方法称作为HttpGet方法1)。
HttpGet Edit方法获取电影的ID参数,使用Entity Framework Find方法来查找电影,并将选中的电影数据返回到Edit视图。当我们的架构系统生成好Edit视图时,会检查Movie类并为类中每个属性生成<label>和<input>的代码。下例显示生成的Edit视图:
@model MvcMovie.Models.Movie
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.ReleaseDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.ReleaseDate)
@Html.ValidationMessageFor(model => model.ReleaseDate)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Genre)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Genre)
@Html.ValidationMessageFor(model => model.Genre)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Price)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Price)
@Html.ValidationMessageFor(model => model.Price)
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
注意观察在试图模板文件头部声明的@model MvcMovie.Models.Movie语句 —— 这指明视图期待在试图模板中model是Movie类型的。
框架代码使用了几个帮助器的方法来简化HTML 标签2。Html.LabelFor帮助器显示字段的名称(“Title","ReleaseData","Genre",或"Price")。Html.EditorFor帮助器生成HTML的<input>元素。Html.ValidationMessageFor帮助器生成属性关联的各种验证信息。
运行应用程序并将URL导航到/Movies。点击编辑(Edit)链接.在浏览器中,查看页面的源代码。页面的HTML代码看起来将和下面示例类似。(为了更清楚,菜单标签已被排除)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Edit</title>
<link href="/Content/Site.css" rel="stylesheet" type="text/css" />
<script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
<script src="/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
<div class="page">
<header>
<div id="title">
<h1>MVC Movie App</h1>
</div>
...
</header>
<section id="main">
<h2>Edit</h2>
<script src="/Scripts/jquery.validate.min.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
<form action="/Movies/Edit/4" method="post"> <fieldset>
<legend>Movie</legend>
<input data-val="true" data-val-number="The field ID must be a number."
data-val-required="The ID field is required." id="ID" name="ID" type="hidden" value="4" />
<div class="editor-label">
<label for="Title">Title</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Title" name="Title" type="text" value="Rio Bravo" />
<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="ReleaseDate">ReleaseDate</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-required="The ReleaseDate field is required."
id="ReleaseDate" name="ReleaseDate" type="text" value="4/15/1959 12:00:00 AM" />
<span class="field-validation-valid" data-valmsg-for="ReleaseDate" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Genre">Genre</label>
</div>
<div class="editor-field">
<input class="text-box single-line" id="Genre" name="Genre" type="text" value="Western" />
<span class="field-validation-valid" data-valmsg-for="Genre" data-valmsg-replace="true"></span>
</div>
<div class="editor-label">
<label for="Price">Price</label>
</div>
<div class="editor-field">
<input class="text-box single-line" data-val="true" data-val-number="The field Price must be a number."
data-val-required="The Price field is required." id="Price" name="Price" type="text" value="9.99" />
<span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
</form>
<div>
<a href="/Movies">Back to List</a>
</div>
</section>
<footer>
</footer>
</div>
</body>
</html>
<input>标签都在<form>的HTML标签内,<form>的action属性设置成了Post到/Movies/Edit的URL。在点击编辑(Edit)按钮时,表单数据将会被Post到服务器。
处理POST 请求
下面清单显示了Edit行为方法的HttpPost版本。
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
ASP.NET 框架模型绑定器获取post回来的表单值,创建一个Movie对象作为movie参数传递。ModelState.IsValid在代码中执行检查,验证提交的表单数据是否可以用于修改Movie对象。如果数据是有效的,代码将电影数据保存到MovieDBContext示例中的Movies集合中。之后代码通过调用将持久化修改到数据库的MovieDBContext中的SaveChanges方法将新的电影数据保存到数据库中。在保存数据之后,代码将用户重定向到MoviesController类中的Index行为方法,这能让更新后的电影数据在电影列表中显示出来。
如果Post的数据无效,他们将会被重新显示在表单中。在Edit.cshtml试图模板中的Html.ValidationMessageFor 帮助器负责处理显示合适的错误信息3。
本地化提示:如果你一般是工作在非英语环境,请查看Supporting ASP.NET MVC 3 Validation with Non-English Locales.
让Edit方法更强壮
通过框架系统生成的HttpGet Edit方法没有检查传给它的ID是否有效。如果用户在URL中移除ID部分,将会报如下错误(http://localhost:xxxxx/Movies/Edit)。
用户同样能够传一个数据库中不存在的ID,比如http://localhost:xxxxx/Movies/Edit/1234。你可以在HttpGet Edit 行为方法中修改两处来解决这个局限性4。首先,设置ID参数,在没有明确传递ID的时候默认值为0。你还可以在在将电影对象返回给试图模板之前检查Find方法实际查找到的电影。更新后的Edit方法如下:
public ActionResult Edit(int id = 0)
{
Movie movie = db.Movies.Find(id);
if (movie == null)
{
return HttpNotFound();
}
return View(movie);
}
如果没有找到电影,将调用HttpNotFound方法。
所有的HttpGet方法都采用类似的模式。他们获取一个电影对象(或者对象列表,本例中Index),传递对象到视图。Create方法将一个空的电影对象传递到Create视图。所有的创建,编辑,删除或另外一些修改数据的方法都是在其方法的HttpPost的重载方法中处理5。在HTTP GET方法中修改数据存在安全风险,描述可参看博客ASP.NET MVC Tip #46 - Don't use Delete Links because they create Security Holes。在GET方法中修改数据同样也违反了HTTP的最佳实践和REST设计模式,REST明确GET请求不应该改变你应用程序的状态。换句话说,执行GET操作应该是一个安全而无副作用的操作。
添加一个搜索方法和搜索视图
在本节中,你将添加一个SearchIndex行为方法以便让你能通过风格和名字来搜索电影。这将使/Movies/SearchIndex URL可以生效。请求将会显示一个包含input元素的HTML表单,让用户可以通过输入来查找电影。当用户点击表单的提交,SearchIndex行为方法将会获取用户Post的查找信息并使用这些信息在数据库中查找。
显示SearchIndex表单
让我们从在已经存在的MoviesController类中添加一个SearchIndex行为方法开始。这个方法将会放回一个包含HTML表单的视图。下面是他的代码:
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
SearchIndex方法的第一行创建如下的LINQ查询来获取电影:
var movies = from m in db.Movies
select m;
这时候查询已经创建,但是还没有链接到数据库中6。
如果searchString参数包含字符串,将用搜索字串的值来修改电影查找的条件,所用代码如下:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
LINQ 查询在他们创建或使用像Where/OrderBy这类方法修改的时候是不会执行的。反而是执行查询被延后,这意味着表达式的求值被推后到真实值被真正遍历或者调用了ToList方法。在这个SearchIndex例子中,查找是在SearchIndex视图中执行的。关于更多延期执行查询,请查看Query Execution(译注:中文版查询执行)
现在你可以来实现展示给用户的SearchIndex视图了。在SearchIndex方法内右键,选择添加视图(Add View)。在添加视图对话框中,指定你将以Movie对象作为model类传递给试图模板。在框架模板(译注:VS中作为支架模板,本处还是作为框架翻译)列表中选择列表,点击添加(Add)。
当你点击添加按钮后,Views\Movies\SearchIndex.cshtml试图模板将被创建。因为你在框架模板(Scaffold template)中选择了列表(List),Visual Web Developer自动在视图中生成(构建)一些默认内容。框架创建一个HTML的表单。它会检测Movie类,并为类中的每个属性生成<label>代码。下面是生成的Create视图代码清单:
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th>
Title
</th>
<th>
ReleaseDate
</th>
<th>
Genre
</th>
<th>
Price
</th>
<th></th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.ReleaseDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.Genre)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
@Html.ActionLink("Details", "Details", new { id=item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id=item.ID })
</td>
</tr>
}
</table>
运行应用程序并导航到/Movies/SearchIndex。在URL后面附加上类似?searchString=ghost的查询字串。就能看到筛选后的电影了。
如果你修改SearchIndex方法的签名,让方法有一个叫id的参数,参数id将会和Global.asax文件中设置的默认路由中的{id}占位符相匹配。
{controller}/{action}/{id}
修改后的SearchIndex方法看起来如下:
public ActionResult SearchIndex(string id)
{
string searchString = id;
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
这样你就可以将搜索标题用路由数据(URL的一部分)来替代查询字串值来传递。
尽管如此,但你不可能期待用户每次都通过修改URL来搜索他们想要的电影。因此,现在你需要添加UI来帮助他们筛选电影。如果你为了测试路由绑定ID是如何传递参数而修改了SearchIndex方法的签名,那么把它改回来,让你的SearchIndex方法包含一个名为searchString的字串型参数。
public ActionResult SearchIndex(string searchString)
{
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
return View(movies);
}
打开Views\Movies\SearchIndex.cshtml文件,在紧接@Html.ActionLink("Create New", "Create")后面添加如下内容:
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString")
<input type="submit" value="Filter" /></p>
}
下例展示了Views\Movies\SearchIndex.cshtml文件添加筛选器标记后的一部分:
@model IEnumerable<MvcMovie.Models.Movie>
@{
ViewBag.Title = "SearchIndex";
}
<h2>SearchIndex</h2>
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm()){
<p> Title: @Html.TextBox("SearchString") <br />
<input type="submit" value="Filter" /></p>
}
</p>
Html.BeginForm帮助器创建一个开放的<form>标签,Html.BeginForm帮助器控制表单,让用户在点击筛选(Filter)按钮提交表单时将表单Post回他本身。
运行应用程序并尝试搜索一个电影。
这里没有HttpPost重载的SearchIndex方法。你不需要HttpPost,因为这个方法不会改变应用程序的状态,而仅仅是筛选数据。
你可以田间一个如下的HttpPost的SearchIndex方法。在这种情况下,行为调用者将匹配HttpPost SearchIndex方法,并且HttpPost的SearchIndex方法运行将会如下图所示的结果。
[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}
然而,即使你添加了SearchIndex方法的HttpPost版本,.不管我们如何实现所有功能,它仍然有局限。试下下你想收藏一个特殊的搜索结果或者你想发送一个链接给你的朋友,以便他们也能通过点击这个链接来看到同样结果的电影筛选列表。留意到HTTP POST 请求使用的URL和GET请求的URL是一样的(localhost:xxxxx/Movies/SearchINdex) —— 在URL中,不包含搜索信息。现在,搜索字串信息是通过表单字段值发送到服务器的。这就意味着你不可能在收藏或者在发送给朋友的URL中获取到搜索信息。
解决方法是使用BeginForm的一个重载来指定POST请求需要添加搜索信息到URL中,并且需要路由到SearchIndex方法的HttpGet版本。用下面的替换当前存在的无参数BeginForm方法:
@using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get))
现在,当你提交搜索的时候,URL包含一个搜索查询字串。即使你已经有一个HttpPost 的SearchIndex方法,查询也将转到HttpGet的SearchIndex行为方法中。
添加通过风格搜索
如果你已经添加了一个HttpPost版本的SearchIndex 方法,现在请删除它。
下一步,你将添加一个让用户通过风格搜索电影的功能。用下面的代码替换SearchIndex方法:
public ActionResult SearchIndex(string movieGenre, string searchString)
{
var GenreLst = new List<string>();
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
GenreLst.AddRange(GenreQry.Distinct());
ViewBag.movieGenre = new SelectList(GenreLst);
var movies = from m in db.Movies
select m;
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
}
这个版本的SearchIndex方法多了一个名为movieGenre的参数。代码的开始几行创建了一个List对象来保存从数据库中拿到的电影风格。
下面的代码是从数据库中获取所有的风格的LINQ查询。
var GenreQry = from d in db.Movies
orderby d.Genre
select d.Genre;
代码使用普通List集合的AddRange方法来添加所有不同的风格到列表中。(如果没有Distinct,会添加到重复的风格 —— 比如,comedy将会在我们的例子中被添加两次)。而后代码将风格的列表保存在ViewBag对象中。
下面的代码展示了如何校验movieGenre参数。如果不为空,代码进一步约束电影查询,限制所选的电影为指定的风格。
if (string.IsNullOrEmpty(movieGenre))
return View(movies);
else
{
return View(movies.Where(x => x.Genre == movieGenre));
}
为SearchIndex 视图添加标签来支持按风格搜索
在Views\Movies\SearchIndex.cshtml文件中,仅挨TextBox帮助器之前田间一个Html.DropDownList帮助器。完整的标签如下:
<p>
@Html.ActionLink("Create New", "Create")
@using (Html.BeginForm()){
<p>Genre: @Html.DropDownList("movieGenre", "All")
Title: @Html.TextBox("SearchString")
<input type="submit" value="Filter" /></p>
}
</p>
运行应用程序并且浏览到/Movies/SearchIndex。测试按风格,按名称,和按名称和风格搜索。
在本节中,你测试了框架生成的CRUD行为方法和视图。你创建了一个能让用户通过电影标题和风格来搜索的搜索行为方法和视图。在下一节中,你将会看到如何为Movie模型添加一个属性和如何添加一个能自动创建测试数据库的初始化器。
MitchellChu 译注参考说明:
1. We'll refer to action methods that are implicitly assigned the HttpGet attribute as HttpGet methods.这里的refer to...as是作为称...为...,把...作为...的意思来翻译。
2. streamline:把...简化
3. take care of :处理
4. to address sth.:解决某事。 limitation:局限性
5. do so 不会翻译,这里作为处理,其逻辑意思应该是:这些操作的处理逻辑应该放在HttpPost特性方法中。
6. The query is defined at this point, but hasn't yet been run against the data store.这句翻译可能有误,对于run against把握不好。