首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > VC/MFC >

MVC中使用EF(六):更新关联数据

2013-10-09 
MVC中使用EF(6):更新关联数据更新关联数据By Tom Dykstra|July 30, 2013Translated by litdwg原文地址:htt

MVC中使用EF(6):更新关联数据

更新关联数据By Tom Dykstra|July 30, 2013Translated by litdwg原文地址:http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application

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没有直接暴漏连接表,需要显式的操作导航属性(向其中添加、移除实体)来完成。

将要完成的效果如下:

MVC中使用EF(六):更新关联数据

MVC中使用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.cshtmlViews\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 页面:

MVC中使用EF(六):更新关联数据

点击Create. Index页面将显示新创建的课程信息。列表中的部门名称来自于导航属性,说明正确建立了关联。

MVC中使用EF(六):更新关联数据

运行 Edit 页面.

MVC中使用EF(六):更新关联数据

修改数据,点击 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 更新白名单中的属性值,关于白名单的介绍在指南的第二节..

         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.cshtmlHire 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>

      运行,测试效果

      MVC中使用EF(六):更新关联数据

      在Instructor编辑页面实现课程分配操作

      教师可同时承担多门课程,添加为教师编辑页面增加通过组选框设定承担课程的功能:

      MVC中使用EF(六):更新关联数据

      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 查看效果:

      MVC中使用EF(六):更新关联数据

      点击 Edit 查看Edit page.

      MVC中使用EF(六):更新关联数据

      修改一些课程的分配然后点击 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.

热点排行