翻譯:Contoso 大學 - 6 – 更新關聯數據


By Tom Dykstra, Tom Dykstra is a Senior Programming Writer on Microsoft's Web Platform & Tools Content Team.

原文地址: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 大學 - 使用 EF Code First 創建 MVC 應用

在上一次的課程中,你已經學習了如何顯示關聯的數據,我們將要更新關聯的數據。大多數情況下,可能就是更新表的外鍵字段。對於多對多的關系來說,由於 EF 並沒有直接將表與表之間的連接關系暴露出來,你就必須通過顯式對相關的導航屬性進行添加或者刪除實體來完成。

下面的截圖展示了我們馬上要完成的工作。

6-1  定制課程的創建和編輯頁面

當新的課程實體創建的時候,必須包含相關的 Department。為了達到這個目的,腳手架創建的代碼,包括創建和編輯的控制器以及視圖,都包含了對 Department 的下拉列表的支持。下拉列表設置 Course.DepartmentId 外鍵屬性,這對於 EF 通過 Department 導航屬性來加載 Department 實體來說是必須的。下面將要對腳手架生成的代碼進行一些小的改動,增加錯誤的處理以及對列表內容進行排序。

打開 CourseController.cs, 刪除原來的 Edit 和 Create 方法,使用下面的代碼替換它們。

public ActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}

[HttpPost]
public ActionResult Create(Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Courses.Add(course);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (DataException)
    {
        //Log the error (add a variable name after DataException)
        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]
public ActionResult Edit(Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Entry(course).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (DataException)
    {
        //Log the error (add a variable name after DataException)
        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 方法獲取經過對 Name 進行排序的 Department,然后創建用於下拉列表的 SelectList 集合,通過 ViewBag 傳遞到視圖中。這個方法包含一個參數,允許調用方可選地傳遞一個初始選中項目的值。

HttpGet Create 方法調用沒有設置選中項的 PopulateDepartmentsDropDownList 方法,因為對於新創建的課程來說,還沒有確定歸屬的系。

public ActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}

HttpGet Edit 方法則設置了當前選中的項目,基於當前被編輯課程的 DepartmentId。

public ActionResult Edit(int id)
{
    Course course = db.Courses.Find(id);
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

對於 Create 和 Edit 的 HttpPost 方法來說,在錯誤信息處理之后,都包含了設置當前選中項目的代碼。

 catch (DataException)
{
    //Log the error (add a variable name after DataException)
    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 之前增加一個新的字段,允許用戶輸入課程編號。想之前演示的那樣,腳手架沒有生成主鍵字段,因為主鍵字段對用戶沒有意義。所以需要添加以便用戶能夠輸入這個值。

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

運行 Create 頁面 ( 在課程的 Index 頁面,點擊 Create New  ),然后輸入新的課程。

點擊 Create,在 Index 中應該可以看到包含新創建課程的列表。系的名稱通過導航屬性獲取到,顯示的數據是正確的。

運行編輯頁面 ( 在課程的 Index 頁面中在某個課程上選擇 Edit )

修改一些數據,然后點擊 Save,可以看到更新之后的課程數據。

6-2  增加教師的編輯頁面

在修改教師信息的時候,我們希望也能夠修改教師的辦公室分配。教師實體 Instructor 和辦公室分配 OfficeAssignment存在一對一或者一對零的關系,這意味着你必須處理如下的狀態:

  • 如果教師原來存在一個辦公室分配,但是用戶刪除了它,那么,你必須移除並且刪除這個辦公室分配 OfficeAssignment 實體。
  • 如果教師原來沒有辦公室分配,但是用戶輸入了一個,你必須創建一個新的辦公室分配。
  • 如果用戶修改了原來的辦公室分配,你必須修改當前的辦公分配 OfficeAssignment 實體。

打開 InstructorController.cs,看一下 HttpGet Edit 方法。

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors.Find(id);
    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)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    return View(instructor);
}

代碼中刪除了 ViewBag 語句,增加了使用預先加載的相關 OfficeAssignment 和 Course 實體 ( 現在還不需要課程實體,一會就會用到 )。由於 Find 方法不能使用預先加載,所以這里使用 Where 和 Single 方法來獲取教師。

將 HttpPost 的 Edit 方法使用下面的代碼替換,這里處理了 OfficeAssignment 更新。

[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection)
{
    var instructorToUpdate = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
    {
        try
        {
            if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
            {
                instructorToUpdate.OfficeAssignment = null;
            }

            db.Entry(instructorToUpdate).State = EntityState.Modified;
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (DataException)
        {
            //Log the error (add a variable name after DataException)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
            return View();
        }
    }
    return View(instructorToUpdate);
}

代碼中處理了如下的內容:

  • 從數據庫中獲取了教師 Instructor 實體,並且預先加載了相關的的辦公室分配 OfficeAssignment 和課程 Course 導航屬性。如同在 HttpGet 的 Edit 方法一樣。
  • 使用通過模型綁定獲取的數據,更新 Instructor 實體,除了課程 Course 導航屬性之外。

 

If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))

( 第二和第三個參數在屬性名的前面沒有前綴,而且沒有被包含的屬性列表 ),如果驗證失敗, TryUpdateModel 方法返回 false,程序將直接轉到方法最后的 return View 語句。

  • 如果辦公位置為空,設置 Instructor.OfficeAssignment 屬性為 null,在 OfficeAssignment 表相關的行將會被刪除。

 

if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
    instructorToUpdate.OfficeAssignment = null;
}
  • 將修改保存到數據庫中。

