在前面的教程中,您创建 MVC 应用程序中,存储和显示数据使用实体框架和 SQL 服务器 LocalDB。在本教程中,您会审查和自定义的 CRUD (创建、 读取、 更新、 删除) MVC 脚手架会自动为您在控制器和视图中创建的代码。
注它是常见的做法,实施资源库模式,以创建您的控制器和数据访问层之间的一个抽象层。为了保持这些教程简单,不会直到后来在这个系列教程执行一个存储库。
在本教程中,您将创建以下 web 页:
创建详细信息页
学生Index
页的搭建的代码排除在外的Enrollments
属性,因为该属性包含一个集合。在Details
页中,您将显示在 HTML 表中的集合的内容。
在ControllersStudentController.cs,Details
视图的操作方法使用 Find
方法检索单个Student
实体。
public ActionResult Details(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
注册表项值将传递给作为id
参数方法和索引页上来自路网数据的Details的超链接。
- 打开ViewsStudentDetails.cshtml。每个字段显示使用的
DisplayFor
的帮手,如下面的示例所示:<div class="display-label"> @Html.DisplayNameFor(model => model.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> </fieldset> <p> @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) | @Html.ActionLink("Back to List", "Index") </p>
此代码将遍历中
Enrollments
导航属性的实体。对于每个Enrollment
实体的属性中,它显示课程标题和品位。从存储在Enrollments
实体的Course
导航属性的Course
实体检索课程标题。所有这些数据是从数据库中检索自动当需要它时。(也就是说,您正在使用延迟加载在这里。您没有指定预先加载Courses
导航属性,所以第一次尝试访问该属性,查询被发送到数据库以检索数据。你可以阅读更多关于延迟加载和读取相关数据教程稍后在本系列中的预先加载。) -
通过选择Students选项卡并单击一个Details链接为Alexander Carson运行该页。您为所选的学生看到课程和成绩的列表:
更新创建页
- 在ControllersStudentController.cs,用下面的代码将
try-catch
块和绑定属性添加到搭建方法替换HttpPost
Create
操作方法:[HttpPost] [ValidateAntiForgeryToken] 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 /* 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."); } return View(student); }
此代码将添加创建的 ASP.NET MVC 模型联编程序对
Students
主体性的Student
实体集,然后将更改保存到数据库。(指容易你一起提交的表单数据的 ASP.NET MVC 功能的模型联编程序; 模型联编程序将已发布的窗体值转换为 CLR 类型,并将它们传递给操作方法的参数。在这种情况下,模型联编程序实例化一个Student
实体为您使用的Form
集合中的属性值。)ValidateAntiForgeryToken
属性有助于防止跨站点请求伪造攻击。安全说明:
Bind
属性添加保护免受过度过帐。例如,假设Student
实体包括你不想要此 web 页后,可以更新的Secret
财产。public class Student { public int StudentID { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } public DateTime EnrollmentDate { get; set; } public string Secret { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } }
即使你没有一个
Secret
字段在 web 页上,黑客可以使用一种工具如fiddler ,或者写一些 JavaScript,发布一个Secret
窗体值。没有限制模型联编程序时它会创建一个Student
实例,所使用的字段的绑定属性模型联编程序会拿起那Secret
窗体值,用它来更新Student
实体实例。然后无论价值黑客指定为Secret
窗体字段将更新您的数据库中。下图显示了fiddler 工具 (与值"OverPost") 的Secret
字段中添加到已发布的窗体值。
"OverPost"将然后将成功添加到
Secret
属性插入行,虽然您从来没有打算 web 页将无法更新该属性的值。它是安全的最佳做法,
Include
参数用白名单中的字段的Bind
属性。它也是可以到您想要排除的黑名单字段使用Exclude
参数。Include
是更安全的原因是当你向实体中添加新的属性,新的领域不会自动保护的Exclude
列表。另一种替代方法,其中一个首选的许多人来说,是只查看模型使用模型绑定。视图模型包含只有您要绑定的属性。一旦完成了 MVC 模型联编程序,您将查看模型属性复制到的实体实例。
除了在
Bind
属性try-catch
块是搭建代码对您已经的更改。如果所做的更改在保存时,会捕获异常从DataException派生,显示一般错误消息。DataException异常有时是由外部的应用程序,而不是一个编程错误,某个东西导致的所以建议用户再试一次。虽然在此示例中未实施,生产高质量的应用会记录日志记录机制如ELMAH异常 (和非 null 内部异常).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.chstml还包括
@Html.AntiForgeryToken()
,其作品中的控制器有助于防止跨站点请求伪造攻击的ValidateAntiForgeryToken
属性。不需要更改在Create.cshtml.
-
运行该页面选择学生选项卡,然后单击Create.
一些数据验证的工作原理,默认情况。输入名称和无效的日期并单击 Create以查看错误消息。
以下突出显示的代码演示模型验证检查。
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Student student) { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } return View(student); }
把日期改到一个有效的值,如 2005/9/1 并单击创建要看见出现在index页中的新学生。
更新编辑文章页面
在ControllersStudentController.cs,HttpGet
Edit
方法 (没有HttpPost
属性的一个) 使用Find
方法检索所选的Student
实体,正如您看到的Details
方法。您不需要更改此方法。
但是,使用以下代码,以添加try-catch
块和绑定属性替换HttpPost
Edit
操作方法:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("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.");
}
return View(student);
}
此代码是类似于您在HttpPost
Create
方法中看到。然而,而不是添加创建的模型联编程序到实体集的实体,此代码指示已更改的实体上设置一个标志。当调用SaveChanges方法时,修改国旗导致实体框架来创建 SQL 语句要更新的数据库行。将更新的数据库行的所有列,其中包括用户没有更改,并且忽略并发冲突。(您将了解如何处理并发稍后在本系列教程中)。
实体状态和附加 SaveChanges 方法
跟踪的数据库上下文是否在内存中的实体与它们相应的行在数据库中,同步此信息确定调用SaveChanges
方法时,会发生什么。例如,当你传递给Add方法的一个新的实体,该实体的状态将设置为Added
。然后当你调用SaveChanges方法,数据库上下文发出 SQLINSERT
命令。
实体可以处于以下状态之一:
Added
。该实体不存在尚未在数据库中。SaveChanges
方法必须发出一个INSERT
语句。Unchanged
。什么都不需要去做与此实体SaveChanges
法。当您从数据库中读取一个实体时,该实体开始与这种地位。Modified
。某些或所有实体的属性值已被都修改。SaveChanges
方法必须发出一个UPDATE
语句。Deleted
。该实体已标记为删除。SaveChanges
方法必须发出DELETE
语句。Detached
。该实体不被跟踪的数据库上下文。
在桌面应用程序中,状态变化通常会自动设置。在桌面应用程序的类型中,您阅读的实体,并对一些其属性值进行更改。这将导致其实体的状态自动更改为Modified
。然后当你调用SaveChanges
,实体框架生成更新仅有实际的属性更改 SQLUPDATE
语句。
Web 应用程序的已断开连接的性质不允许为此连续的序列。读取实体的DbContext被处置后呈现的页面。当调用HttpPost
Edit
操作方法、 发送新的请求和你有DbContext的一个新实例时,所以你必须手动将实体状态设置为Modified.
然后当您调用SaveChanges
,实体框架更新的数据库行的所有列,因为该上下文有没有办法知道哪些属性更改。
如果您想要更新用户实际更改的字段的 SQLUpdate
语句,可以以某种方式 (如隐藏字段) 保存的原始值,以便调用HttpPost
Edit
方法时,他们是可用。然后可以Student
使用创建实体的原始值,调用Attach
方法与该实体的原始版本,更新到新的值,该实体的值,然后调用SaveChanges.
更多的信息,请参阅实体状态和 SaveChanges和 MSDN 数据开发人员中心中的本地数据。
ViewsStudentEdit.cshtml中的代码类似于你看到在Create.cshtml,并没有修改的必要。
通过选择学生选项卡,然后单击Edit超链接运行该页。
改变的一些数据,单击保存。你看到在索引页中更改的数据。
更新删除页
在ControllersStudentController.cs,HttpGet
Delete
方法的模板代码使用Find
方法检索所选的Student
实体,正如您看到的Details
和Edit
的方法。然而,若要实现自定义错误消息到SaveChanges
调用失败时,您将向此方法和其相应的视图添加一些功能。
当你看到更新,并创建操作,删除操作需要两个操作方法。调用 GET 请求的响应的方法显示一个视图,使用户有机会批准或取消删除操作。如果用户批准它,则会创建一个 POST 请求。当发生这种情况时, HttpPost
Delete
方法称为,然后该方法实际上执行删除操作。
你会将try-catch
块添加到HttpPost
Delete
方法,以处理对数据库进行更新时可能会出现的任何错误。如果发生错误,该HttpPost
Delete
方法调用HttpGet
Delete
方法,传递给它的参数,指示发生了错误。HttpGet Delete
方法然后重新显示确认页和错误消息,使用户有机会取消或再试一次。
- 用下面的代码管理错误报告取代
HttpGet
Delete
操作方法:public ActionResult Delete(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) { return HttpNotFound(); } return View(student); }
这段代码接受一个可选的布尔参数指示是否它被称为后未能保存的更改。
HttpGet
Delete
方法调用没有重复以前的错误时,此参数为false
。当它对数据库更新错误的响应HttpPost
Delete
方法调用时,参数为true
和一条错误消息传递给视图。 -
下面的代码,执行实际的删除操作,并捕获任何数据库更新错误替换
HttpPost
Delete
操作方法 (名为DeleteConfirmed
)。[HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException/* dex */) { // uncomment dex and log error. return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
此代码检索所选的实体,然后调用Remove方法,将该实体的状态设置为
Deleted
。当调用SaveChanges
时,生成 SQLDELETE
的命令。Delete
,也有从DeleteConfirmed
改变操作方法的名称。搭建的代码命名为DeleteConfirmed
,给出了HttpPost
方法独特的签名HttpPost
Delete
方法。(CLR 需要有不同的方法参数的重载的方法)。现在,签名是独一无二的你可以坚持使用 MVC 公约并使用HttpPost
相同的名称,HttpGet
删除方法。如果在一个高容量应用程序提高性能是一个优先事项,你可以避免不必要的 SQL 查询,以检索行通过替换调用
Find
并Remove
的方法,用下面的代码,黄色突出显示框所示的代码行:Student studentToDelete = new Student() { StudentID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
此代码实例化一个
Student
实体使用唯一主键值,然后将实体状态设置为Deleted
。这是所有实体框架所需删除的实体。正如所指出,
HttpGet
Delete
方法不会删除数据。执行删除操作响应 GET 请求 (或对于那件事,执行任何编辑操作,创建操作或更改数据的任何其他操作) 会产生安全风险。有关更多信息,请参见ASP.NET MVC 提示 #46 — — 不要使用删除链接,因为它们创建安全漏洞Stephen 瓦尔特的博客上。 -
在ViewsStudentDelete.cshtml,添加一条错误消息之间的
h2
标题和h3
标题中,如下面的示例所示:<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
运行页上选择学生选项卡,然后单击删除超链接:
-
单击删除。没有删除学生显示索引页。(您将看到的错误处理代码在处理并发教程稍后在这一系列行动中的示例。)
确保该数据库的连接是否关闭了
为了确保正确关闭数据库连接和他们持有腾出的资源,你应该看到它释放上下文实例。这就是为什么搭建的代码提供Dispose方法末尾的StudentController
类中StudentController.cs,如下面的示例所示:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
Controller
基类已经实现IDisposable
接口,所以这段代码只是添加重写Dispose(bool)
方法来显式处理上下文实例。
摘要
你现在有一套完整的执行简单的 CRUD 操作,为Student
实体的页面。MVC 佣工用于生成数据字段的 UI 元素。MVC 佣工有关的详细信息,请参见呈现的窗体使用 HTML 帮助器(页面是 MVC 3 但仍然相关的 MVC 4)。
在接下来的教程中会展开索引页的功能,通过添加排序和分页。