教學課程:使用 EF Core 更新相關資料-ASP.NET MVCTutorial: Update related data - ASP.NET MVC with EF Core

在先前的教學課程中,您顯示了相關資料。在本教學課程中,您會藉由更新外部索引鍵欄位和導覽屬性來更新相關資料。In the previous tutorial you displayed related data; in this tutorial you'll update related data by updating foreign key fields and navigation properties.

下列圖例顯示了您將操作的一些頁面。The following illustrations show some of the pages that you'll work with.

Course [編輯] 頁面

Instructor [編輯] 頁面

在本教學課程中,您:In this tutorial, you:

  • 自訂 Courses 頁面Customize Courses pages
  • 新增 Instructors [編輯] 頁面Add Instructors Edit page
  • 將課程新增至 [編輯] 頁面Add courses to Edit page
  • 更新 [刪除] 頁面Update Delete page
  • 將辦公室位置和課程新增至 [建立] 頁面Add office location and courses to Create page

PrerequisitesPrerequisites

自訂 Courses 頁面Customize Courses pages

當新的課程實體建立時,其必須要與現有的部門具有關聯性。When a new course entity is created, it must have a relationship to an existing department. 若要達成此目的,Scaffold 程式碼包含了控制器方法和 [建立] 和 [編輯] 檢視,當中包含了一個可選取部門的下拉式清單。To facilitate this, the scaffolded code includes controller methods and Create and Edit views that include a drop-down list for selecting the department. 下拉式清單會設定 Course.DepartmentID 外部索引鍵屬性,以讓 Entity Framework 使用適當的 Department 實體載入 Department 導覽屬性。The drop-down list sets the Course.DepartmentID foreign key property, and that's all the Entity Framework needs in order to load the Department navigation property with the appropriate Department entity. 您將使用 Scaffold 程式碼,但會稍微對其進行一些變更以新增錯誤處理及排序下拉式清單。You'll use the scaffolded code, but change it slightly to add error handling and sort the drop-down list.

CoursesController.cs 中,刪除四個 Create 及 Edit 方法,並以下列程式碼取代:In CoursesController.cs, delete the four Create and Edit methods and replace them with the following code:

public IActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
    if (ModelState.IsValid)
    {
        _context.Add(course);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var courseToUpdate = await _context.Courses
        .FirstOrDefaultAsync(c => c.CourseID == id);

    if (await TryUpdateModelAsync<Course>(courseToUpdate,
        "",
        c => c.Credits, c => c.DepartmentID, c => c.Title))
    {
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

Edit HttpPost 方法後,建立一個新的方法,該方法會將部門資訊載入下拉式清單。After the Edit HttpPost method, create a new method that loads department info for the drop-down list.

private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
    var departmentsQuery = from d in _context.Departments
                           orderby d.Name
                           select d;
    ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);
}

PopulateDepartmentsDropDownList 方法會取得依照名稱排序的所有部門清單,為下拉式清單建立 SelectList 集合,然後將集合傳遞給位於 ViewBag 中的檢視。The PopulateDepartmentsDropDownList method gets a list of all departments sorted by name, creates a SelectList collection for a drop-down list, and passes the collection to the view in ViewBag. 方法接受選擇性的 selectedDepartment 參數,可允許呼叫程式碼在呈現下拉式清單時指定選取的項目。The method accepts the optional selectedDepartment parameter that allows the calling code to specify the item that will be selected when the drop-down list is rendered. 檢視會將名稱 "DepartmentID" 傳遞到 <select> 標籤協助程式,協助程式接著便會知道要在 ViewBag 物件中尋找一個名為 "DepartmentID" 的 SelectListThe view will pass the name "DepartmentID" to the <select> tag helper, and the helper then knows to look in the ViewBag object for a SelectList named "DepartmentID".

HttpGet Create 方法會呼叫 PopulateDepartmentsDropDownList 方法,而不設定選取項目,因為新課程所屬的部門還未建立:The HttpGet Create method calls the PopulateDepartmentsDropDownList method without setting the selected item, because for a new course the department isn't established yet:

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

