教學課程:更新相關資料 - ASP.NET MVC 搭配 EF Core

在先前的教學課程中,您顯示了相關資料。在本教學課程中,您會藉由更新外部索引鍵欄位和導覽屬性來更新相關資料。

下列圖例顯示了您將操作的一些頁面。

Course Edit page

Edit Instructor page

在本教學課程中,您已:

  • 自訂 Courses 頁面
  • 新增 Instructors [編輯] 頁面
  • 將課程新增至 [編輯] 頁面
  • 更新 [刪除] 頁面
  • 將辦公室位置和課程新增至 [建立] 頁面

必要條件

自訂 Courses 頁面

當新的 Course 實體建立時,其必須要與現有的部門具有關聯性。 若要達成此目的,Scaffold 程式碼包含了控制器方法和 [建立] 和 [編輯] 檢視,當中包含了一個可選取部門的下拉式清單。 下拉式清單會設定 Course.DepartmentID 外部索引鍵屬性,以讓 Entity Framework 使用適當的 Department 實體載入 Department 導覽屬性。 您將使用 Scaffold 程式碼,但會稍微對其進行一些變更以新增錯誤處理及排序下拉式清單。

CoursesController.cs 中,刪除四個 Create 及 Edit 方法,並以下列程式碼取代:

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 方法後,建立一個新的方法,該方法會將部門資訊載入下拉式清單。

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 中的檢視。 方法接受選擇性的 selectedDepartment 參數,可允許呼叫程式碼在呈現下拉式清單時指定選取的項目。 檢視會將名稱 "DepartmentID" 傳遞到 <select> 標籤協助程式,協助程式接著便會知道要在 ViewBag 物件中尋找一個名為 "DepartmentID" 的 SelectList

HttpGet Create 方法會呼叫 PopulateDepartmentsDropDownList 方法,而不設定選取項目,因為新課程所屬的部門還未建立:

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

HttpGet Edit 方法會根據已指派給正在編輯之課程的部門識別碼來設定選取項目:

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 方法都同時包含了在錯誤之後重新顯示頁面時設定選取項目的程式碼。 這可確保當頁面重新顯示以顯示錯誤訊息時,任何已選取的部門都會維持該選取狀態。

將 .AsNoTracking 新增至 Details 及 Delete 方法

若要最佳化 Course [詳細資料] 和 [刪除] 頁面的效能,請在 Details 和 HttpGet Delete 方法中新增 AsNoTracking 呼叫。

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

Views/Courses/Create.cshtml 中,將一個「選取部門」選項新增至 [部門] 下拉式清單,將標題從 DepartmentID 變更為 Department,然後新增一個驗證訊息。

<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" />
</div>

Views/Courses/Edit.cshtml 中,為 [部門] 欄位進行您剛剛為 Create.cshtml 進行的相同變更。

同樣的,在 Views/Courses/Edit.cshtml 中,在 [標題] 欄位之前新增一個課程號碼欄位。 由於課程號碼是主索引鍵,雖然會顯示,但您無法變更它。

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

在 [編輯] 檢視中已有一個針對課程號碼的隱藏欄位 (<input type="hidden">)。 新增 <label> 標籤協助程式無法消除隱藏欄位的必要,因為它無法讓課程號碼包含在使用者按一下位於 [編輯] 頁面上的 [儲存] 時以 Post 方式提交的資料中。

Views/Courses/Delete.cshtml 中,在頂端新增一個課程號碼欄位,並將部門識別碼變更為部門名稱。

@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 所做的相同變更。

測試 [課程] 頁面

執行應用程式,選取 [Course] 索引標籤,按一下 [新建],並輸入新的課程資料:

Course Create page

按一下 [建立]。 Courses [索引] 頁面便會顯示,並且清單中已有新建立的課程。 [索引] 頁面中的部門名稱來自於導覽屬性,顯示關聯性已正確建立。

按一下 Courses [索引] 頁面中課程的 [編輯]

Course Edit page

變更頁面上的資料,然後按一下 [儲存]。 Courses [索引] 頁面便會顯示,並且清單中已有更新的課程資料。

新增 Instructors [編輯] 頁面

當您編輯講師記錄時,您可能會想要更新講師的辦公室指派。 Instructor 實體與 OfficeAssignment 實體具有一對零或一關聯性,表示您的程式碼必須處理下列狀況:

  • 若使用者清除了原先擁有值的辦公室指派,刪除 OfficeAssignment 實體。

  • 若使用者輸入了辦公室指派的值,而該指派原先是空白的,請建立新的 OfficeAssignment 實體。

  • 若使用者變更辦公室指派的值,請變更現有 OfficeAssignment 實體中的值。