Views\Instructor\Edit.cshtml, 在 Hire 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>

運行頁面 ( 選中教師 Instructors ,點擊某個教師的 Edit 鏈接 )

修改辦公室位置的值,然后保存 Save。

新的辦公位置出現在 Index 頁面上,打開數據庫中的 OfficeAssgnment 表,可以看到表中的數據行。

運行編輯頁面,將辦公位置 Office Location 清除掉,然后保存 Save。在 Index 頁面上辦公位置將顯示為空白,在表中的行被刪除了。

6-3  在教師編輯頁面增加課程分配

教師可以教授任意數量的課程。現在你需要擴展教師的編輯頁面,增加通過一系列復選框分配可能的能力,如下所示。

在課程 Course 和教師 Instructor 之間存在多對多的關系,這意味着你不需要直接訪問表之間的關聯。而是通過增加或者刪除 Instructor. Course 實體來完成。

在 UI 界面上,與教師相關的課程被顯示為一組復選框,在數據庫中的每一門課程都有一個對應的復選框,教師當前教授的課程對應的復選框處於被選中狀態。用戶可以通過選中或者取消選中來改變教師教授的課程。如果課程的數量巨大,你可能需要采取其他的方式來顯示這些數據,但是你可以使用類似的方式來控制導航屬性以便創建和刪除關系。

為了為視圖提供復選框數據,你需要使用視圖模型 ViewModel,在 ViewModels 文件夾中創建 AssignedCourseData.cs,使用下面的代碼替換生成的代碼。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

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 導航集合屬性中。為了更加有效地遍歷教師講授的課程,將教師講授的課程通過 HashSet 集合處理,課程中教師講授的課程的 Assigned 屬性被賦予 true。在視圖中通過這個屬性來判斷復選框是否顯示為選中狀態。最后,這個集合通過 ViewBag 傳遞到視圖中。

然后,增加當用戶點擊 Save 后執行的代碼。將 HttpPost 中的 Edit 方法使用下面的代碼替換掉,這里通過調用一個新的方法將教師 Instructor 的導航屬性 Courses 更新。

[HttpPost]
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, "", null, new string[] { "Courses" }))
    {
        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)
        {
            //Log the error (add a variable name after DataException)
            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);
            }
        }
    }
}

如果沒有復選框被選中,在 UpdateInstructorCourse 方法中,使用一個空的集合來初始化 Courses 導航屬性。

if (selectedCourses == null)
{
    instructorToUpdate.Courses = new List();
    return;
}

然后,代碼遍歷數據庫中所有的課程,如果課程的復選框被選中了,但是沒有包含在教師 Insturctor 的 Courses 集合中。這個課程將會被加入到導航屬性集合中。

if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
    if (!instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.Courses.Add(course);
    }
}

如果課程沒有被選中,但是在教師的導航屬性 Courses 集合中,就從集合屬性中刪除掉。

else
{
    if (instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.Courses.Remove(course);
    }
}

在 Views\Instructor\Edit.cshtml 中,在 OfficeAssignment 區域的 div 之后,增加一個 Courses 區域,通過一組復選框來顯示課程的狀態。

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

代碼創建一個 HTML 的三列表格,每一列中顯示課程的標題和編號,后面跟着一個復選框。復選框的名稱是相同的 ( “selectedCourses” ),這樣在模型綁定的時候就會被連接成一組。復選框的 value 屬性設置為 CourseID。當提交頁面的時候,選中的復選框代表的 CourseID 將以數組的形式傳遞給控制器。

在復選框被初始化的時候,選中課程對應的復選框的 checked 屬性被設置為選中狀態。

在修改了課程狀態之后,當回到 Index 頁面的時候,需要驗證這些修改。因此,需要在頁面的表格中增加一列,在這里不需要使用 ViewBag,因為需要的信息已經通過教師實體 Instructor 的導航屬性 Courses 傳遞到頁面了。

在 Views\Instuctor\Index.cshtml 中,在 <th>Office</th> 之后,增加一個 <th>Course</th> 的標題列,如下所示。

<tr> 
    <th></th> 
    <th>Last Name</th> 
    <th>First Name</th> 
    <th>Hire Date</th> 
    <th>Office</th>
    <th>Courses</th>
</tr> 

然后,在辦公位置的單元格之后增加一個詳細內容的單元格。

<td>
    @{
        foreach (var course in item.Courses)
        {
            @course.CourseID @:  @course.Title <br />
        }
    }
</td>

運行 Instructor 的 Index 頁面,檢查教師的授課情況。

點擊 Edit 查看編輯頁面。

修改一些課程的授課情況,然后保存 Save,修改的結果在 Index 頁面中可以看到。

你已經完成了修改關聯數據的工作。通過目前的課程你已經可以完成增、刪、改、查所有的操作,但是還沒有考慮並發問題。下一次我們將探討並發問題,展示處理並發的方式,然后對已經完成的實體的增、刪、改、查的代碼增加並發處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM