原址:http://www.asp.net/mvc/tutorials/getting-started-with-aspnet-mvc3/getting-started-with-mvc3-part6-cs
这个教程将会使用Microsoft Visual Web Developer 2010 Express Service Pack 1来教会您构建一个基于ASP.NET MVC Web应用。 在您开始之前,请确保已经安装了下面罗列的必备条件。您可以点击接下来的链接来下载它们:Web Platform Installer。或者您可以使用下面的链接来单个安装:
如果您使用的是Visual Studio 2010, 可以点击接下来的链接来安装这些必备条件: Visual Studio 2010 prerequisites.
在这个Visual Web Developer项目中将会全程使用c#. Download the C# version.。如果您比较擅长VB, 可以在这个教程中改为VB Visual Basic version
在这次章节中,您将会检查一下在movie控制器(Controller)中生成的方法和视图(View)。 之后,您将会添加一个定制的搜索页面。
启动应用和浏览器,在浏览器中工具栏的地址栏中追加/Movies ,然后链接到Movies控制器。将鼠标指针停留在Edit链接上,看看这时显示的URL链接到哪里。
这个Edit链接是Views\Movies\Index.cshtml 视图(View)中通过Html.ActionLink
方法生成的。
@Html.ActionLink("Edit", "Edit", new { id=item.ID })
这个Html对象是一个帮助器,这个帮助器是在WebViewPage
基类中暴露的一个属性。这个帮助器中的ActionLink
方法简单的动态生成了一段HTML代码去链接到控制器(controllers)中的action方法。ActionLink方法的第一个参数是在页面中呈现一段链接文本(例如,<a>Edit Me</a>
) ,第二个参数是需要调用的action方法,最后的一个参数是一个匿名对象 ,用于生成路由数据(在这个列子中,就是ID 4)。
在上一张图片中生成的链接是http://localhost:xxxxx/Movies/Edit/4 。这个默认的路由匹配的URL格式是{controller}/{action}/{id}。因此,ASP.NET解释了http://localhost:xxxxx/Movies/Edit/4
转化成一个请求,带着ID是4的参数链接到Movies控制器(controller)中的一个Edit action方法 。
您也能使用一段查询字符串来传递action方法中的参数。例如,URL http://localhost:xxxxx/Movies/Edit?ID=4 也是传递ID是4的参数到Movies控制器(controller)中的Edit actin方法。
打开Movies控制器(controller)。这两个Edit action方法显示如下。
//
// 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);
}
注意第2个Edit action方法,在方法之前有一个HttpPost
属性。这个属性指定了Edit的重载方法仅仅被一个POST的请求调用。您可以应用HttpGet
属性在第一个edit方法中,但是这不是必须需的,因为默认就是这样。(我们提交的action方法,默认的是在方法上加了HttpGet属性的HttpGet方法。)
这个HttpGet方法拥有一个电影ID参数,使用Entity Framework找到方法查找电影,并且将查询后的电影返回到Edit视图(view)。当这个系统在创建了Edit视图(view)时,它检查了Movie类并且为每个属性创建了<label>
和<input>
标签用于在视图中呈现。下面的例子展现了生成后的Edit视图(view):
@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>
注意在这个视图(view)模板文件的顶部怎么会有一段@model MvcMovie.Models.Movie声明
— 这指明了在视图(view)模板中mode的类型。
这段代码使用了几个帮助器(helper)方法去生成HTML标签。这个Html.LabelFor
帮助器(helper)用来显示字段的名称(Title", "ReleaseDate", "Genre", or "Price")。这个TheHtml.EditorFor
帮助器(helper)显示一段HTML<input>元素。这个Html.ValidationMessageFor
帮助器(helper)用来显示关联属性的验证信息。
启动应用导航到/Movies URL。点击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>
在一段HTML <form>元素中包含了一个<input>元素,它触发一个表单提交,通过<form>元素的action属性将信息透过post的方式请求到/Movies/Edit URL.。当点击Edit按钮的时候,这个表单信息将会发送到服务器。
POST请求过程
下面的监听显示了action方法的一个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 framework模型绑定器(model binder)携带了一个创建的Movie对象(这个对象被当做参数)进行传递。ModelState.IsValid在代码中是用来执行验证,因为数据从表单中提交,时长会修改一个Movie对象,如果这个数据是有效的,这段代码将把电影信息保存到
MovieDBContext实例中,在调用
MovieDBContext中的
SaveChanges方法后,这个新的电影信息将会保存到数据库中。在保存数据以后,这段代码将会重定向用户到
MoviesController类的Index action方法中,然后会更新电影信息,在电影列表中显示
。
如果传递的值无效,将会在表单中重新显示这些值。在Edit.cshtml 视图(view)模板中的Html.ValidationMessageFor
帮助器(helpers)会显示这些错误信息。
让Edit方法变得更加稳健
通过系统自动生成的这个HttpGet Edit方法没有检查ID的有效性。如果一个用户从URL(http://localhost:xxxxx/Movies/Edit)中移除了ID,就会显示下面的错误信息:
一个用户可能也会输入一个数据库中不存在的ID,比如http://localhost:xxxxx/Movies/Edit/1234 。您可以在HttpGet Edit action方法中改变两个地方来改善这个问题。首先,当没有准确的输入一个ID的时候,让ID参数具有一个默认值0,您也能在返回一个movie对象到视图(view)前,通过Find方法检查这个movie是否存在,更新后的Edit方法如下所示。
public ActionResult Edit(int id = 0) { Movie movie = db.Movies.Find(id); if (movie == null) { return HttpNotFound(); } return View(movie); }
如果没有电影被找到,这个HttpNotFound
方法会被调用。
所有的HttpGet方法都有一个相似的模式, 他们获得一个movie对象(或者在Index中,是一个对象列表),并且传递model到视图(view)。这个Create方法传送一个空的movie对象到Create视图(view)。所有的方法(添加, 修改, 删除或其他的数据修改)都是HttpPost重载方法。 通过HTTP GET方法来修改数据会有安全风险,如在ASP.NET MVC Tip #46 – Don’t use Delete Links because they create Security Holes这篇博文中有相关的描述。通过一个GET方法来修改数据也违反了HTTP最佳实践和REST架构。指定GET请求将不会改变您的应用的状态,换句话说,执行一个GET操作是一个没有效果的安全操作。
增加搜索方法和视图(View)
在这个章节中,您将增加一个SearchIndex的action方法,这样就可以根据电影的类型或名称搜索电影,之后Movies/SearchIndex这个URL将会变得可用。
这个请求将会显示一个包含了input元素的HTML表单,用户可以填写信息来搜索一部电影。当一个用户提交了表单,这个action方法将会使用用户输入的信息去数据库中搜索相关的电影,并返回。
显示SearchIndex表单
向已存在的MoviesController类中增加一个
SearchIndex的action方法。这个方法将会返回包含了一段HTML表单的视图(view),下面是具体的代码:
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;
在这个地方的查询表达式,至今还没有跟数据库打交道。
如果这个searchString
参数包含了一段查询所用的字符串,电影查询就要修改成根据搜索的字符串进行筛选,使用下面的代码:
if (!String.IsNullOrEmpty(searchString)) { movies = movies.Where(s => s.Title.Contains(searchString)); }
当定义LINQ查询的时候或者当LINQ查询调用了如Where或者OrderBy语句进行修改时,这些语句还没有被执行,换句话说,LINQ查询是延迟执行的,这意味着要么重复这个表达式或者直到调用了ToList
方法后,才会被执行。在这个SearchIndex示例中,查询是在SearchIndex视图(view)中被执行了。 要查阅更多的延迟查询资料,看看Query Execution。
现在您能够完成SearchIndex视图(view),用来给用户显示表单信息。在这个SearchIndex方法内右键,然后点击Add View。在Add View 对话框中,指定您要传送到视图(view)模板的Movie对象。在Scaffold template 列表中选择List,最后点击Add。
当您单击Add 按钮时,Views\Movies\SearchIndex.cshtml 视图(view)模板被创建了,因为您在Scaffold template 列表中选择了List,Visual Web Developer 会给视图(view)自动的生成一些默认的内容,生成器创建了一个表单,它分析了Movie类,并用代码给每个属性创建了<Label>元素。下面的代码展示了生成后的视图(view):
@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,这个参数就会匹配到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。因此,现在您将要增加UI来帮助他们筛选电影。如果您刚刚为了测试路由规则把参数改成了ID,那么现在还是把参数改回成原来的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
帮助器(helper)创建了一个<form>标签。当用户单击Filter按钮提交表单时,这个Html.BeginForm帮助器(helper)会将表单信息提交给控制器(Cotroller)。
启动这个应用并且尝试搜索一部电影。
这儿没有重载HttpPost的SearchIndex方法,您不需要它,因为这个方法不会改变应用的状态,仅仅是用来筛选数据。
您可以增加下面的HttpPost的SearchIndex方法。在这次示例中,action调用了匹配的
SearchIndex方法,并且这个方法将会显示下面的图片信息。HttpPost的
[HttpPost]
public string SearchIndex(FormCollection fc, string searchString)
{
return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>";
}
然而,即使您给SearchIndex
方法增加了这个HttpPost版本, 有一个疑问,它是怎么被完全执行的, 想象一下,您想要标记一个特殊的搜索或者您想要给一个朋友发送一个链接,这样,他们可以看到一些筛选后的电影列表。注意到通过HTTP POST请求的URL和通过HTTP 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 action方法。
按照类型进行搜索
如果您给SearchIndex
方法增加了一个HttpPost版本,那么,现在删除它。
接下来,您将会增加一个特性,来根据类型搜索电影。把SearchIndex
方法替换成下面的代码:
这个版本的SearchIndex
方法增加了一个参数,参数名称是movieGenre
,代码的第一行创建了一个List对象去存放从数据库中筛选的电影类型。
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));
}
}
下面的代码是用来从数据库中查询出所有类型的LINQ查询语句。
var GenreQry = from d in db.Movies orderby d.Genre select d.Genre;
代码使用了List集合的
AddRange方法去增加所有的不重复的类型到列表中。
(没有这个Distinect修饰语,重复的类型将会被添加—例如,喜剧片在这个示例中将会被添加两次)。接下来的代码将这个筛选后的类型集合存放到ViewBag对象中。
下面的代码展示了怎么检查movieGenre
参数。如果这个参数不是空的,就需要在电影中根据指定的类型进行筛选。
if (string.IsNullOrEmpty(movieGenre)) return View(movies); else { return View(movies.Where(x => x.Genre == movieGenre)); }
在视图(View)中增加标签,来支持根据类型进行搜索
在Views\Movies\SearchIndex.cshtml文件中,在TextBox帮助器(helper)前
增加一个Html.DropDownList
帮助器(helper),完成后的标签如下所示:
<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操作方法并且使用framework来生成视图(view)。您创建了一个搜索的action方法和视图,让用户可以根据电影名称和类型进行搜索。在下一章节中,您将会看到怎么在Movie模型(model)中增加一个属性和怎么增加一个初始化程序去自动的创建一个测试数据库。