在 ASP.NET MVC 应用程序中使用实体框架更新相关数据(共 10 个)
作者:Tom Dykstra
Contoso University 示例 Web 应用程序演示如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 创建 ASP.NET MVC 4 应用程序。 若要了解系列教程,请参阅本系列中的第一个教程。
在上一教程中,你显示了相关数据;在本教程中,你将更新相关数据。 对于大多数关系,可以通过更新相应的外键字段来完成此操作。 对于多对多关系,Entity Framework 不会直接公开联接表,因此必须显式向相应导航属性添加和删除实体。
下图是将会用到的页面。
自定义课程的创建和编辑页面
创建新的课程实体时,新实体必须与现有院系有关系。 为此,基架代码需包括控制器方法、创建视图和编辑视图,且视图中应包括用于选择院系的下拉列表。 下拉列表设置了 Course.DepartmentID
外键属性,而这正是 Entity Framework 使用适当的 Department
实体加载 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
根据已分配给正在编辑的课程的部门 ID 设置所选项目:
public ActionResult Edit(int id)
{
Course course = db.Courses.Find(id);
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
这HttpPost
两种方法Create
Edit
,还包括在错误后重新显示页面时设置选定项的代码:
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 中,添加突出显示的代码以在“标题”字段之前创建新的课程编号字段。 如前面的教程中所述,默认情况下,主键字段不是基架的,但此主键有意义,因此你希望用户能够输入键值。
@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 中,在 Title 字段之前添加课程编号字段。 因为它是主键,因此会显示它,但无法更改。
<div class="editor-label">
@Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
@Html.DisplayFor(model => model.CourseID)
</div>
运行“创建”页(显示“课程索引”页,然后单击“新建”)并输入新课程的数据:
单击 “创建” 。 “课程索引”页显示,其中新课程已添加到列表中。 索引页列表中的院系名称来自导航属性,表明已正确建立关系。
运行“编辑”页(显示“课程索引”页,然后单击“在课程上编辑”)。
更改页面上的数据,然后单击“保存”。 “课程索引”页随更新的课程数据一起显示。
为讲师添加编辑页面
编辑讲师记录时,有时希望能更新讲师的办公室分配。 该 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);
}
此代码删除 ViewBag
该语句,并添加关联 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);
}
该代码执行以下操作:
使用
OfficeAssignment
导航属性的预先加载从数据库获取当前的Instructor
实体。 这与在方法中HttpGet
Edit
执行的操作相同。用模型绑定器中的值更新检索到的
Instructor
实体。 使用 TryUpdateModel 重载可以安全列出要包含的属性。 这样可以防止第二个教程中所述的过度发布。if (TryUpdateModel(instructorToUpdate, "", new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
如果办公室位置为空,请将
Instructor.OfficeAssignment
属性设置为 NULL,以便删除OfficeAssignment
表中的相关行。if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location)) { instructorToUpdate.OfficeAssignment = null; }
将更改保存到数据库。
在 Views\Instructor\Edit.cshtml 中,在“雇用日期”字段的元素之后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.Courses
和删除实体。
用于更改讲师所对应的课程的 UI 是一组复选框。 该复选框中会显示数据库中的所有课程,选中讲师当前对应的课程即可。 用户可以通过选择或清除复选框来更改课程分配。 如果课程数量要大得多,你可能希望使用不同的方法在视图中呈现数据,但你会使用相同的操作导航属性方法来创建或删除关系。
若要为复选框列表的视图提供数据,将使用视图模型类。 在 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
导航属性中是否存在该课程。 若要在检查是否将课程分配给讲师时创建有效的查找,分配给讲师的课程将放入 HashSet 集合中。 对于分配讲师的课程,该 Assigned
属性设置为 true
。 视图将使用此属性来确定应将哪些复选框显示为选中状态。 最后,列表将传递给属性中的 ViewBag
视图。
接下来,添加用户单击“保存”时执行的代码。 将HttpPost
Edit
方法替换为以下代码,该代码调用更新Courses
实体导航属性Instructor
的新方法。 突出显示所作更改。
[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 导航属性。 为此,需要从模型绑定中排除 Courses
属性。 这不需要对调用 TryUpdateModel 的代码进行任何更改,因为你使用的是 安全列表 重载, Courses
并且不在包含列表中。
如果未选中复选框,则使用 UpdateInstructorCourses
空集合初始化 Courses
导航属性的代码:
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
之后,代码会循环访问数据库中的所有课程,并逐一检查当前分配给讲师的课程和视图中处于选中状态的课程。 为便于高效查找,后两个集合存储在 HashSet
对象中。
如果某课程的复选框处于选中状态,但该课程不在 Instructor.Courses
导航属性中,则会将该课程添加到导航属性中的集合中。
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
后面div
立即添加以下突出显示的代码,添加包含复选框数组的 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")
}
此代码将创建一个具有三列的 HTML 表。 每个列中都有一个复选框,随后是一段由课程编号和标题组成的描述文字。 复选框都具有相同的名称(“selectedCourses”),这会通知模型联编程序,它们将被视为组。 每个 value
复选框的属性设置为“发布页面时”的值 CourseID.
,模型绑定器会将数组传递给控制器,该控制器仅包含 CourseID
所选复选框的值。
最初呈现复选框时,分配给讲师的课程的复选框具有 checked
属性(选中这些属性)。
更改课程作业后,您需要能够在网站返回到 Index
页面时验证更改。 因此,需要向该页中的表添加列。 在这种情况下,无需使用该 ViewBag
对象,因为要显示的信息已位于 Courses
要作为模型传递给页面的 Instructor
实体的导航属性中。
在 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>
}
运行“讲师索引”页,查看分配给每个讲师的课程:
单击讲师上的“编辑”以查看“编辑”页面。
更改一些课程作业,然后单击“ 保存”。 所作更改将反映在索引页上。
注意:当课程数量有限时,编辑讲师课程数据的方法非常有效。 若是远大于此的集合,则需要使用不同的 UI 和不同的更新方法。
更新 Delete 方法
更改 HttpPost 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 资源的链接可在本系列的最后一篇教程结束时找到。