HttpGet Edit 方法會根據已指派給正在編輯之課程的部門識別碼來設定選取項目:The HttpGet Edit method sets the selected item, based on the ID of the department that's already assigned to the course being edited:

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

CreateEdit 的 HttpPost 方法都同時包含了在錯誤之後重新顯示頁面時設定選取項目的程式碼。The HttpPost methods for both Create and Edit also include code that sets the selected item when they redisplay the page after an error. 這可確保當頁面重新顯示以顯示錯誤訊息時,任何已選取的部門都會維持該選取狀態。This ensures that when the page is redisplayed to show the error message, whatever department was selected stays selected.

將 .AsNoTracking 新增至 Details 及 Delete 方法Add .AsNoTracking to Details and Delete methods

若要最佳化 Course [詳細資料] 和 [刪除] 頁面的效能,請在 Details 和 HttpGet Delete 方法中新增 AsNoTracking 呼叫。To optimize performance of the Course Details and Delete pages, add AsNoTracking calls in the Details and HttpGet Delete methods.

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}

修改 Course 檢視Modify the Course views

Views/Courses/Create.cshtml 中,將一個「選取部門」選項新增至 [部門]**** 下拉式清單,將標題從 DepartmentID 變更為 Department,然後新增一個驗證訊息。In Views/Courses/Create.cshtml, add a "Select Department" option to the Department drop-down list, change the caption from DepartmentID to Department, and add a validation message.

<div class="form-group">
    <label asp-for="Department" class="control-label"></label>
    <select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="DepartmentID" class="text-danger" />

Views/Courses/Edit.cshtml 中,為 [部門] 欄位進行您剛剛為 Create.cshtml 進行的相同變更。In Views/Courses/Edit.cshtml, make the same change for the Department field that you just did in Create.cshtml.

同樣的,在 Views/Courses/Edit.cshtml 中,在 [標題]**** 欄位之前新增一個課程號碼欄位。Also in Views/Courses/Edit.cshtml, add a course number field before the Title field. 由於課程號碼是主索引鍵,雖然會顯示,但您無法變更它。Because the course number is the primary key, it's displayed, but it can't be changed.

<div class="form-group">
    <label asp-for="CourseID" class="control-label"></label>
    <div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

在 [編輯] 檢視中已有一個針對課程號碼的隱藏欄位 (<input type="hidden">)。There's already a hidden field (<input type="hidden">) for the course number in the Edit view. 新增 <label> 標籤協助程式無法消除隱藏欄位的必要,因為它無法讓課程號碼包含在使用者按一下位於 [編輯]**** 頁面上的 [儲存]**** 時以 Post 方式提交的資料中。Adding a <label> tag helper doesn't eliminate the need for the hidden field because it doesn't cause the course number to be included in the posted data when the user clicks Save on the Edit page.

Views/Courses/Delete.cshtml 中,在頂端新增一個課程號碼欄位,並將部門識別碼變更為部門名稱。In Views/Courses/Delete.cshtml, add a course number field at the top and change department ID to department name.

@model ContosoUniversity.Models.Course