更新 Instructor 控制器

InstructorsController.cs 中,變更 HttpGet Edit 方法中的程式碼,使其載入 Instructor 實體的 OfficeAssignment 導覽屬性,並呼叫 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 方法來處理辦公室指派更新:

[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);
}

程式碼會執行下列操作:

  • 將方法名稱變更為 EditPost,因為簽章目前與 HttpGet Edit 方法相同 (ActionName 屬性指出 /Edit/ URL 仍在使用中)。

  • 針對 OfficeAssignment 導覽屬性使用積極式載入從資料庫中取得目前的 Instructor 實體。 這與您在 HttpGet Edit 方法中所做的事情一樣。

  • 使用從模型繫結器取得的值更新擷取的 Instructor 實體。 TryUpdateModel 多載可讓您宣告要包含的屬性。 這可防止大量指派,如同在第二個教學課程中所解釋的。

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • 如果辦公室位置空白,請將 Instructor.OfficeAssignment 屬性設定為 null,以便刪除 OfficeAssignment 資料表中的相關資料列。

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

更新 Instructor [編輯] 檢視

Views/Instructors/Edit.cshtml 中,在 [儲存] 按鈕前的結尾處新增一個用於編輯辦公室位置的欄位:

<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] 索引標籤,然後在講師上按一下 [編輯]。 變更 [辦公室位置],然後按一下 [儲存]

Instructor Edit page

將課程新增至 [編輯] 頁面

講師可教授任何數量的課程。 現在您將藉由使用核取方塊群組,新增變更課程指派的能力來強化 Instructor [編輯] 頁面,如以下螢幕擷取畫面所示:

Instructor Edit page with courses

CourseInstructor 實體之間的關聯性為多對多。 若要新增和移除關聯性,您必須往返 CourseAssignments 聯結實體集新增和移除實體。

可讓您變更講師指派之課程的 UI 為一組核取方塊。 資料庫中每個課程的核取方塊都會顯示,而該名講師目前受指派的課程會已選取狀態顯示。 使用者可以選取或清除核取方塊來變更課程指派。 若課程數量要大上許多,您可能會想要使用不同的方法來在檢視中呈現資料,但您操縱聯結實體以建立或刪除關聯性的方法是相同的。

更新 Instructor 控制器

若要為核取方塊清單提供檢視的資料,您必須使用檢視模型類別。

SchoolViewModels 資料夾中,建立 AssignedCourseData.cs,然後使用下列程式碼取代現有的程式碼:

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 方法。 所做的變更已醒目提示。

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 方法以提供資訊給核取方塊陣列。

PopulateAssignedCourseData 方法中的程式碼會讀取所有的 Course 實體以使用檢視模型類別載入課程清單。 針對每個課程,程式碼會檢查課程是否存在於講師的 Courses 導覽屬性中。 為了在檢查課程是否已指派給講師的過程中更有效率,指派給講師的課程會放入一個 HashSet 集合中。 Assigned 屬性會針對已指派給講師的課程設定為 true。 檢視會使用這個屬性,來判斷哪一個核取方塊必須顯示為已選取。 最後,清單會傳遞至位於 ViewData 的檢視中。

接下來,新增當使用者按一下 [儲存] 時要執行的程式碼。 使用下列程式碼取代 EditPost 方法,然後新增一個方法,該方法會更新 Instructor 實體的 Courses 導覽屬性。

[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 變回 Edit

由於檢視沒有 Course 實體的集合,模型繫結器無法自動更新 CourseAssignments 導覽屬性。 相較於使用模型繫結器更新 CourseAssignments 導覽屬性,您會在新的 UpdateInstructorCourses 方法中進行相同的操作。 因此您必須從模型繫結中排除 CourseAssignments 屬性。 這不需要對呼叫 TryUpdateModel 的程式碼進行任何變更,因為您使用的是需要明確核准的多載,而 CourseAssignments 未在包含清單中。

如果沒有選取任何核取方塊,UpdateInstructorCourses 中的程式碼會使用空集合初始化 CourseAssignments 導覽屬性並傳回:

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);
            }
        }
    }
}

程式碼會執行迴圈,尋訪資料庫中所有的課程,並檢查每個已指派給講師的課程,以及在檢視中選取的課程。 為了協助達成有效率的搜尋,後者的兩個集合會儲存在 HashSet 物件中。

如果課程的核取方塊已選取,但課程並未位於 Instructor.CourseAssignments 導覽屬性中,則課程便會新增至導覽屬性的集合中。

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 導覽屬性中,則課程便會從導覽屬性的集合中移除。

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

