在上节中添加控制器以后,项目中自动生成了增删改查视图。本节中我们将对其进行修改。
注意:在实际开发中,通常会在数据库和数据访问层之间使用仓库模式来创建抽象层,由于本系列教程我们主要讲EF,所有不会涉及这些。有关信息可查看:ASP.NET Data Access Content Map.
我们可以看到mvc自动创建了一些视图页(Create,Delete,Detail)。
创建详情页
我们打开Student控制器中的Detail方法,如下:
1 public ActionResult Details(int? id) 2 { 3 if (id == null) 4 { 5 return new HttpStatusCodeResult(HttpStatusCode.BadRequest); 6 } 7 Student student = db.Students.Find(id); 8 if (student == null) 9 { 10 return HttpNotFound(); 11 } 12 return View(student); 13 }
我们看到方法中传入了一个Id参数,并且它来自Index页面里的Detail超链接里的路由数据(route data)。这里说下route data
route data
默认的路由格式为:controller,action 和ID.在下面的URL中,控制器为Instructor,Action为Index,id是1。这就是路由数据的值。
http://localhost:1230/Instructor/Index/1?courseID=2021
其中"?courseID=2021"。是一个查询字符串的值。如果你将id作为查询字符串的值,也可以,如下。
http://localhost:1230/Instructor/Index?id=1&CourseID=2021
在Razor视图中,ActionLink创建了url,下面代码中,id与默认路由匹配,所以,id被添加到路由数据中。
@Html.ActionLink("Select", "Index", new { id = item.PersonID })
然而下面的代码中,在默认路由中没有与CourseID参数相匹配的,所以它就会作为一个查询字符串加入。
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
虽然项目中有Detail视图,但是我们无法查看学生的成绩信息,index视图忽略了Enrollments属性。所以修改Detail视图页如下,添加Enrollments信息。
1 @model ContosoUniversity.Models.Student 2 3 @{ 4 ViewBag.Title = "Details"; 5 } 6 7 <h2>Details</h2> 8 9 <div> 10 <h4>Student</h4> 11 <hr /> 12 <dl class="dl-horizontal"> 13 <dt> 14 @Html.DisplayNameFor(model => model.LastName) 15 </dt> 16 17 <dd> 18 @Html.DisplayFor(model => model.LastName) 19 </dd> 20 21 <dt> 22 @Html.DisplayNameFor(model => model.FirstMidName) 23 </dt> 24 25 <dd> 26 @Html.DisplayFor(model => model.FirstMidName) 27 </dd> 28 29 <dt> 30 @Html.DisplayNameFor(model => model.EnrollmentDate) 31 </dt> 32 33 <dd> 34 @Html.DisplayFor(model => model.EnrollmentDate) 35 </dd> 36 <dt> 37 @Html.DisplayNameFor(model=>model.Enrollments); 38 </dt> 39 <dd> 40 <table class="table"> 41 <tr> 42 <th>Course Title</th> 43 <th>Grade</th> 44 </tr> 45 @foreach(var item in Model.Enrollments) 46 { 47 <tr> 48 <td> 49 @Html.DisplayFor(modelItem=>item.Course.Title) 50 </td> 51 <td> 52 @Html.DisplayFor(modelItem=>item.Grade); 53 </td> 54 </tr> 55 } 56 </table> 57 </dd> 58 </dl> 59 </div> 60 <p> 61 @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) | 62 @Html.ActionLink("Back to List", "Index") 63 </p>
ctrl+k+d格式化下代码。ok!
上述代码遍历了Enrollments导航属性中的实体。每一个Enrollment实体,显示出了Course的Title和Grade。通过检索Enrollments实体中的Course导航属性中存储的Enrollments实体,从而查询到Course的Title值。当需要这些数据的时候就会自动执行查询。换句话说,此处使用了懒加载,无需为Course的导航属性指定预加载,所以在查询学生信息的时候,不会查询Enrollment信息。然而,当第一次连接到Enrollments的导航属性的时候,会发送一个查询请求到数据库。有关更多懒加载(lazy loading)和预加载(eager loading)请查看Reading Related Data。
运行效果如下:
Detail页面完成。
修改Create页面
修改控制器中HttpPost特性修饰的Create方法:添加try catch语句
public ActionResult Create([Bind(Include="LastName, FirstMidName, EnrollmentDate")] Student student) { try {
//服务器端验证,稍后将会看到客户端验证 if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } } catch (DataException) { ModelState.AddModelError("","创建失败!"); } return View(student); }
从上述代码Bind属性中移除ID,是因为ID是主键,向数据库中插入数据的时候,会自动创建主键,无需我们自己添加。
安全提示:ValidateAntiForgeryToken
属性可以防止跨站点请求伪造攻击。它需在视图中有相应的Html.AntiForgeryToken()声明,稍后将会看到。
Bind属性可以让我们在创建的时候,可以向数据库中写入那些字段。比如,在Student实体中,有个Secret属性,但是在向数据库中写入实体的时候,我们不希望在页面传递Secret的值到数据库。
如果Student实体包含一个Secret属性,但你不想在页面设置这个字段,即使你页面上没有这字段,黑客也可以通过工具( fiddler)或者写一些JavaScript传递这个字段值。如果没有模型绑定的Bind属性,黑客就可以添加Secret字段到你的实体中。你可以通过Bind的Include参数设置白名单,也可以通过Exclude设置黑名单。使用Include会更安全,因为当你给实体添加一个新的属性的时候,这个字段不会自动被包含在黑名单中。
为了防止在页面输入你不想传入数据库中的字段,在编辑的时候,读取第一次访问数据库中得到的实体信息,然后调用TryUpdateModel
。指定被允许传入的属性列表。
另一种方法也就是现在很多开发者使用的方法:使用视图模型而不是模型绑定的实体类。在视图模型中仅包含你想要修改的属性。一旦MVC模型绑定完成,复制视图模型属性到实体实例。此步骤可以使用工具例如: AutoMapper。使用实体实例的db.Entry设置它的状态为Unchanged,然后对包含在视图模型中的每一个实体属性,设置属性名.IsModified为true。
除了Bind,还添加了try-catch语句块。这样当创建数据出现错误的时候,会抛出“创建失败”的异常。而不会抛出代码错误。在实际项目中我们会将这些异常写入日志中,更多信息请查看: Monitoring and Telemetry (Building Real-World Cloud Apps with Azure).
Create.chstml页面中也包含@Html.AntiForgeryToken()
。这会与controller中的ValidateAntiForgeryToken
一起,防止跨站点请求伪造攻击。(prevent cross-site request forgery attacks.)
在Create页面创建信息的时候,如果输入的日期格式不正确,页面会提示日期无效可见开启了客户端验证,后面章节中,我们会学习客户端验证的特性。
Create页面完成。
修改HttpPost修饰的Edit方法
Student控制器中有两个Edit方法,这里我们只修改带有HttpPost的Edit方法。
[HttpPost,ActionName("Edit")] [ValidateAntiForgeryToken] public ActionResult EditPost(int ?id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } var studentToUpdate = db.Students.Find(id); if (TryUpdateModel(studentToUpdate,"",new string []{ "LastName", "FirstMidName", "EnrollmentDate"})) { try { db.SaveChanges(); return RedirectToAction("Index"); } catch (DataException) { ModelState.AddModelError("","修改失败"); } } return View(studentToUpdate); }
自动生成的代码我们不推荐使用。因为在使用Bind属性的时候,如果有些字段没有写在Include中,Bind就会清除掉这些之前就已经存在的数据。以后mvc自动生成的Edit方法中的代码会修改为不在包含Bind属性。
上述代码中,读取实体信息,然后调用TryUpdateModel将实体信息修改为用户的输入信息然后通过表单post到数据库。EF会自动跟踪然后为实体设置Modified 标志。当SaveChanges被调用的时候,Modified
标志会使EF创建sql语句修改数据库中的数据。这里可以忽略并发冲突,并且数据库中的所有数据都被修改,包括用户没有修改的实体。后面的教程中我们将会学习如何处理并发冲突,如果你只是想修改数据库中的一些字段,可以设置属性为Unchanged,或者设置你的字段为Modifed。
为了防止在修改的时候,在页面输某些你不想保存到数据库的字段(overposting)。可以在Edit页面,将你想修改的字段放在TryUpdateModel
参数中。
我们修改了HttpPost修饰的Edit方法,为了便于区分,所以将方法更名为EditPost。
实体状态,Attach和Savechanges方法
我们写的的数据库上下文类,可以跟踪到内存中的实体是否与数据库的数据内容同步。是否同步决定了当你调用Savechanges方法将会发生什么。例如:当你向Add方法中添加一个实体的时候,这个实体的状态就会被设置成Added,然后你调用Savechanges方法,数据库上下文就会发出一个sql的INSERT命令。
实体可能是如下状态:
Added:数据库中不存在该实体,Savechanges方法会发出一个Insert声明。
UnChanged:Savechanges方法不会对该实体做任何处理,当你从数据库中读取一个实体的时候,就是以这种状态开始的。
Modified:实体的一些或者全部属性值被修改后,Savechanges方法会发出一个Update声明
Deleted:实体已经被标记为删除,Savechanges方法会发出一个Delete声明。
Detached:实体未被数据上下文跟踪。
在桌面应用程序中,通常都是自动设置状态变化,读取实体并修改其属性值,这会将实体状态自动设置成Modified,然后调用Savechanges方法,EF会生成Update语句仅修改你想修改的属性值。
断开连接的web app不会发生以上这些连续操作。页面呈现出以后DbContext会读取显示的实体信息,调用HttpPost修饰的Edit方法时,会生成一个新请求和一个新的DbContext实例。因此你必须手动设置实体状态为Modified。当调用Savechanges方法时,EF修改了数据库中的所有行,因为上下文无法知道你修改的是哪个属性。
如果你想让sql 的update语句只修改你要修改的属性值。可以用一些方法保留原始值(例如隐藏字段)。可以使用原始值创建实体并调用Attach方法。修改实体值为新值然后调用Savechanges方法。有关信息请查看:Entity states and SaveChanges 和Local Data
Edit页面完成。
修改Delete页面
同前面一样,delete操作也有两个方法,Get请求的方法会展现一个视图给用户确定是否删除,如果用户删除,那么就执行Post请求。当post请求执行后才会真正删除数据。
Get请求的delete方法中,通过Find方法找到实体,这里对这个稍作修改,如果查找实体失败,给与提示用户。
在post请求的delete方法中,添加try-catch。
public ActionResult Delete(int? id,bool? saveChangesError=false) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "删除失败!"; } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }
// POST: /Student/Delete/5 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student studentToDelete = new Student() {ID=id }; db.Entry(studentToDelete).State = EntityState.Deleted; db.SaveChanges(); } catch (DataException) { return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
当选择了一要删除的实体后,上述代码就会在数据库中查找,然后调用Remove方法将实体状态设置为Deleted。当调用SaveChanges方法的时候就会生成一个delete的sql语句。我们将方法改名由原来自动生成的DeleteConfirmed
改为为Delete,修改之前自动生成的代码中,将HttpPost的删除方法命名为
DeleteConfirmed
是为了给HttpPost方法一个特殊的签名。现在我们这里用到了重载,httppost和httpget方法名就可以相同了。
为了提高程序性能,避免不必要的sql查询检索数据。可以将上述代码中的Find和remove方法替换为如下。
Student studentToDelete = new Student() { ID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
使用主键声明一个实体,然后设置该实体的状态为 Deleted,
修改Delete.cshtml视图代码,添加ViewBag
<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
Delete功能完成!
关闭数据库连接
关闭连接尽可能快的释放资源。当我们不使用的时候要销毁上下文实例。代码提供在控制器最后提供了一个Dispose方法,基础的Controller类已经实现了IDisposable接口,因此只需重写Dispose(bool)
方法销毁数据库上下文实例即可。
protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); }
事务处理
当你修改多行数据或者表,然后执行Savechanges方法的时候,EF会默认自动执行事务。EF自动确保你所做更改要么全部成功,要么全部失败。如果在修改一系列数据的过程中,前面所有的更改会自动回滚。有关事务请查看: Working with Transactions。