@{
    ViewData["Title"] = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

Views/Courses/Details.cshtml 中,進行您方才對 Delete.cshtml 進行的相同變更。In Views/Courses/Details.cshtml, make the same change that you just did for Delete.cshtml.

測試 Course 頁面Test the Course pages

執行應用程式,選取 [Course]**** 索引標籤,按一下 [新建]****,並輸入新的課程資料:Run the app, select the Courses tab, click Create New, and enter data for a new course:

Course [建立] 頁面

按一下 [建立]。Click Create. Courses [索引] 頁面便會顯示,並且清單中已有新建立的課程。The Courses Index page is displayed with the new course added to the list. [索引] 頁面中的部門名稱來自於導覽屬性,顯示關聯性已正確建立。The department name in the Index page list comes from the navigation property, showing that the relationship was established correctly.

按一下 Courses [索引] 頁面中課程的 [編輯]****。Click Edit on a course in the Courses Index page.

Course [編輯] 頁面

變更頁面上的資料,然後按一下 [儲存]****。Change data on the page and click Save. Courses [索引] 頁面便會顯示,並且清單中已有更新的課程資料。The Courses Index page is displayed with the updated course data.

新增 Instructors [編輯] 頁面Add Instructors Edit page

當您編輯講師記錄時,您可能會想要更新講師的辦公室指派。When you edit an instructor record, you want to be able to update the instructor's office assignment. Instructor 實體與 OfficeAssignment 實體具有一對零或一關聯性,表示您的程式碼必須處理下列狀況:The Instructor entity has a one-to-zero-or-one relationship with the OfficeAssignment entity, which means your code has to handle the following situations:

  • 若使用者清除了原先擁有值的辦公室指派,刪除 OfficeAssignment 實體。If the user clears the office assignment and it originally had a value, delete the OfficeAssignment entity.

  • 若使用者輸入了辦公室指派的值,而該指派原先是空白的,請建立新的 OfficeAssignment 實體。If the user enters an office assignment value and it originally was empty, create a new OfficeAssignment entity.

  • 若使用者變更辦公室指派的值,請變更現有 OfficeAssignment 實體中的值。If the user changes the value of an office assignment, change the value in an existing OfficeAssignment entity.

更新 Instructor 控制器Update the Instructors controller

InstructorsController.cs 中,變更 HttpGet Edit 方法中的程式碼,使其載入 Instructor 實體的 OfficeAssignment 導覽屬性,並呼叫 AsNoTrackingIn InstructorsController.cs, change the code in the HttpGet Edit method so that it loads the Instructor entity's OfficeAssignment navigation property and calls AsNoTracking:

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    return View(instructor);
}

使用下列程式碼取代 HttpPost Edit 方法來處理辦公室指派更新:Replace the HttpPost Edit method with the following code to handle office assignment updates:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .FirstOrDefaultAsync(s => s.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    return View(instructorToUpdate);
}

此程式碼會執行下列動作:The code does the following:

  • 將方法名稱變更為 EditPost,因為簽章目前與 HttpGet Edit 方法相同 (ActionName 屬性指出 /Edit/ URL 仍在使用中)。Changes the method name to EditPost because the signature is now the same as the HttpGet Edit method (the ActionName attribute specifies that the /Edit/ URL is still used).

  • 針對 OfficeAssignment 導覽屬性使用積極式載入從資料庫中取得目前的 Instructor 實體。Gets the current Instructor entity from the database using eager loading for the OfficeAssignment navigation property. 這與您在 HttpGet Edit 方法中所做的事情一樣。This is the same as what you did in the HttpGet Edit method.

  • 使用從模型繫結器取得的值更新擷取的 Instructor 實體。Updates the retrieved Instructor entity with values from the model binder. 多載可 TryUpdateModel 讓您宣告要包含的屬性。The TryUpdateModel overload enables you to declare the properties you want to include. 這可防止大量指派,如同在第二個教學課程中所解釋的。This prevents over-posting, as explained in the second tutorial.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • 若辦公室位置為空白,將 Instructor.OfficeAssignment 屬性設為 Null,以刪除在 OfficeAssignment 資料表中的相關資料列。If the office location is blank, sets the Instructor.OfficeAssignment property to null so that the related row in the OfficeAssignment table will be deleted.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • 將變更儲存到資料庫。Saves the changes to the database.

更新 Instructor [編輯] 檢視Update the Instructor Edit view

Views/Instructors/Edit.cshtml 中,在 [儲存]**** 按鈕前的結尾處新增一個用於編輯辦公室位置的欄位:In Views/Instructors/Edit.cshtml, add a new field for editing the office location, at the end before the Save button:

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

執行應用程式,選取 [Instructor]**** 索引標籤,然後在講師上按一下 [編輯]****。Run the app, select the Instructors tab, and then click Edit on an instructor. 變更 [辦公室位置]****,然後按一下 [儲存]****。Change the Office Location and click Save.

Instructor [編輯] 頁面

