MVC5 Entity Framework学习之更新相关数据
在上篇文章中学习了如何在页面中显示相关数据,本节中将学习如何对相关数据进行更新。对于大多数实体关系,可以通过更新外键或导航属性来更新数据,对于多对多关系,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;PopulateDepartmentsDropDownList方法通过获取所有department并按照名称进行排序来为下拉列表创建一个SelectList 集合,并通过ViewBag属性将集合传递给视图。该方法接收一个可选的selectedDepartment参数,并通过代码来指定当下拉列表被呈现时被选中的列表项。视图会将DepartmentID传递给DropDownList帮助器,然后帮助器就会在ViewBag对象中查找名为DepartmentID的SelectList。
HttpGet Create方法调用PopulateDepartmentsDropDownList方法时并没有设置已选列表项,因为对于一个新的course来说,其所属的department还未被确定。
public ActionResult Create() { PopulateDepartmentsDropDownList(); return View(); }HttpGetEdit方法根据分配给course的department ID来设置已选列表项
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); }Create和Edit的HttpPost方法同样包含了当出现错误后重新显示页面时设置已选列表项的代码
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 来作为标题,所以请使用下面的代码修改Views\Course\Create.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") }同样在Views\Course\Edit.cshtml中进行同样的修改。
通常框架不会生成主键,因为主键值是由数据库生成的并且是不可更改的,而且显示给用户是无意义的。对于Course实体,框架为CourseID字段生成了一个文本框,因为DatabaseGeneratedOption.None属性意味着用户应当可以输入主键值,但是该字段只有在你希望将其显示在其他视图的时候才是有意义的,所以你需要手动添加它。
打开Views\Course\Edit.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包含在要发送的数据中。
在Views\Course\Delete.cshtml and Views\Course\Details.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>运行项目,在Course Index页面中点击Create New,输入数据
进入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); }需要为RetryLimitExceededException添加命名空间,在RetryLimitExceededException上点击右键,选择Resolve - using System.Data.Entity.Infrastructure
- 修改方法名为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; }
- 保存更改至数据库
打开Views\Instructor\Edit.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; } } }打开 InstructorController.cs,修改 HttpGet Edit方法
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; }上面的代码将Courses导航属性指定为预先加载,并调用PopulateAssignedCourseData方法使用AssignedCourseData视图模型类来为复选框提供数据。
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以便在视图中设置它们为选中状态,为了进行高效查找,这两个集合都被存储在HashSet对象中。
如果某个course的复选框被选中但该course并不在Instructor.Courses导航属性中,那么该course会被添加至导航属性集合中。
if (selectedCoursesHS.Contains(course.CourseID.ToString())) { if (!instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Add(course); } }如果某个course的复选框没有被选中但该course却在Instructor.Courses导航属性中,那么该course会从导航属性中移除。
else { if (instructorCourses.Contains(course.CourseID)) { instructorToUpdate.Courses.Remove(course); } }打开Views\Instructor\Edit.cshtml,在OfficeAssignment 字段所在的div元素之后,Save按钮所在的div元素之前,添加一组Courses 字段的复选框
<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导航属性中了。
打开Views\Instructor\Index.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>运行项目,打开Instructor Index页面
点击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>();作为另一种替代方法,你可以在Course模型中通过修改属性的getter访问器来自动的创建该集合。
private ICollection<Course> _courses; public virtual ICollection<Course> Courses { get { return _courses ?? (_courses = new List<Course>()); } set { _courses = value; } }如果你通过这种方法修改了Courses属性,你需要在控制器中删除初始化属性的那些代码。
打开Views\Instructor\Create.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>请确保@:</tr><tr>、@:<td>、@:</td>和@:</tr>应该在单独的一行中
运行项目,进入Create页面,添加一个instructor
事务处理
就像之前在MVC5 Entity Framework学习之实现基本的CRUD功能解释过的那样,默认情况下Entity Framework回隐式的的实现事务。
原文:Updating Related Data with the Entity Framework in an ASP.NET MVC
Application
欢迎转载,请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/39253075
还大家一个健康的网络环境,从你我做起
THE END
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。