MVC5+EF6--8 更新关联数据
近期学习MVC5+EF6,找到了Microsoft的原文,一个非常棒的系列,Getting Started with Entity Framework 6 Code First using MVC 5,网址:http://www.asp.net/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-an-entity-framework-data-model-for-an-asp-net-mvc-application。
这个系列的原文,可以上面网址找到。我也从网上找到了相关的译文。申明,这些译文,我不是原创,而是从网上找来的,为避免下次还要网上查询,现将这些译文整理放在上面。以后找时间将原文地址附上。
MVC5+EF6--8 更新关联数据
原文网址:
Contoso University示例网站演示如何使用Entity Framework 5创建ASP.NET MVC 4应用程序。Entity Framework有三种处理数据的方式: Database First, Model First, and Code First. 本指南使用代码优先。其它方式请查询资料。示例程序是为Contoso University建立一个网站。功能包括:学生管理、课程创建、教师分配。 本系列指南逐步讲述如何实现这一网站程序。
如有问题,可在这些讨论区提问: ASP.NET Entity Framework forum, the Entity Framework and LINQ to Entities forum, or StackOverflow.com.
上一节完成了相关联数据的显示,本节将学习如何更新关联数据。大部分关联关系可通过更新相应的外键来完成。对于多对多关系,EF没有直接暴漏连接表,需要显式的操作导航属性(向其中添加、移除实体)来完成。
将要完成的效果如下:
定制课程的 Create 和Edit 页面
课程实体创建后是和某个部门有关联的。为了展示这一点,自动生成的代码生成了相应的控制器方法以及创建、编辑视图,其中包括可选择部门的下拉列表。下拉列表设置 Course.DepartmentID外键属性,这样EF就可以正确加载Department 导航属性的对应实体。这里只简单修改代码,增加错误处理和下拉列表排序功能。
在 CourseController.cs, Edit 和 Create 方法修改后代码如下:
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
[Bind(Include = "CourseID,Title,Credits,DepartmentID")]
Course course)
{
try
{
if (ModelState.IsValid)
{
db.Courses.Add(course);
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.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
public ActionResult Edit(int id)
{
Course course = db.Courses.Find(id);
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "CourseID,Title,Credits,DepartmentID")]
Course course)
{
try
{
if (ModelState.IsValid)
{
db.Entry(course).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.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
var departmentsQuery = from d in db.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
}
PopulateDepartmentsDropDownList 方法获取按名排列的部门列表, 为下拉列表构建一个SelectList 集合,使用 ViewBag 属性将其传递到视图.方法有一个可选参数selectedDepartment 以便设置下拉列表默认值. 视图将把DepartmentID 传递给DropDownList 帮助器, 帮助器从 ViewBag 中寻找名为DepartmentID的 SelectList.
HttpGet Create 调用 PopulateDepartmentsDropDownList 方法时不使用默认值,因为此时还没有创建新课程数据:
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
HttpGet Edit 方法则设置默认值,因为此时课程在编辑时有原始的部门信息:
public ActionResult Edit(int id)
{
Course course = db.Courses.Find(id);
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
HttpPost 方法在捕获异常之后再次显示创建或编辑页面时,初始化下拉列表默认值:
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.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
代码确保如果发生异常返回页面时,原有的操作数据还在.
在 Views\Course\Create.cshtml, 在 Title 域之前添加代码,提供录入课程编号的编辑域。之前曾经介绍过,自动生成代码不会保护对主键的编辑域.
@model ContosoUniversity.Models.Course
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Course</legend>
<div class="editor-label">
@Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.CourseID)
@Html.ValidationMessageFor(model => model.CourseID)
</div>
<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.Credits)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Credits)
@Html.ValidationMessageFor(model => model.Credits)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.DepartmentID, "Department")
</div>
<div class="editor-field">
@Html.DropDownList("DepartmentID", String.Empty)
@Html.ValidationMessageFor(model => model.DepartmentID)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
在 Views\Course\Edit.cshtml, Views\Course\Delete.cshtml, 和Views\Course\Details.cshtml, 添加如下代码
<div class="editor-label">
@Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
@Html.DisplayFor(model => model.CourseID)
</div>
运行 Create 页面:
点击Create. Index页面将显示新创建的课程信息。列表中的部门名称来自于导航属性,说明正确建立了关联。
运行 Edit 页面.
修改数据,点击 Save. 索引页面显示更新后的数据.
为 Instructors添加编辑页面
编辑instructor 记录时希望同时更新所在办公室信息. Instructor 和 OfficeAssignment 实体存在一对零或一的关系, 也就是要处理以下场景:
- 如果之前有办公室信息,编辑时将其去除,需要删除OfficeAssignment 实体.
- 如果之前没有办公室信息,编辑时添加了,需要创建OfficeAssignment 实体.
- 如果编辑了办公室信息,需要更新OfficeAssignment 实体.
在 InstructorController.cs , 自动生成的HttpGet Edit 方法代码如下:
public ActionResult Edit(int id = 0)
{
Instructor instructor = db.Instructors.Find(id);
if (instructor == null)
{
return HttpNotFound();
}
ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.InstructorID);
return View(instructor);
}
自动生成的代码创建了下拉列表,我们将其修改以下,使用文本框:
public ActionResult Edit(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.InstructorID == id)
.Single();
return View(instructor);
}
代码通过贪婪加载方式获取OfficeAssignment 实体。Find 方法无法使用贪婪加载,因此使用Where 和Single 方法。
将HttpPost Edit 方法替换为如下代码:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formCollection)
{
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.InstructorID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.Entry(instructorToUpdate).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.");
}
}
ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", id);
return View(instructorToUpdate);
}
这部分代码的作用是:
- 从数据库通过贪婪加载获取Instructor 和 OfficeAssignment实体. 这是和 HttpGet Edit 方法一样的.
- 使用模型绑定器数据更新 Instructor 实体. TryUpdateModel 更新白名单中的属性值,关于白名单的介绍在指南的第二节..
- 如果办公室信息为空,将 Instructor.OfficeAssignment 属性设为null, OfficeAssignment 表中相应的记录也将删除.
- 保存对数据库的修改.
· if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
· if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
· {
· instructorToUpdate.OfficeAssignment = null;
}
在 Views\Instructor\Edit.cshtml, Hire Date 的div 标记之后, 添加办公室信息的编辑域:
<div class="editor-label">
@Html.LabelFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>
运行,测试效果
在Instructor编辑页面实现课程分配操作
教师可同时承担多门课程,添加为教师编辑页面增加通过组选框设定承担课程的功能:
Course 和 Instructor 实体之间是多对多关系,意味着无法之间访问连接表。通过 Instructor.Courses导航属性中添加或删除关联实体的方式来实现关系的维护.
功能通过组选框来实现。列出数据库中所有课程,通过选择框确定是否选择,教师当前承担的课程处于选中状态。用户通过选中或者取消选中的操作修改课程的分配情况. 如果课程数目很多,你可能希望使用别的显示方法,但操作导航属性来添加或删除关系的方法是一样的。
创建模型类以便为视图的组选框提供数据. 在ViewModels 文件夹创建 AssignedCourseData.cs,代码如下:
namespace ContosoUniversity.ViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
在 InstructorController.cs, 替换 HttpGet Edit 方法代码如下.
public ActionResult Edit(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
PopulateAssignedCourseData(instructor);
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = db.Courses;
var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}
代码使用贪婪模式加载 Courses 导航属性,调用PopulateAssignedCourseData 方法实现为视图提供AssignedCourseData 视图模型的数据.
PopulateAssignedCourseData 方法读取所有 Course 实体. 对每一个Courses 检查是否已经存在于导航属性. 为了提高效率,将当前承担课程的ID形成一个 HashSet 集合. 承担课程的 Assigned 属性将设为 true . 视图将使用此属性决定哪些选择框处于被选中状态. 最后通过ViewBag 的一个属性将列表传递到视图.
下一步,完成保存代码。
使用如下代码替换 HttpPost Edit 方法的代码, 调用一个新的方法更新Instructor 实体的 Courses 导航属性.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formCollection, string[] selectedCourses)
{
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.Entry(instructorToUpdate).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.");
}
}
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in db.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
}
}
视图不包含Course 实体集合, 因此模型绑定器不能直接更新Courses 导航属性. 更新由 UpdateInstructorCourses 方法完成. 因此要把 Courses属性从模型绑定器中排除出去. 这并不需要修改 TryUpdateModel 的代码,因为使用了白名单, Courses 不在名单之内.
如果没有选中任何课程, UpdateInstructorCourses 将 Courses 导航属性设为一个空的列表:
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
代码执行循环检查数据库中的每一课程,若此课程被选中则判断是否已经包含在相关数据中,如果没有则添加到导航属性。为了提高效率,把选中课程Id和已有课程ID放在哈希表中。
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
如果某课程没有选中但存在于 Instructor.Courses 导航属性,则将其从中移除.
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
在 Views\Instructor\Edit.cshtml, 添加如下高亮代码,在OfficeAssignment 之后增加选中 Courses 的组选框。
@model ContosoUniversity.Models.Instructor
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
<fieldset>
<legend>Instructor</legend>
@Html.HiddenFor(model => model.InstructorID)
<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>
<div class="editor-label">
@Html.LabelFor(model => model.FirstMidName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.FirstMidName)
@Html.ValidationMessageFor(model => model.FirstMidName)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.HireDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.HireDate)
@Html.ValidationMessageFor(model => model.HireDate)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
<table>
<tr>
@{
int cnt = 0;
List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;
foreach (var course in courses) {
if (cnt++ % 3 == 0) {
@: </tr> <tr>
}
@: <td>
<input type="checkbox"
name="selectedCourses"
value="@course.CourseID"
@(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
@course.CourseID @: @course.Title
@:</td>
}
@: </tr>
}
</table>
</div>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
This code creates an HTML table that hasthree columns. In each column is a check box followed by a caption thatconsists of the course number and title. 创建了一个三列表格,每一列包含复选框、课程编号和名称。所有复选框的名字都是一样的("selectedCourses"), 模型绑定器由此得知将其作为一组信息来处理. 单选框的 value 设为对于课程的 CourseID. 当编辑提交之后,模型绑定器将被选中的复选框的值组合为一个数组传给控制器。
在 Views\Instructor\Index.cshtml, 在Office 列之后添加 Courses :
<tr>
<th></th>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
</tr>
修改视图代码:
@model ContosoUniversity.ViewModels.InstructorIndexData
@{
ViewBag.Title = "Instructors";
}
<h2>Instructors</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table>
<tr>
<th></th>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
</tr>
@foreach (var item in Model.Instructors)
{
string selectedRow = "";
if (item.InstructorID == ViewBag.InstructorID)
{
selectedRow = "selectedrow";
}
<tr class="@selectedRow" valign="top">
<td>
@Html.ActionLink("Select", "Index", new { id = item.InstructorID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) |
@Html.ActionLink("Details", "Details", new { id = item.InstructorID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.InstructorID })
</td>
<td>
@item.LastName
</td>
<td>
@item.FirstMidName
</td>
<td>
@String.Format("{0:d}", item.HireDate)
</td>
<td>
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
</td>
<td>
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
</tr>
}
</table>
@if (Model.Courses != null)
{
<h3>Courses Taught by Selected Instructor</h3>
<table>
<tr>
<th></th>
<th>ID</th>
<th>Title</th>
<th>Department</th>
</tr>
@foreach (var item in Model.Courses)
{
string selectedRow = "";
if (item.CourseID == ViewBag.CourseID)
{
selectedRow = "selectedrow";
}
<tr class="@selectedRow">
<td>
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
</td>
<td>
@item.CourseID
</td>
<td>
@item.Title
</td>
<td>
@item.Department.Name
</td>
</tr>
}
</table>
}
@if (Model.Enrollments != null)
{
<h3>Students Enrolled in Selected Course</h3>
<table>
<tr>
<th>Name</th>
<th>Grade</th>
</tr>
@foreach (var item in Model.Enrollments)
{
<tr>
<td>
@item.Student.FullName
</td>
<td>
@Html.DisplayFor(modelItem => item.Grade)
</td>
</tr>
}
</table>
}
</td>
运行 Instructor Index 查看效果:
点击 Edit 查看Edit page.
修改一些课程的分配然后点击 Save. 修改结果在Index页面展示.
Note: 这种方式在课程数目不多时有效。如果课程数目很多需要修改显示方式和更新方法。
更新 Delete 方法
修改代码,当删除教师时,为其分配的办公室信息随之删除:
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.InstructorID == id)
.Single();
instructor.OfficeAssignment = null;
db.Instructors.Remove(instructor);
db.SaveChanges();
return RedirectToAction("Index");
}
已经完成了完整的CRUD操作,但没有处理同步问题。下一节将引入同步问题,介绍处理方法,为CRUD操作添加同步处理。
Entity Framework 相关资源,可查看 the last tutorial in this series.
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。