將課程新增至 [編輯] 頁面Add courses to Edit page

講師可教授任何數量的課程。Instructors may teach any number of courses. 現在您將藉由使用核取方塊群組,新增變更課程指派的能力來強化 Instructor [編輯] 頁面,如以下螢幕擷取畫面所示:Now you'll enhance the Instructor Edit page by adding the ability to change course assignments using a group of check boxes, as shown in the following screen shot:

Instructor [編輯] 頁面與課程

Course 與 Instructor 實體的關係為多對多。The relationship between the Course and Instructor entities is many-to-many. 若要新增和移除關聯性,您必須在 CourseAssignments 聯結實體集中新增和移除實體。To add and remove relationships, you add and remove entities to and from the CourseAssignments join entity set.

可讓您變更講師指派之課程的 UI 為一組核取方塊。The UI that enables you to change which courses an instructor is assigned to is a group of check boxes. 資料庫中每個課程的核取方塊都會顯示,而該名講師目前受指派的課程會已選取狀態顯示。A check box for every course in the database is displayed, and the ones that the instructor is currently assigned to are selected. 使用者可選取或清除核取方塊來變更課程指派。The user can select or clear check boxes to change course assignments. 若課程數量要大上許多,您可能會想要使用不同的方法來在檢視中呈現資料,但您操縱聯結實體以建立或刪除關聯性的方法是相同的。If the number of courses were much greater, you would probably want to use a different method of presenting the data in the view, but you'd use the same method of manipulating a join entity to create or delete relationships.

更新 Instructor 控制器Update the Instructors controller

若要針對核取方塊清單提供資料給檢視,您必須使用一個檢視模型類別。To provide data to the view for the list of check boxes, you'll use a view model class.

SchoolViewModels 資料夾中建立 AssignedCourseData.cs,然後以下列程式碼取代現有的程式碼:Create AssignedCourseData.cs in the SchoolViewModels folder and replace the existing code with the following code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}

InstructorsController.cs 中,使用下列程式碼取代 HttpGet Edit 方法。In InstructorsController.cs, replace the HttpGet Edit method with the following code. 所做的變更已醒目提示。The changes are highlighted.

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = _context.Courses;
    var instructorCourses = new HashSet<int>(instructor.CourseAssignments.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)
        });
    }
    ViewData["Courses"] = viewModel;
}

程式碼會為 Courses 導覽屬性新增積極式載入,然後使用 AssignedCourseData 檢視模型類別來呼叫新的 PopulateAssignedCourseData 方法以提供資訊給核取方塊陣列。The code adds eager loading for the Courses navigation property and calls the new PopulateAssignedCourseData method to provide information for the check box array using the AssignedCourseData view model class.

PopulateAssignedCourseData 方法中的程式碼會讀取所有的 Course 實體以使用檢視模型類別載入課程清單。The code in the PopulateAssignedCourseData method reads through all Course entities in order to load a list of courses using the view model class. 針對每個課程,程式碼會檢查課程是否存在於講師的 Courses 導覽屬性中。For each course, the code checks whether the course exists in the instructor's Courses navigation property. 為了在檢查課程是否已指派給講師的過程中更有效率,指派給講師的課程會放入一個 HashSet 集合中。To create efficient lookup when checking whether a course is assigned to the instructor, the courses assigned to the instructor are put into a HashSet collection. Assigned 屬性會針對已指派給講師的課程設定為 true。The Assigned property is set to true for courses the instructor is assigned to. 檢視會使用這個屬性,來判斷哪一個核取方塊必須顯示為已選取。The view will use this property to determine which check boxes must be displayed as selected. 最後,清單會傳遞至位於 ViewData 的檢視中。Finally, the list is passed to the view in ViewData.

