在上一篇指南中创建了一个MVC程序,使用EF和SQL Server LocalDB存储、展示数据。这一指南回顾并自定义CRUD代码,之前的代码是MVC结构自动创建的控制器和视图代码.
Note 通常的做法是在控制器和数据访问层之间使用仓库模式创建一个抽象层。这里暂时不实现这一功能,后续再进行补充。请查看(Implementing the Repository and Unit of Work Patterns).
本指南将创建如下页面:
创建详情页面
The scaffolded code for the Students Index
page left out the Enrollments
property, because that property holds a collection. In the Details
page you'll display the contents of the collection in an HTML table.学生列表页面没有显示Enrollments属性,因为此属性是一个集合。在详情页面通过HTML表格显示集合的内容。
在 ControllersStudentController.cs, Details
方法使用 Find
检索单个Student实体.
publicActionResultDetails(int id =0){Student student = db.Students.Find(id);if(student ==null){returnHttpNotFound();}returnView(student);}
主键的值通过地址路由参数传递。.
- 打开 ViewsStudentDetails.cshtml. 每个字段通过
DisplayFor
显示:<divclass="display-label"> @Html.DisplayNameFor(model => model.LastName) </div><divclass="display-field"> @Html.DisplayFor(model => model.LastName) </div>
-
在EnrollmentDate
之后,在fieldset标记结束之前
, 添加如下代码显示注册信息列表:<divclass="display-label"> @Html.LabelFor(model => model.Enrollments) </div><divclass="display-field"><table><tr><th>Course Title</th><th>Grade</th></tr> @foreach (var item in Model.Enrollments) { <tr><td> @Html.DisplayFor(modelItem => item.Course.Title) </td><td> @Html.DisplayFor(modelItem => item.Grade) </td></tr> } </table></div></fieldset><p> @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) | @Html.ActionLink("Back to List", "Index") </p>
循环显示Enrollments导航属性中的每一个Enrollment实体,其中显示的课程名是通过Enrollment中的导航实体Course找到的。所有数据在需要时从数据库自动检索。也就是说,这里使用了延迟加载。无需添加代码,在第一次使用此属性时,数据从数据库中检索得到。随后在Reading Related Data 进一步介绍延迟加载。
-
点击列表中某一学生,即可看到详情信息如下:
更新创建页面
- 在ControllersStudentController.cs, 替换
HttpPost
Create
方法的代码,添加try-catch
和Bind attribute :[HttpPost][ValidateAntiForgeryToken]publicActionResultCreate([Bind(Include="LastName, FirstMidName, EnrollmentDate")]Student student){try{if(ModelState.IsValid){ db.Students.Add(student); db.SaveChanges();returnRedirectToAction("Index");}}catch(DataException/* dex */){//Log the error (uncomment dex variable name after DataException and add a line here to write a log.ModelState.AddModelError("","Unable to save changes. Try again, and if the problem persists see your system administrator.");}returnView(student);}
在上述代码中,从表单提交来的数据中生成一个Student实体,添加到EF对应的Students DataSet,并存入数据库。
-
Security Note: Bind特性用来避免过度提交。例如Student实体包含一个Secret属性,此属性不需要通过创建赋值。
publicclassStudent{publicintStudentID{get;set;}publicstringLastName{get;set;}publicstringFirstMidName{get;set;}publicDateTimeEnrollmentDate{get;set;}publicstringSecret{get;set;}publicvirtualICollection<Enrollment>Enrollments{get;set;}}
即便页面表单中没有可录入secret属性值的地方,黑客依然可以通过一些工具对其赋值,如使用
fiddler, 或者通过 JavaScript. 这将使得secret被赋意想不到的值并存入数据库,比如黑客使用fiddler,如下图:.
安全的做法是通过Bind特性的
Include
参数指明可以赋值的字段(白名单)。或者使用Exclude
参数设置黑名单.Include
更为安全,因为如果添加一个新字段,默认是不允许赋值的。另一种安全的做法是使用视图模型,待MVC模型绑定器(MVC model binder)完成从表单到模型的生成后,把值从视图模型拷贝到实体。
除了Bind特性,
try-catch
是做的另一改动。由可能会产生一些数据异常,这些异常编程时无法控制的,应该将这些异常记录到日志,便于维护(这里没有记录) -
ViewsStudentCreate.cshtml的代码和Details.cshtml相似, 除了使用
EditorFor
和ValidationMessageFor代替了
DisplayFor
. :<divclass="editor-label"> @Html.LabelFor(model => model.LastName) </div><divclass="editor-field"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName) </div>
-
在 Students 页面点击Create New.
有些数据验证是默认的。.
即便客户端验证的JavaScript被浏览器禁止,服务器端的验证依然能保证在输入无效数据时做相应的响应操作.
[HttpPost][ValidateAntiForgeryToken]publicActionResultCreate(Student student){if(ModelState.IsValid){ db.Students.Add(student); db.SaveChanges();returnRedirectToAction("Index");}returnView(student);}
数据合法,将把新的学生信息添加到列表。.
修改更新的POST页面
在ControllersStudentController.cs, HttpGet
Edit
方法(没有HttpPost
特性的那个) 使用Find方法找到数据并显示,如Details
中一样。这里不做修改。.
修改 HttpPost
Edit
添加try-catch
和Bind attribute:
[HttpPost][ValidateAntiForgeryToken]publicActionResultEdit([Bind(Include="StudentID, LastName, FirstMidName, EnrollmentDate")]Student student){try{if(ModelState.IsValid){ db.Entry(student).State=EntityState.Modified; db.SaveChanges();returnRedirectToAction("Index");}}catch(DataException/* dex */){//Log the error (uncomment dex variable name after DataException and add a line here to write a log.ModelState.AddModelError("","Unable to save changes. Try again, and if the problem persists see your system administrator.");}returnView(student);}
更新和创建相似,只是不再添加,而是将对应的实体状态设为Modified,当执行SaveChanges ,Modified 标记促使Entity Framework 创建SQL更新语句并执行。.此记录所有字段将更新(包括未被修改的字段),同步冲突被忽略。 (在之后的Handling Concurrency 中将介绍如何处理冲突.)
Entity States and the Attach and SaveChanges Methods
数据上下文追踪内存中的实体与数据库中的记录是否一致。这将决定调用SaveChanges
方法时发生什么操作。例如,当使用Add 方法增加一个实体时,状态将被设置为Added
. 调用SaveChanges 时,将生成 SQL INSERT
command.
实体可能的状态如下 following states:
Added
. 此实体在数据库中不存在。SaveChanges
必须生成INSERT
statement.Unchanged
. 没有变化,当实体从数据库中读取时的状态。Modified
. 有属性值发生了改变。SaveChanges
必须生成UPDATE
statement.Deleted
. 此实体被删除。SaveChanges
必须生成DELETE
statement.Detached
. 此实体没有被数据库上下文追踪。.
在桌面程序,状态的改变是自动发生的。
连接断开是web程序的特点, DbContext 在读取数据后即被释放。 当调用HttpPost
Edit
将使用新的DbContext对象, 因此必须人为设置实体状态为Modified.
然后在保存时更新所有字段,因为不知道哪些字段发生了改变。.
如果只想更新发生了改变的字段,可将原始值保存下来(比如使用隐藏域),调用Attach方法,然后将实体更新到新的值,再调用SaveChanges。详细信息请查看Entity states and SaveChanges 和Local Data in the MSDN Data Developer Center.
ViewsStudentEdit.cshtml 和Create.cshtml代码相似,未作改变.
点击Edit 链接,打开编辑页面.
修改并点击Save. .
修改Delete页面
在ControllersStudentController.cs, HttpGet
Delete
和Details
and Edit
相似。. However, to implement a custom error message when the call toSaveChanges
fails, you'll add some functionality to this method and its corresponding view.
HttpPost Delete
中添加try-catch
.如果发生错误,HttpPost
Delete
方法调用HttpGet
Delete
并传递错误信息,HttpGet Delete
将显示错误信息。
- 替换
HttpGet
Delete
代码如下:publicActionResultDelete(bool? saveChangesError=false,int id =0){if(saveChangesError.GetValueOrDefault()){ViewBag.ErrorMessage="Delete failed. Try again, and if the problem persists see your system administrator.";}Student student = db.Students.Find(id);if(student ==null){returnHttpNotFound();}returnView(student);}
可选的布尔参数用来表示是否在执行失败后被调用. 默认值是false,当
HttpPost
Delete
执行失败时,调用get,参数设置为 -
替换
HttpPost
Delete
(名为DeleteConfirmed
) ,捕获数据错误.[HttpPost][ValidateAntiForgeryToken]publicActionResultDelete(int id){try{Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges();}catch(DataException/* dex */){// uncomment dex and log error. returnRedirectToAction("Delete",new{ id = id, saveChangesError =true});}returnRedirectToAction("Index");}
也可以使用如下语句,提高执行速度:
Student studentToDelete =newStudent(){StudentID= id }; db.Entry(studentToDelete).State=EntityState.Deleted;
创建一个实体,只赋值了主键,并设置状态为Deleted,EF在同步数据库时将删除对应的记录。
-
HttpGet
Delete
没有删除数据,不应该在get方法中执行数据的更新操作,更多信息,请查看 ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes . -
在ViewsStudentDelete.cshtml, 添加显示错误信息的代码:
<h2>Delete</h2><pclass="error">@ViewBag.ErrorMessage</p><h3>Are you sure you want to delete this?</h3>
Run the page by selecting the Students tab and clicking a Delete hyperlink:
-
点击Delete. 列表页面将不再显示此数据。 (在Handling Concurrency 将看到执行出错的处理情况.)
确保数据连接关闭
为了释放资源,StudentController中添加了Dispose 方法:
protectedoverridevoidDispose(bool disposing){ db.Dispose();base.Dispose(disposing);}
Controller
基类已经实现了 IDisposable
接口, 这里只是重载了Dispose(bool)
方法释放数据上下文资源.
总结
已经完成了基本的CRUD. 下一指南将扩展列表页,实现排序和分页.