Views/Instructors/Edit.cshtml 中,藉由將下列程式碼新增到 [辦公室] 欄位的 div 元素後及 [儲存] 按鈕的 div 元素前,來新增 [課程] 欄位與核取方塊陣列。

注意

當您將程式碼貼至 Visual Studio 時,分行符號可能會變更,而讓程式碼斷行。 如果程式碼在貼上之後看起來不同,請按 Ctrl+Z 一次以復原自動格式化。 這會修正分行符號,使他們看起來就跟您在這裡看到的一樣。 縮排不一定要是完美的,但 @:</tr><tr>@:<td>@:</td>@:</tr> 必須要如顯示般各自在獨立的一行上,否則您會接收到執行階段錯誤。 當選取新的程式碼區塊時,按下 Tab 鍵三次來讓新的程式碼對準現有的程式碼。 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 表格,該表格中有三個資料行。 在每個資料行中,核取方塊的後方會是由課程號碼和標題組成的標題。 所有核取方塊的名稱都是 ("selectedCourses"),會告知模型繫結器應將其視為一個群組。 每個核取方塊的值屬性都會設為值 CourseID。 在張貼頁面時,模型繫結器便會將只包含我們選取核取方塊 CourseID 值的陣列傳遞到控制器。

核取方塊一開始轉譯時,已指派給該名講師的課程便會帶有已勾選的屬性,使其顯示為已選取狀態。

執行應用程式,選取 [Instructor] 索引標籤,然後按一下講師上的 [編輯] 以查看 [編輯] 頁面。

Instructor Edit page with courses

變更一些課程指派,然後按一下 [儲存]。 您所做的變更會反映在 [索引] 頁面上。

注意

這裡所用來編輯講師課程資料的方法在課程的數量有限時運作相當良好。 針對更大的集合,將需要不同的 UI 和不同的更新方法。

更新 [刪除] 頁面

InstructorsController.cs 中,刪除 DeleteConfirmed 方法並在相同位置插入下列程式碼。

[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));
}

此程式碼會進行下列變更:

  • CourseAssignments 導覽屬性進行積極式載入。 您必須包含這個,否則 EF 將無法得知相關 CourseAssignment 而無法刪除他們。 若要避免在此讀取他們,您可以在資料庫中設定串聯刪除。

  • 若要刪除的講師已指派為任何部門的系統管理員,請先從部門中移除講師的指派。

將辦公室位置和課程新增至 [建立] 頁面

InstructorsController.cs 中,刪除 HttpGet 和 HttpPost Create 方法,然後在相同位置新增下列程式碼:

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 方法中看到的類似,除了一開始沒有選取任何課程之外。 HttpGet Create 方法會呼叫 PopulateAssignedCourseData 方法,不是因為可能會有已選取的課程,而是為了提供空集合給檢視中的 foreach 迴圈 (否則檢視程式碼會擲回 Null 參考例外狀況)。

HttpPost Create 方法會在檢查驗證錯誤並將新的講師新增到資料庫前將每個選取的課程新增到 CourseAssignments 導覽屬性中。 即使發生模型錯誤,課程也會新增,這使得當發生模型錯誤 (例如使用者鍵入了無效的日期),並且頁面重新顯示並帶有錯誤訊息時,任何課程選取都會自動還原。

請注意,為了要能夠將課程新增到 CourseAssignments 導覽屬性,您必須將屬性以空集合初始化:

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

作為在控制器程式碼中完成這項操作的替代方案,您可以在 Instructor 模型中藉由將屬性 getter 變更為在不存在時自動建立集合來完成,如以下範例所示:

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

若您使用這種方式修改了 CourseAssignments 屬性,您便可以移除控制器中的明確屬性初始化程式碼。

Views/Instructor/Create.cshtml 中,在 [提交] 按鈕前新增一個辦公室位置文字方塊及課程核取方塊。 若為 [編輯] 頁面,請修正 Visual Studio 於您貼上時重新格式化程式碼

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

執行應用程式並建立一名講師,以進行測試。

處理交易

如同在 CRUD 教學課程中所述,Entity Framework 隱含實作了交易。 針對您需要更多控制的案例 -- 例如,若您想要在一個交易中包含在 Entity Framework 之外完成的作業 -- 請參閱交易

取得程式碼

下載或檢視已完成的應用程式。

下一步

在本教學課程中,您已:

  • 自訂 Courses 頁面
  • 新增 Instructors [編輯] 頁面
  • 將課程新增至 [編輯] 頁面
  • 更新 [刪除] 頁面
  • 將辦公室位置和課程新增至 [建立] 頁面

若要了解如何處理並行衝突,請前往下一個教學課程。