在上篇文章中学习了如何在页面中显示相关数据,本节中将学习如何对相关数据进行更新。对于大多数实体关系,可以通过更新外键或导航属性来更新数据,对于多对多关系,Entity Framework不会直接公开连接表,所以你需要通过相应的导航属性来添加和移除实体。
先看完成后的效果图
为Courses自定义Create 和Edit 页面
当一个新的course实体被创建时,该实体必须关联到一个已存在的department。要做到这一点,生成的框架代码应该要包括控制器方法和用于选择department的下列列表的Create和Edit视图。下拉列表用来设置Course.DepartmentID外键属性,这是Entity Framework加载Department导航属性所必需的。
打开Coursecontroller.cs,删除Create和Edit方法,添加如下代码
- 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 (RetryLimitExceededException /* dex */)
- {
- //Log the error (uncomment dex variable name 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)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- Course course = db.Courses.Find(id);
- if (course == null)
- {
- return HttpNotFound();
- }
- 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 (RetryLimitExceededException /* dex */)
- {
- //Log the error (uncomment dex variable name 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);
- }
在该类开始部分添加如下命名空间
- using System.Data.Entity.Infrastructure;
HttpGet Create方法调用PopulateDepartmentsDropDownList方法时并没有设置已选列表项,因为对于一个新的course来说,其所属的department还未被确定。
- public ActionResult Create()
- {
- PopulateDepartmentsDropDownList();
- return View();
- }
- public ActionResult Edit(int? id)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- Course course = db.Courses.Find(id);
- if (course == null)
- {
- return HttpNotFound();
- }
- PopulateDepartmentsDropDownList(course.DepartmentID);
- return View(course);
- }
- catch (RetryLimitExceededException /* dex */)
- {
- //Log the error (uncomment dex variable name 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);
上面的代码确保当页面为了显示错误信息而重新显示时被选中的department应该保持被选中状态。
框架根据department字段自动生成了带有下拉列表的Course视图,但你并不想使用DepartmentID 来作为标题,所以请使用下面的代码修改ViewsCourseCreate.cshtml
- @model ContosoUniversity.Models.Course
- @{
- ViewBag.Title = "Create";
- }
- <h2>Create</h2>
- @using (Html.BeginForm())
- {
- @Html.AntiForgeryToken()
- <div class="form-horizontal">
- <h4>Course</h4>
- <hr />
- @Html.ValidationSummary(true)
- <div class="form-group">
- @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.CourseID)
- @Html.ValidationMessageFor(model => model.CourseID)
- </div>
- </div>
- <div class="form-group">
- @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.Title)
- @Html.ValidationMessageFor(model => model.Title)
- </div>
- </div>
- <div class="form-group">
- @Html.LabelFor(model => model.Credits, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.Credits)
- @Html.ValidationMessageFor(model => model.Credits)
- </div>
- </div>
- <div class="form-group">
- <label class="control-label col-md-2" for="DepartmentID">Department</label>
- <div class="col-md-10">
- @Html.DropDownList("DepartmentID", String.Empty)
- @Html.ValidationMessageFor(model => model.DepartmentID)
- </div>
- </div>
- <div class="form-group">
- <div class="col-md-offset-2 col-md-10">
- <input type="submit" value="Create" class="btn btn-default" />
- </div>
- </div>
- </div>
- }
- <div>
- @Html.ActionLink("Back to List", "Index")
- </div>
- @section Scripts {
- @Scripts.Render("~/bundles/jqueryval")
- }
通常框架不会生成主键,因为主键值是由数据库生成的并且是不可更改的,而且显示给用户是无意义的。对于Course实体,框架为CourseID字段生成了一个文本框,因为DatabaseGeneratedOption.None属性意味着用户应当可以输入主键值,但是该字段只有在你希望将其显示在其他视图的时候才是有意义的,所以你需要手动添加它。
打开ViewsCourseEdit.cshtml,在Title字段之前添加一个course number字段,因为它是主键但是只用于显示而不能被修改
- <div class="form-group">
- @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.DisplayFor(model => model.CourseID)
- </div>
- </div>
Edit视图中已经有一个course number的隐藏字段(Html.HiddenFor帮助器)。对于隐藏字段来说添加一个Html.LabelFor帮助器是没必要的,因为它并不会在用户点击Save时将course number包含在要发送的数据中。
在ViewsCourseDelete.cshtml and ViewsCourseDetails.cshtml中,将department的标题为"Department",并在Title字段之前添加一个course number字段
- <dt>
- Department
- </dt>
- <dd>
- @Html.DisplayFor(model => model.Department.Name)
- </dd>
- <dt>
- @Html.DisplayNameFor(model => model.CourseID)
- </dt>
- <dd>
- @Html.DisplayFor(model => model.CourseID)
- </dd>
进入Edit页面
修改数据并点击Save,可以在Course Index页面中看到更新过的数据
为Instructors添加Edit页面
当你编辑一条instructor记录时,你希望能够更新instructor的office分配情况。Instructor 实体和OfficeAssignment实体之间是一对零或一的关系,这意味着你必须必须处理以下情况:
- 如果用户删除了一个已存在的office,你必须移除并删除这个OfficeAssignment实体
- 如果用户新增了一个office,你必须新建一个OfficeAssignment实体
- 如果用户修改了一个office,你必须修改已经存在的OfficeAssignment实体
打开InstructorController.cs,查看HttpGet Edit方法
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- Instructor instructor = db.Instructors.Find(id);
- if (instructor == null)
- {
- return HttpNotFound();
- }
- ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID);
- return View(instructor);
- }
- public ActionResult Edit(int? id)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- Instructor instructor = db.Instructors
- .Include(i => i.OfficeAssignment)
- .Where(i => i.ID == id)
- .Single();
- if (instructor == null)
- {
- return HttpNotFound();
- }
- return View(instructor);
- }
上面的代码删除了ViewBag语句并为相关联的OfficeAssignment实体指定为预先加载。但你不能在Find方法上使用预先加载,所以这里使用了Where和Single方法来选择instructor。
使用下面的代码替换HttpPost Edit方法,该方法用来更新office分配情况
- [HttpPost, ActionName("Edit")]
- [ValidateAntiForgeryToken]
- public ActionResult EditPost(int? id)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- var instructorToUpdate = db.Instructors
- .Include(i => i.OfficeAssignment)
- .Where(i => i.ID == 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 (RetryLimitExceededException /* dex */)
- {
- //Log the error (uncomment dex variable name 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(instructorToUpdate);
- }
- 修改方法名为EditPost,因为方法名和HttpGet方法是一样的(ActionName属性指定依旧使用/Edit/ URL方式)
- 使用延迟加载通过OfficeAssignment导航属性从数据库中得到当前Instructor 实体,就像在HttpGet Edit方法中所做的那样
- 使用模型绑定器中的数据更新检索到的Instructor实体,通过使用TryUpdateModel重载方法,可以让你指定那些你希望传递的属性的白名单,这样可以防止过分提交(over-posting),就像在之前解释的那样(实现基本的CRUD功能)
- if (TryUpdateModel(instructorToUpdate, "",
- new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
- 如果office地点为空,那么将Instructor.OfficeAssignment属性设置为null,以便OfficeAssignment表中相关的行都将被删除。
- if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
- {
- instructorToUpdate.OfficeAssignment = null;
- }
- 保存更改至数据库
打开ViewsInstructorEdit.cshtml,在 Hire Date字段的div元素后面添加一个用于编辑office地点的字段
- <div class="form-group">
- @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.OfficeAssignment.Location)
- @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
- </div>
- </div>
运行项目,点击Instructors选项卡,点击Edit链接,修改Office Location,最后点击Save
为Instructor Edit页面添加Course 分配功能
一个Instructor可以教授任意数量的course,接下来你将通过使用一组复选框来为Instructor Edit页面添加course分配功能
Course和Instructor实体之间是多对多的关系,这意味着你不需要直接访问连接表中的外键属性。相反,你可以通过Istructor.Courses导航属性来添加和移除实体。
在页面中你可以使用一组复选框来选择将哪些course分配给instructor,对于数据库中的每一门course都使用一个复选框来显示,并设置已分配给instructor的course为选中状态。用户可以通过选择或清除复选框来更改课程分配情况,如果course数量太多,你可能希望通过调用不同的方法来呈现数据,但是你应该使用操作导航属性的方法来创建或删除关系。
为了给视图中的复选框提供数据,你需要使用数据模型类。在在ViewModels文件夹中创建AssignedCourseData.cs类,并使用下面的代码替换
- namespace ContosoUniversity.ViewModels
- {
- public class AssignedCourseData
- {
- public int CourseID { get; set; }
- public string Title { get; set; }
- public bool Assigned { get; set; }
- }
- }
- public ActionResult Edit(int? id)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- Instructor instructor = db.Instructors
- .Include(i => i.OfficeAssignment)
- .Include(i => i.Courses)
- .Where(i => i.ID == id)
- .Single();
- PopulateAssignedCourseData(instructor);
- if (instructor == null)
- {
- return HttpNotFound();
- }
- 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;
- }
PopulateAssignedCourse方法通过读取所有Course实体并使用模型视图类来加载course列表。对于每一门course,该方法会检查在instructor的Courses导航属性中是否存在该course。为了更高效的检查一门course是否被分配给一个instructor,我们将分配给instructor的course放入了一个HashSet集合,并将已分配的course的Assigned属性设置为True,视图会使用该属性来确定哪些复选框应该被显示为选中状态。最后通过ViewBag 属性将列表数据传递给视图。
接下来添加用户单击Save时应当执行的代码,修改EditPost 方法,并添加一个用于更新Instructor实体的Courses导航属性的方法
- [HttpPost]
- [ValidateAntiForgeryToken]
- public ActionResult Edit(int? id, string[] selectedCourses)
- {
- if (id == null)
- {
- return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
- }
- var instructorToUpdate = db.Instructors
- .Include(i => i.OfficeAssignment)
- .Include(i => i.Courses)
- .Where(i => i.ID == 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 (RetryLimitExceededException /* dex */)
- {
- //Log the error (uncomment dex variable name 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);
- }
- }
- }
- }
这里将方法名由EditPost修改为Edit。
由于视图中并没有Course实体集合,所以模型绑定器不能自动更新Courses导航属性,这里使用了UpdateInstructorCourses方法而不是使用模型绑定器来更新Course导航属性。因此,你需要将Course属性从模型绑定器中移除,要做到这一点,你只需要调用 TryUpdateModel方法而不用修改任何代码,因为你正在使用白名单而Courses属性并不在被包含的列表中。
如果没有复选框被选中,UpdateInstructorCourses方法会使用一个空集合来初始化Courses导航属性。
- if (selectedCourses == null)
- {
- instructorToUpdate.Courses = new List<Course>();
- return;
- }
如果某个course的复选框被选中但该course并不在Instructor.Courses导航属性中,那么该course会被添加至导航属性集合中。
- 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);
- }
- }
- <div class="form-group">
- <div class="col-md-offset-2 col-md-10">
- <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>
- </div>
如果你在粘贴代码后发现换行与缩进不像上面中的那样,你可以使用快捷键Ctrl-K-D来修复它们,同时你要保证@:</tr><tr>、@:<td>、@:</td>和@:</tr>要在单独的一行上,否则会出现运行时错误。
上面的代码创建了一个包含三列的HTML表格,每一列都有一个具有编号和标题的复选框,所有的复选框都使用同一个"selectedCourses"名称,这样可以让模型绑定器将它们作为一个组来进行处理。每个复选框的Value属性被设置为CourseID值,当页面被提交时,模型绑定器将包含有被选中的复选框的CourseID值的数组传递给控制器。
当复选框开始被呈现时,已分配给instructor的course会拥有一个checked属性,这些course会被设置为选中状态。
当修改了course分配情况后,你希望能够验证这些修改,因此你需要向页面中表格添加一个Courses列。在本例中,你不需要使用ViewBag对象,因为所需要的数据已经包含在作为模型传递给视图的Instructor实体的Courses导航属性中了。
打开ViewsInstructorIndex.cshtml,在Office 标题后添加一个Courses标题- <tr>
- <th>Last Name</th>
- <th>First Name</th>
- <th>Hire Date</th>
- <th>Office</th>
- <th>Courses</th>
- <th></th>
- </tr>
然后添加新的单元格
- <td>
- @if (item.OfficeAssignment != null)
- {
- @item.OfficeAssignment.Location
- }
- </td>
- <td>
- @{
- foreach (var course in item.Courses)
- {
- @course.CourseID @: @course.Title <br />
- }
- }
- </td>
- <td>
- @Html.ActionLink("Select", "Index", new { id = item.ID }) |
- @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
- @Html.ActionLink("Details", "Details", new { id = item.ID }) |
- @Html.ActionLink("Delete", "Delete", new { id = item.ID })
- </td>
点击Edit链接,进入Edit页面
修改Course分配情况,点击Save,查看数据是否正确。
注意:对于有限的Course数量,我们使用上面的方法是没有任何问题的,但是对于具有大量Course的情况下,我们需要使用特定的UI和更新方式。
修改DeleteConfirmed方法
打开InstructorController.cs,修改DeleteConfirmed方法
- [HttpPost, ActionName("Delete")]
- [ValidateAntiForgeryToken]
- public ActionResult DeleteConfirmed(int id)
- {
- Instructor instructor = db.Instructors
- .Include(i => i.OfficeAssignment)
- .Where(i => i.ID == id)
- .Single();
- instructor.OfficeAssignment = null;
- db.Instructors.Remove(instructor);
- var department = db.Departments
- .Where(d => d.InstructorID == id)
- .SingleOrDefault();
- if (department != null)
- {
- department.InstructorID = null;
- }
- db.SaveChanges();
- return RedirectToAction("Index");
- }
- 当instructor被删除时,删除office 分配记录(如果有)
- 如果instructor被作为department的administrator,从该department中删除此instructor,如果不做如上修改,在当你删除一个已经作为department的administrator的instructor时,会出现参照完整性错误。
为Create 页面添加office地点和course
打开InstructorController.cs,修改HttpGet和HttpPost的Create方法
- public ActionResult Create()
- {
- var instructor = new Instructor();
- instructor.Courses = new List<Course>();
- PopulateAssignedCourseData(instructor);
- return View();
- }
- [HttpPost]
- [ValidateAntiForgeryToken]
- public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment" )]Instructor instructor, string[] selectedCourses)
- {
- if (selectedCourses != null)
- {
- instructor.Courses = new List<Course>();
- foreach (var course in selectedCourses)
- {
- var courseToAdd = db.Courses.Find(int.Parse(course));
- instructor.Courses.Add(courseToAdd);
- }
- }
- if (ModelState.IsValid)
- {
- db.Instructors.Add(instructor);
- db.SaveChanges();
- return RedirectToAction("Index");
- }
- PopulateAssignedCourseData(instructor);
- return View(instructor);
- }
上面的代码和Edit方法中的类似,除了没有course被选择。HttpGet的Create方法调用PopulateAssignedCourseData方法并不是因为这里有course被选中,而是为视图中的foreach循环提供一个空集合(否则页面会抛出一个空引用异常)。
HttpPost Create方法在模板代码检查验证错误并将新的instructor 添加到数据库之前将每一个被选中的course添加至Courses导航属性中。当出现模型错误时,course仍会被添加,所以当出现模型错误时(如用户输入无效日期),页面应该显示一条错误信息,并自动恢复对course所做的任何更改。
注意,为了能够将course添加到Courses导航属性中,你必须将该导航属性初始化为一个空集合。
- instructor.Courses = new List<Course>();
- private ICollection<Course> _courses;
- public virtual ICollection<Course> Courses
- {
- get
- {
- return _courses ?? (_courses = new List<Course>());
- }
- set
- {
- _courses = value;
- }
- }
打开ViewsInstructorCreate.cshtml,在hire date自段之后,Submit按钮之前添加一个office地点文本框和course复选框
- <div class="form-group">
- @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
- <div class="col-md-10">
- @Html.EditorFor(model => model.OfficeAssignment.Location)
- @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
- </div>
- </div>
- <div class="form-group">
- <div class="col-md-offset-2 col-md-10">
- <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>
- </div>
运行项目,进入Create页面,添加一个instructor