MVC中使用EF(6):更新關聯數據

更新關聯數據

By Tom Dykstra|July 30, 2013
Translated by litdwg

Contoso University示例網站演示如何使用Entity Framework 5創建ASP.NET MVC 4應用程序。Entity Framework有三種處理數據的方式: Database FirstModel First, and Code First. 本指南使用代碼優先。其它方式請查詢資料。示例程序是爲Contoso University建立一個網站。功能包括:學生管理、課程創建、教師分配。 本系列指南逐步講述如何實現這一網站程序。

如有問題,可在這些討論區提問: ASP.NET Entity Framework forum, the Entity Framework and LINQ to Entities forum, or StackOverflow.com.

上一節完成了相關聯數據的顯示,本節將學習如何更新關聯數據。大部分關聯關係可通過更新相應的外鍵來完成。對於多對多關係,EF沒有直接暴漏連接表,需要顯式的操作導航屬性(向其中添加、移除實體)來完成。

將要完成的效果如下:

Course_create_page

Instructor_edit_page_with_courses

定製課程的 Create 和Edit 頁面

課程實體創建後是和某個部門有關聯的。爲了展示這一點,自動生成的代碼生成了相應的控制器方法以及創建、編輯視圖,其中包括可選擇部門的下拉列表。下拉列表設置 Course.DepartmentID外鍵屬性,這樣EF就可以正確加載Department 導航屬性的對應實體。這裏只簡單修改代碼,增加錯誤處理和下拉列表排序功能。

 CourseController.csEdit  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 頁面:

Course_create_page

點擊Create. Index頁面將顯示新創建的課程信息。列表中的部門名稱來自於導航屬性,說明正確建立了關聯。

Course_Index_page_showing_new_course

運行 Edit 頁面.

Course_edit_page

修改數據,點擊 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>

運行,測試效果

Changing_the_office_location

Instructor編輯頁面實現課程分配操作

教師可同時承擔多門課程,添加爲教師編輯頁面增加通過組選框設定承擔課程的功能:

Instructor_edit_page_with_courses

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

Instructor_index_page

點擊 Edit 查看Edit page.

Instructor_edit_page_with_courses

修改一些課程的分配然後點擊 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.

發佈了12 篇原創文章 · 獲贊 0 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章