在上一个课程中,你已经创建了 MVC 应用,使用 EF 和 SQL Server Compact 保存和显示数据。在这个课程中,你将要复习并定制 MVC 脚手架为你的控制器和视图自动创建的 CRUD (创建、读取、更新和删除)代码。注意:为了在你的控制器和数据访问层之间进行抽象,通常的做法是实现仓储模式。为了保持这个课程的简洁,在这个系列的最后课程之前,我们不会实现仓储模式。
在这个课程中,你将要创建如下的页面。
2-1 创建详细页
脚手架创建的代码遗留下了学生注册属性没有处理,因为这个属性是集合属性。在详细页面中,你将要在 HTML 表格中显示这个集合的内容。
在 ControllersStudentController.cs 中,详细页面的 Action 方法类似如下的代码:
public ViewResult Details(int id)
{
Student student = db.Students.Find(id);
return View(student);
}
代码使用 Find 方法来获取单个的 Student 实体,使用传递给方法的 id 关键字。Id 来自 Index 页面中的超级链接提供的查询字符串。
打开 ViewsStudentDetails.cshtml。每个字段使用 DisplayFor 助手方法进行显示,类似如下所示:
<div class="display-label">LastName</div>
<div class="display-field">
@Html.DisplayFor(model => model.LastName)
</div>
为了显示注册课程列表,在注册日期 EnrollmentDate
字段之后,fieldset 结束标记之前,增加如下的代码。
<div class="display-label">
@Html.LabelFor(model => model.Enrollments)
</div>
<div class="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>
代码遍历导航属性 Enrollments
中所有的实体,对于这个属性中的每一个 Enrollment 实体,将显示其中的课程和年级。课程标题通过 Enrollments
导航属性保存的 Course
实体来获得。在需要的时候,所有的数据从数据库中获取。( 从另外一个角度说,在这里使用了延迟加载,你没有为 Courses 导航属性指定饿汉模式加载,所以,在你第一次试图访问这个属性的时候,将会向数据库发出一次查询来获取数据,你可以在这个系列后面的 读取相关数据 部分获取更加详细的说明 )
运行这个页面,选择 Students 选项卡,然后点击 Details 超级链接,你就可以看到课程的列表。
2-2 建立创建页面
在 ControllersStudentController.cs,使用下面的代码替换HttpPost
Create
这个 Action 方法,为脚手架创建的代码增加 try-catch 块。
[HttpPost]
public ActionResult Create(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Students.Add(student);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
这些代码将通过 ASP.NET MVC 模型绑定创建的实体对象加入到 Students 集合中,然后保存修改到数据库中。( 模型绑定是 ASP.NET MVC 的一个功能用于简化你获取通过表单提交的数据,模型绑定转换提交的表单数据到 .NET 中的数据类型,通过 Action 方法的参数传递进来,在这里,模型绑定通过表单数据为你实例化了一个 Student 的实体对象实例 )
这里的 try-catch 块是这些代码区别于脚手架创建的代码的唯一不同之处,在保存数据的时候,如果派生自DataException 的异常被抛出,错误信息将会被显示出来,这类错误典型的是由于一些内部错误,而不是程序错误,所以建议用户再重新试一次。在 ViewsStudentCreate.cshtml 中的代码与 Details.cshtml 中类似,除了将每个字段的 DisplayFor 代替为EditorFor
和 ValidationMessageFor 助手方法.下面的示例演示了相关的代码。
<div class="editor-label">
@Html.LabelFor(model => model.LastName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.LastName)
@Html.ValidationMessageFor(model => model.LastName)
</div>
在 Create.cshtml. 中不需要进行修改。
重新运行页面,选择 Students选项卡,点击 Create New。
默认会进行数据验证,输入名字和一个错误的日期,然后点击 Create,查看一下错误提示。
现在,你会看到通过 JavaScript 实现的客户端验证,但是,服务器端的验证也已经实现了。即使客户端验证失败了,坏的数据也会被捕获到,在服务器端会抛出一个异常。
将日期修改为一个正确的日期,例如:9/1/2005,然后点击 Create,会看到一个新的学生出现在 Index页面上。
2-3 创建一个编辑页面
在 ControllersStudentController.cs 中,Http Edit 方法 ( 其中没有使用 HttpPost标签的那一个 ) 使用 Find 方法来获取选中的学生实体,像你在 Details 方法中看到的一样,不需要修改这个方法。
实际上,需要修改 HttpPost Edit 方法,使用下面的代码为它增加 try-catch处理。
[HttpPost]
public ActionResult Edit(Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
}
return View(student);
}
这段代码非常类似在 Create 中的代码,实际上,除了将通过模型绑定创建的对象添加到实体集中,这段代码还设置了实体的标志来表示这个实体已经被修改过了。当 SaveChanges方法被调用的时候,数据库中行的所有字段都将会更新。包括用户没有修改过的字段,并发冲突被忽略掉 ( 你将会在这个系列的后面学习如何处理并发问题 )。
实体的状态,连接以及 SaveChanges 方法
数据库上下文对象维护内存中的对象与数据库中数据行之间的同步。这些信息在调用 SaveChanges方法被调用的时候使用。例如,当使用 Add 方法传递一个新的实体对象时,实体的状态被设置为 Added,在调用 SaveChanges方法的时候,数据库上下文使用 SQL 命令 Insert来插入数据。
实体的状态可能为如下之一:
Added. 实体在数据库中不存在。SaveChanges 方法必须执行 Insert 命令
Unchanged. 在调用 SaveChanges 的时候不需要做任何事情,当从数据库读取数据的时候,实体处于此状态。
Modified. 某些或者全部的实体属性被修改过. SaveChanges方法需要执行 Update 命令。
Deleted. 实体标记为已删除,SaveChanges 方法必须执行 Delete 命令。
Detached. 实体的状态没有被数据库上下文管理。
在桌面应用中,实体的状态改变典型地自动完成。在这种类型的应用中,你读取一个实体,然后修改某些属性的值,这使得实体的状态被自动修改为 Modified。然后,在调用 SaveChanged 的时候,实体框架生成 Update 命令进行更新,只有你修改的实际属性被更新。
但是,在 Web 应用程序中,这个处理序列不是连续的。因为数据库上下文读取的实体对象实例在页面被呈现之后被丢弃了。当 HttpPost Edit 方法被调用的时候,导致一个新的请求和一个新的数据库上下文对象被生成,所以,你必须手工设置实体的状态为 Modified。然后调用 SaveChanges 方法,实体框架更新数据库中数据行的所有列,因为数据库上下文没有办法知道你修改了哪些属性。
如果你希望 Update 语句仅仅更新你实际上修改的字段。你可以通过某种途径保存原有的数据值 ( 例如通过隐藏域 ),以便在 HttpPost Edit 方法被调用的时候这些值存在。然后,可以使用原始的数据来创建一个 Student 实体,使用 Attach 方法调用连接含有原始值的实体对象,然后,使用新的值来更新实体对象,最后再调用 SaveChanges 方法,更多的详细内容,可以查看 EF 团队的博客: Add/Attach and Entity States和Local Data。
在 ViewsStudentEdit.cshtml中的代码类似于 Create.cshtml ,不需要修改。
运行页面,选择 Students 选项卡,然后点击 Edit 超级链接。
修改一些数据,然后点击 Save,可以在 Index 页面看到修改后的数据。
2-4 创建删除页面
在 ControllersStudentController.cs, 模板生成的 HttpGet Delete 方法使用 Find 方法来获取 Student 实体,像在Details 和 Edit 方法中一样。实际上,为了实现在调用 SaveChanges 方法失败的时候使用的错误页面,你需要为这个方法和相应的视图增加一些功能。
像在更新和创建操作中一样,删除操作也需要两个方法。GET 方法用于显示一个视图,使用户可以允许或者取消删除操作,如果用户允许删除操作,那么,将会发出一个 Post 请求,HttpPost Delete 方法将会被调用,然后执行实际的删除操作。
你需要为 HttpPost Delete 方法增加一个 try-catch 块来捕获数据库更新过程中的任何异常。如果错误出现,HttpPost Delete 方法调用 HttpGet Delete 方法,传递一个参数表示错误发生了。HttpGet Delete 方法就会根据错误信息重新显示确认页面,使用户可以取消或者重试。
使用下面的代码替换 HttpGet Delete 方法中的代码,这里会管理错误报告。
public ActionResult Delete(int id, bool? saveChangesError)
{
if (saveChangesError.GetValueOrDefault())
{
ViewBag.ErrorMessage = "Unable to save changes. Try again, and if the problem persists see your system administrator.";
}
return View(db.Students.Find(id));
}
这段代码接收一个可选的 bool 类型参数,这个参数用来表示是在更新失败之后调用这个方法。在页面请求中被调用的时候,这个参数为 null ( false ),当通过 HttpPost Delete 方法更新数据库失败后,被调用的时候,参数被设置为 true,错误信息被传递到视图中。
将 HttpPost Delete 方法 ( 名为 DeleteConfirmed 方法 )的代码替换成下面的代码,这将会执行实际的删除操作,并捕获任何数据库更新错误。
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id)
{
try
{
Student student = db.Students.Find(id);
db.Students.Remove(student);
db.SaveChanges();
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
return RedirectToAction("Delete",
new System.Web.Routing.RouteValueDictionary {
{ "id", id },
{ "saveChangesError", true } });
}
return RedirectToAction("Index");
}
这段代码获取选中的实体,然后调用 Remove 方法将实体的状态设置为 Deleted。当调用 SaveChanged 的时候,SQL 命令 Delete 被生成并执行。
如果性能是应用的高优先级目标,你可以省略掉不需要的 SQL 查询处理,使用下面的代码行调用 Find 和 Remove 方法。
Student studentToDelete = new Student() { StudentID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;
这段代码实例化了一个 Student 实体,仅仅设置了主键的值,然后将实体的状态设置为 Deleted。EF 删除实体仅仅需要这些信息。
需要注意的是,HttpGet Delete 方法并不真正删除数据,在 GET 请求处理中进行删除存在着安全风险 ( 同样在进行编辑,创建,或者任何修改数据的处理中 ),更多的资料,参见:ASP.NET MVC Tip #46 — Don't use Delete Links because they create Security Holes
在 ViewsStudentDelete.cshtml 中,在 h2 和 h3 之间增加下面的代码:
<p class="error">@ViewBag.ErrorMessage</p>
运行页面,选择 Students 选项卡,点击 Delete 超级链接。
点击 Delete,Index 页面中就不会再显示被删除的学生了。( 在处理并发的部分可以看到错误处理 )
2-5 确认数据库连接没有忘记关闭
为了确认数据库连接被正确关闭,以及资源被正确释放,你需要释放数据库上下文,这就是为什么在 StudentController 代码的最后会看到 Dispose 方法的原因,在 StudentController.cs, 如下所示:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
基类 Controller 已经实现了接口 IDisposable,所以这段代码简单地重写了 Dispose ( bool ) 方法来显式释放上下文对象。
你现在已经有了一套完整的页面对 Student 进行增、删、改、查处理。在下一课程中,将会为 Index 页面增加排序和分页功能。