接下來,新增當使用者按一下 [儲存]**** 時要執行的程式碼。Next, add the code that's executed when the user clicks Save. 使用下列程式碼取代 EditPost 方法,然後新增一個方法,該方法會更新 Instructor 實體的 Courses 導覽屬性。Replace the EditPost method with the following code, and add a new method that updates the Courses navigation property of the Instructor entity.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
        .FirstOrDefaultAsync(m => m.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        UpdateInstructorCourses(selectedCourses, instructorToUpdate);
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    UpdateInstructorCourses(selectedCourses, instructorToUpdate);
    PopulateAssignedCourseData(instructorToUpdate);
    return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

方法簽章現在已和 HttpGet Edit 方法不同,因此方法名稱會從 EditPost 變回 EditThe method signature is now different from the HttpGet Edit method, so the method name changes from EditPost back to Edit.

由於檢視沒有 Course 實體的集合,模型繫結器無法自動更新 CourseAssignments 導覽屬性。Since the view doesn't have a collection of Course entities, the model binder can't automatically update the CourseAssignments navigation property. 相較於使用模型繫結器更新 CourseAssignments 導覽屬性,您會在新的 UpdateInstructorCourses 方法中進行相同的操作。Instead of using the model binder to update the CourseAssignments navigation property, you do that in the new UpdateInstructorCourses method. 因此,您需要 CourseAssignments 從模型系結中排除屬性。Therefore, you need to exclude the CourseAssignments property from model binding. 這並不需要對呼叫的程式碼進行任何變更, TryUpdateModel 因為您使用的多載需要明確核准,而且 CourseAssignments 不在包含清單中。This doesn't require any change to the code that calls TryUpdateModel because you're using the overload that requires explicit approval and CourseAssignments isn't in the include list.

若沒有選取任何核取方塊,UpdateInstructorCourses 中的程式碼會使用空集合初始化 CourseAssignments 導覽屬性並傳回:If no check boxes were selected, the code in UpdateInstructorCourses initializes the CourseAssignments navigation property with an empty collection and returns:

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

程式碼會執行迴圈,尋訪資料庫中所有的課程,並檢查每個已指派給講師的課程,以及在檢視中選取的課程。The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the view. 為了協助達成有效率的搜尋,後者的兩個集合會儲存在 HashSet 物件中。To facilitate efficient lookups, the latter two collections are stored in HashSet objects.

若課程的核取方塊已被選取,但課程並未位於 Instructor.CourseAssignments 導覽屬性中,則課程便會新增至導覽屬性的集合中。If the check box for a course was selected but the course isn't in the Instructor.CourseAssignments navigation property, the course is added to the collection in the navigation property.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

若課程的核取方塊未被選取,但課程卻位於 Instructor.CourseAssignments 導覽屬性中,則課程便會從導覽屬性的集合中移除。If the check box for a course wasn't selected, but the course is in the Instructor.CourseAssignments navigation property, the course is removed from the navigation property.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

更新 Instructor 檢視Update the Instructor views

Views/Instructors/Edit.cshtml 中,藉由將下列程式碼新增到 [辦公室]**** 欄位的 div 項目後及 [儲存]**** 按鈕的 div 項目前,來新增 [課程 ]**** 欄位與核取方塊陣列。In Views/Instructors/Edit.cshtml, add a Courses field with an array of check boxes by adding the following code immediately after the div elements for the Office field and before the div element for the Save button.

注意

當您將程式碼貼至 Visual Studio 時,分行符號可能會變更,而讓程式碼斷行。When you paste the code in Visual Studio, line breaks might be changed in a way that breaks the code. 如果程式碼在貼上之後看起來不同,請按 Ctrl+Z 一次以復原自動格式化。If the code looks different after pasting, press Ctrl+Z one time to undo the automatic formatting. 這會修正分行符號,使他們看起來就跟您在這裡看到的一樣。This will fix the line breaks so that they look like what you see here. 縮排不一定要是完美的,但 @:</tr><tr>@:<td>@:</td>@:</tr> 必須要如顯示般各自在獨立的一行上,否則您會接收到執行階段錯誤。The indentation doesn't have to be perfect, but the @:</tr><tr>, @:<td>, @:</td>, and @:</tr> lines must each be on a single line as shown or you'll get a runtime error. 當選取新的程式碼區塊時,按 Tab 鍵三次來讓新的程式碼對準現有的程式碼。With the block of new code selected, press Tab three times to line up the new code with the existing code. Visual Studio 2019 已修正這個問題。This problem is fixed in Visual Studio 2019.

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.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>

此程式碼會建立一個 HTML 表格,該表格中有三個資料行。This code creates an HTML table that has three columns. 在每個資料行中,核取方塊的後方會是由課程號碼和標題組成的標題。In each column is a check box followed by a caption that consists of the course number and title. 所有核取方塊的名稱都是 ("selectedCourses"),會告知模型繫結器應將其視為一個群組。The check boxes all have the same name ("selectedCourses"), which informs the model binder that they're to be treated as a group. 每個核取方塊的 Value 屬性都會設為 CourseID 的值。The value attribute of each check box is set to the value of CourseID. 當頁面以 post 方式提交時,模型繫結器便會將只包含我們選取核取方塊之 CourseID 值的陣列傳遞到控制器。When the page is posted, the model binder passes an array to the controller that consists of the CourseID values for only the check boxes which are selected.

核取方塊一開始呈現時,已指派給該名講師的課程便會帶有 Checked 屬性,使其顯示為已選取狀態。When the check boxes are initially rendered, those that are for courses assigned to the instructor have checked attributes, which selects them (displays them checked).

執行應用程式,選取 [Instructor]**** 索引標籤,然後按一下講師上的 [編輯]**** 以查看 [編輯]**** 頁面。Run the app, select the Instructors tab, and click Edit on an instructor to see the Edit page.

Instructor [編輯] 頁面與課程

變更一些課程指派,然後按一下 [儲存]。Change some course assignments and click Save. 您所做的變更會反映在 [索引] 頁面上。The changes you make are reflected on the Index page.

注意

這裡所用來編輯講師課程資料的方法在課程的數量有限時運作相當良好。The approach taken here to edit instructor course data works well when there's a limited number of courses. 針對更大的集合,將需要不同的 UI 和不同的更新方法。For collections that are much larger, a different UI and a different updating method would be required.

更新 [刪除] 頁面Update Delete page

InstructorsController.cs 中,刪除 DeleteConfirmed 方法並在相同位置插入下列程式碼。In InstructorsController.cs, delete the DeleteConfirmed method and insert the following code in its place.

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    Instructor instructor = await _context.Instructors
        .Include(i => i.CourseAssignments)
        .SingleAsync(i => i.ID == id);

    var departments = await _context.Departments
        .Where(d => d.InstructorID == id)
        .ToListAsync();
    departments.ForEach(d => d.InstructorID = null);

    _context.Instructors.Remove(instructor);

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

此程式碼會進行下列變更:This code makes the following changes:

  • CourseAssignments 導覽屬性進行積極式載入。Does eager loading for the CourseAssignments navigation property. 您必須包含這個,否則 EF 將無法得知相關 CourseAssignment 而無法刪除他們。You have to include this or EF won't know about related CourseAssignment entities and won't delete them. 若要避免在此讀取他們,您可以在資料庫中設定串聯刪除。To avoid needing to read them here you could configure cascade delete in the database.

  • 若要刪除的講師已指派為任何部門的系統管理員,請先從部門中移除講師的指派。If the instructor to be deleted is assigned as administrator of any departments, removes the instructor assignment from those departments.

將辦公室位置和課程新增至 [建立] 頁面Add office location and courses to Create page

InstructosController.cs 中,刪除 HttpGet 和 HttpPost Create 方法,然後在相同位置新增下列程式碼:In InstructorsController.cs, delete the HttpGet and HttpPost Create methods, and then add the following code in their place:

public IActionResult Create()
{
    var instructor = new Instructor();
    instructor.CourseAssignments = new List<CourseAssignment>();
    PopulateAssignedCourseData(instructor);
    return View();
}

// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor instructor, string[] selectedCourses)
{
    if (selectedCourses != null)
    {
        instructor.CourseAssignments = new List<CourseAssignment>();
        foreach (var course in selectedCourses)
        {
            var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID = int.Parse(course) };
            instructor.CourseAssignments.Add(courseToAdd);
        }
    }
    if (ModelState.IsValid)
    {
        _context.Add(instructor);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

此程式碼與您在 Edit 方法中看到的類似,除了一開始沒有選取任何課程之外。This code is similar to what you saw for the Edit methods except that initially no courses are selected. HttpGet Create 方法會呼叫 PopulateAssignedCourseData 方法,不是因為可能會有已選取的課程,而是為了提供空集合給檢視中的 foreach 迴圈 (否則檢視程式碼會擲回 Null 參考例外狀況)。The HttpGet Create method calls the PopulateAssignedCourseData method not because there might be courses selected but in order to provide an empty collection for the foreach loop in the view (otherwise the view code would throw a null reference exception).

HttpPost Create 方法會在檢查驗證錯誤並將新的講師新增到資料庫前將每個選取的課程新增到 CourseAssignments 導覽屬性中。The HttpPost Create method adds each selected course to the CourseAssignments navigation property before it checks for validation errors and adds the new instructor to the database. 即使發生模型錯誤,課程也會新增,這使得當發生模型錯誤 (例如使用者鍵入了無效的日期),並且頁面重新顯示並帶有錯誤訊息時,任何課程選取都會自動還原。Courses are added even if there are model errors so that when there are model errors (for an example, the user keyed an invalid date), and the page is redisplayed with an error message, any course selections that were made are automatically restored.

請注意,為了要能夠將課程新增到 CourseAssignments 導覽屬性,您必須將屬性以空集合初始化:Notice that in order to be able to add courses to the CourseAssignments navigation property you have to initialize the property as an empty collection:

instructor.CourseAssignments = new List<CourseAssignment>();

作為在控制器程式碼中完成這項操作的替代方案,您可以在 Instructor 模型中藉由將屬性 getter 變更為在不存在時自動建立集合來完成,如以下範例所示:As an alternative to doing this in controller code, you could do it in the Instructor model by changing the property getter to automatically create the collection if it doesn't exist, as shown in the following example:

private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
    get
    {
        return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
    }
    set
    {
        _courseAssignments = value;
    }
}

若您使用這種方式修改了 CourseAssignments 屬性,您便可以移除控制器中的明確屬性初始化程式碼。If you modify the CourseAssignments property in this way, you can remove the explicit property initialization code in the controller.

Views/Instructor/Create.cshtml 中,在 [提交] 按鈕前新增一個辦公室位置文字方塊及課程核取方塊。In Views/Instructor/Create.cshtml, add an office location text box and check boxes for courses before the Submit button. 若為 [編輯] 頁面,請修正 Visual Studio 於您貼上時重新格式化程式碼As in the case of the Edit page, fix the formatting if Visual Studio reformats the code when you paste it.

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.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>

執行應用程式並建立一名講師,以進行測試。Test by running the app and creating an instructor.

處理交易Handling Transactions

如同在 CRUD 教學課程中所述,Entity Framework 隱含實作了交易。As explained in the CRUD tutorial, the Entity Framework implicitly implements transactions. 針對您需要更多控制的案例 -- 例如,若您想要在一個交易中包含在 Entity Framework 之外完成的作業 -- 請參閱交易For scenarios where you need more control -- for example, if you want to include operations done outside of Entity Framework in a transaction -- see Transactions.

取得程式碼Get the code

下載或檢視已完成的應用程式。Download or view the completed application.

後續步驟Next steps

在本教學課程中,您:In this tutorial, you:

  • 自訂 Courses 頁面Customized Courses pages
  • 新增 Instructors [編輯] 頁面Added Instructors Edit page
  • 將課程新增至 [編輯] 頁面Added courses to Edit page
  • 更新 [刪除] 頁面Updated Delete page
  • 將辦公室位置和課程新增至 [建立] 頁面Added office location and courses to Create page

若要了解如何處理並行衝突,請前往下一個教學課程。Advance to the next tutorial to learn how to handle concurrency conflicts.