【翻译】ASP.NET MVC4 入门(六)仔细查看一下Edit方法和Eidt视图
在这一小节,我们将会仔细查看一下为MoviesController自动生成的action和view。然后我们会添加一个自定义的查找页面。
运行程序,通过在URL后面添加/Movies来查看MoviesController。把鼠标放到Edit超链接上来看一下这个超链接指向哪里。
Eidt超链接是由Views\Movies\Index.cshtml视图里的Html.ActionLink方法生成的。
@Html.ActionLink("Edit", "Edit", new { id=item.ID })
Html对象是作为System.Web.Mvc.WebViewPage基类的一个属性暴露出来的,它是一个辅助类,这个辅助类的ActionLink方法使动态创建一个指向controller中的action方法的HTML超链接变得十分容易(注:ActionLink实际上是一个扩展方法,扩展了HtmlHelper类)。ActionLink方法的第一个参数是要展示出来的超链接的文本内容(例如,<a>Edit Me</a>)。第二个参数是要调用的action的名字,最后一个参数是一个匿名类型用来生成路由数据(route data,不知道怎么翻译)??
==============================我是华丽的分隔符,一下内容是博主自己加===========================================
注:其实我们的cshtml文件会被编译为WebViewPage<TModel>的一个子类,这个类中的代码如下
namespace System.Web.Mvc { public abstract class WebViewPage<TModel> : WebViewPage { public new AjaxHelper<TModel> Ajax { get; set; } public new HtmlHelper<TModel> Html { get; set; } public new TModel Model { get; } public new ViewDataDictionary<TModel> ViewData { get; set; } public override void InitHelpers(); protected override void SetViewData(ViewDataDictionary viewData); } }
而WebViewPage中的代码如下
using System.Web; using System.Web.WebPages; namespace System.Web.Mvc { public abstract class WebViewPage : WebPageBase, IViewDataContainer, IViewStartPageChild { public AjaxHelper<object> Ajax { get; set; } public override HttpContextBase Context { get; set; } public object Model { get; } public TempDataDictionary TempData { get; } public dynamic ViewBag { get; } #region IViewDataContainer Members public ViewDataDictionary ViewData { get; set; } #endregion #region IViewStartPageChild Members public HtmlHelper<object> Html { get; set; } public UrlHelper Url { get; set; } public ViewContext ViewContext { get; set; } #endregion protected override void ConfigurePage(WebPageBase parentPage); public override void ExecutePageHierarchy(); public virtual void InitHelpers(); protected virtual void SetViewData(ViewDataDictionary viewData); } }
所以我们才能在cshtml中使用ViewBag
===================================分隔符=============================================================
生成的超链接在前面的截图里展示了,它指向了http://localhost:xxxx/Movies/Edit/3。默认的路由(在App_Start\RouteConfig.cs文件里定义)使用{controller}/{action}/{id}这种URL模式。因此,ASP.NET将对http://localhost:xxxx/Movies/Edit/3的请求转换成了对MoviesController中Edit方法的调用,并且给参数ID赋值3,下面是App_Start\RouteConfig.cs文件里的内容
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
同样也可以使用查询字符串的方式来传递参数.例如URL http://localhost:xxxx/Movies/Edit?ID=4同样给MoviesController中的Edit方法的参数ID赋值为3
打开MoviesController类,下面是两个Edit方法的代码:
// // GET: /Movies/Edit/5 public ActionResult Edit(int id = 0) { Movie movie = db.Movies.Find(id); if (movie == null) { return HttpNotFound(); } 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); }
需要注意的是第二个Edit方法附加了HttpPost特性。这个特性指出这个重载的Edit方法只能响应post请求。我们也可以给第一个方法加上HttpGet特性,但是并不需要这么做,因为这个特性是默认特性。(也就是说如果方法上没有加HttpGet或者HttpPost特性的话,默认都是HttpGet)。
HttpGet的Edit方法接收一个ID作为参数,然后使用Entity Framework的Find方法查找数据,然后返回选定的数据到Edit视图中。参数ID有一个默认值0,如果找不到指定ID的数据,会返回HttpNotFound。当scaffolding机制创建Edit视图时,它会检查Movie类的代码然后生成代码来为Movie类的每一个属性生成<label>和<input>标签。下面是生成的Edit视图里的代码:
@model MvcMovie.Models.Movie @{ ViewBag.Title = "Edit"; } <h2>Edit</h2> @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> @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
注意到我们在文件内容的第一行有@model MvcMovies.Models.Movie语句,指定了我们可以在视图模板中使用的model是Movie类型。
自动生成的代码使用了好几个辅助方法来生成HTML标签。Html.LabelFor方法展示了name字段("Title","ReleaseDate","Genre","Price"),Html.EditorFor方法会渲染成<input>标签。Html.ValidationMessageFor方法会展示于Movie的属性有关的验证信息。
运行程序,浏览/Movies,点击Edit超链接。在浏览器中查看这个页面的源文件,下面是form表单中的html内容
<form action="/Movies/Edit/3" method="post"> <fieldset> <legend>Movie</legend> <input data-val="true" data-val-number="字段 ID 必须是一个数字。" data-val-required="ID 字段是必需的。" id="ID" name="ID" type="hidden" value="3" /> <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="疯狂的石头" /> <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-date="字段 ReleaseDate 必须是日期。" data-val-required="ReleaseDate 字段是必需的。" id="ReleaseDate" name="ReleaseDate" type="datetime" value="2009/10/10 0:00:00" /> <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="喜剧" /> <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="字段 Price 必须是一个数字。" data-val-required="Price 字段是必需的。" id="Price" name="Price" type="text" value="25.00" /> <span class="field-validation-valid" data-valmsg-for="Price" data-valmsg-replace="true"></span> </div> <p> <input type="submit" value="Save" /> </p> </fieldset> </form>
form表单的action属性被设置为了/Movies/Edit/3,表单中的数据会以post的方式提交到服务器。
处理POST请求
下面是HttpPost版本的Edit方法
[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 MVC model binder 获取post给服务器的表单数据并且创建一个Movie对象,将这个Movie对象传递给Edit方法。ModelState.IsValid方法确保表单中提交的数据可以用来修改一个Movie对象。如果数据通过验证,新的数据会被保存到db(MovieDBContext实例)的Movies集合中。MovieDBContext的SaveChanges方法将新数据保存到了数据库里。保存完数据之后,代码会重定向到Index方法,就会将所有的数据展示出来。
注:原文中接下来的内容是和javascript中的Globalization有关的内容,在这里没有翻译。
添加一个Search方法和Search视图
在这一小节,我们将会添加一个SearchIndex方法来可以按照名字或者分来来查找影片。通过/Movies/SearchIndex这个URL来实现。这个请求将会展现为一个包含了几个Input标签的html表单页面,用户可以在表单中输入数据来查找电影。当用户提交表单时,这个actionn方法将会获得用户传入的数据并且根据这些数据来从数据库进行检索。
展示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;
这里只是定义了查询,但是还没用真正从数据库检索数据。
如果searchString参数不为空的话,movies查询将会根据searchString的值来过滤数据:
if (!String.IsNullOrEmpty(searchString))
{
movies = movies.Where(s => s.Title.Contains(searchString));
}
代码中的s=>s.Title是Lambda表达式,基于方法的linq查询使用Lambda表达式作为参数。linq查询在定义的时候或者使用Where,OrderBy方法进行修改的时候并不会立即执行,相反,查询会被延迟执行,也就是说查询表达式的求值会被推迟,直到真正的值被迭代时(使用foreach)或者ToList之类的方法被调用。在SearchIndex示例中,这个查询会在SearchIndex的视图中执行。更多的关于延迟查询执行的信息,请参考Query Execution。
现在我们可以来实现SearchIndex视图,右键在SearchIndex方法的内部单击,然后选择"Add View"。在弹出的Add View对话框中,指定我们要传递一个Movie对象到视图模板中作为视图的model。在Scaffold template列表中,选择List,然后点击Add。
点击Add按钮后,Views\Movies\SearchIndex.cshtml视图模板就会被创建出来。因为我们在Scaffold template模板中选择了List,Visual Studio会自动在视图中生成以下标签。scaffolding机制会创建一个HTML表单,它会检测Movie类然后生成代码来为Movie的每一个属性渲染一个<label>标签,下面是生成的代码:
@model IEnumerable<MvcMovie.Models.Movie> @{ ViewBag.Title = "SearchIndex"; } <h2>SearchIndex</h2> <p> @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th> @Html.DisplayNameFor(model => model.Title) </th> <th> @Html.DisplayNameFor(model => model.ReleaseDate) </th> <th> @Html.DisplayNameFor(model => model.Genre) </th> <th> @Html.DisplayNameFor(model => model.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,然后再后面追加一个查询字符串,比如?searchString=%e8%a5%bf%e6%b8%b8(注:这里比较囧的一点是我前面输入的数据都是中文的,而中文在URL里是要被编码的,所以这一串奇怪的%e8%a5%bf%e6%b8%b8其实是中文"西游"的utf-8编码),被筛选出来的数据如下:
如果我们更改了SearchIndex方法的签名把参数名字换为id,这样就会符合在Global.asax文件里定义的默认路由模板
{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元素来帮助用户筛选数据。如果你之前把SearchIndex方法的参数名改成了id,现在需要改回searchString,因为接下来的例子中要使用查询字符串。
打开Views\Movies\SearchIndex.cshtml文件,在@Html.ActionLink("Create New","Create")后面添加如下的代码:
@using (Html.BeginForm()) { <p>Title:@Html.TextBox("SearchString")<br> <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按钮来提交表单时将表单提交到自身。(前面通过在SearchIndex方法内部右键添加视图SearchIndex.cshtml,所以该视图对应的action方法是SearchIndex,这个无参数的BeginForm会将表单提交给这个action处理)。
现在运行程序,我们来试着搜索一部电影(有了UI的帮助我们不必对中文进行URL编码了)。
我们并没有对应HttpPost请求的SearchIndex方法,因为我们不需要,这个方法并不会更改应用程序的状态,只是检索数据而已。
我们也可以添加如下的HttpPost版本的SearchIndex方法。这样做的话,请求将会调用HttpPost版本的SearchIndex方法,程序执行起来的效果如截图所示。
[HttpPost] public string SearchIndex(FormCollection fc, string searchString) { return "<h3> From [HttpPost]SearchIndex: " + searchString + "</h3>"; }
但是,即使你添加了HttpPost版本的SearchIndex方法,这一实现仍然是有限制的。想象一下你想要为特定的搜索添加一个书签,或者想要给朋友发送一个链接,通过这个链接他能看到同样筛选出来的数据。值得注意的是使用Http Post发送请求时,URL是和Get请求一样的(都是localhost:xxxx/Movies/SearchIndex),在URL里并没有与搜索信息。现在,搜索的字符串信息是作为一个表单的字段值提交到服务器的,这就意味着你无法将搜索信息保存在书签里或者通过URL发送给朋友。
解决的方法是使用BegionForm的一个重载,这个重载会向服务器发送Get请求,这样就可以将搜索信息添加到URL中。
Html.BeginForm("SearchIndex","Movies",FormMethod.Get)
BeginForm的这个重载,第一个参数是action方法名,第二个参数是controller名,第三个参数指定表单的method。
现在当你提交一此搜索,URL中将会包含一个查询字符串。查询将会调用HttpGet版本的SearchIndex方法,而不会调用HttpPost版本的方法。
按照分类(Genre)来查询
如果实现添加了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方法的话,集合里将会有很多重复值)。然后将这个泛型List存储到了ViewBag中。
下面的代码展示了如何核查movieGenre参数,如果这个参数不为空,代码就会按照电影的分类(Genre属性)来进一步筛选数据。
if (string.IsNullOrEmpty(movieGenre)) { return View(movies); } else { return View(movies.Where(x => x.Genre == movieGenre)); }
在SearchIndex的视图中添加相应标签来支持按照分类进行查询
在Views\Movies\SearchIndex.cshtml文件中加入一个Html.DropDownList方法,就加载TextBox方法的前面,完整的代码如下
<p> @Html.ActionLink("Create New", "Create") @using (Html.BeginForm("SearchIndex","Movies",FormMethod.Get)){ <p>Genre: @Html.DropDownList("movieGenre", "All") Title: @Html.TextBox("SearchString") <input type="submit" value="Filter" /></p> } </p>
运行程序,浏览/Movies/SearchIndex。试一试同时使用分类和名字来筛选数据
这一小节我们深入查看了自动生成的CRUD方法和视图文件。并且我们自己创建了一个SearchIndex方法和对应的视图来允许用户按照指定的条件搜索数据。在下一个小街里,我们将会看看如何为模型Movie添加一个新属,以及如何添加一个初始设定并且自动创建测试数据库。