显示 / 隐藏目录

更新关联数据 - EF Core 与 ASP.NET Core MVC 教程 (7 of 10)

作者 Tom Dykstra 、 Rick Anderson

翻译 刘怡(AlexLEWIS/Forerunner)

Contoso 大学 Web应用程序演示了如何使用 Entity Framework Core 1.1 以及 Visual Studio 2017 来创建 ASP.NET Core 1.1 MVC Web 应用程序。更多信息请参考 第一节教程.

在上一篇教程中,你学习了如何显示关联数据;在本篇教程中你讲学习如何通过更新外键字段和导航属性来更新关联数据。

以下插图显示了您将要使用的一些页面。

Course Edit page

Instructor Edit page

为 Courses 定制 Create 和 Edit 页面

当创建完一个新的 Course 实体,它就必须与现有的部门有联系。为方便起见,基架代码会包含控制其方法和含有部门选择下拉菜单的 Create 与 Edit 视图。下拉菜单设置了 Course.DepartmentID 外键属性,这是所有 Entity Framework 需要用来加载含有 Department 实体的 Department 导航属性的。你会使用到这些基架代码,但需要稍微修改一下它们,比如添加错误处理以及排序下拉列表等。

在 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("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()
        .SingleOrDefaultAsync(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
        .SingleOrDefaultAsync(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("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> Tag Helper,然后 Tag Helper 会到 ViewBag 对象中查找名为「DepartmentID」的 SelectList 集合。

HttpGet 版本的 Create 方法调用 PopulateDepartmentsDropDownList 方法时不设置选择项,是因为对于新课程而言部门尚未建立:

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

HttpGet 版本的 Edit 方法基于已分配给正在编辑的课程的部门 ID 设置选择项:

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

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

Create 和 Edit 的 HttpPost 方法还包括设置选择项的代码(当出现错误后重新显示页面时会使用到)。这就确保了当页面被重新显示并展示错误信息时,所选的部门将继续保持选中状态。

为 Details 和 Delete 方法添加预加载

要想在 Course 的 Details 页面和 Delete 页面中显示部门数据的话,当然还要在 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()
        .SingleOrDefaultAsync(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()
        .SingleOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}

修改 Course 视图

在 Views/Courses/Create.cshtml 中,为 Department 下拉列表添加「选择部门」选项,然后将字段的标题从 DepartmentID 改为 Department。

<div class="form-group">
    <label asp-for="Department" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <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>
</div>

在 Views/Courses/Edit.cshtml 中也作相同的改动(如你之前在 Create.cshtml 中所做的改动一样)。

在 Views/Courses/Edit.cshtml 中,在 Credits 字段之前添加课程编号字段。因为它是主键,所以该字段只显示、不可修改。

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

在 Edit 视图中已经有一个银行字段(<input type="hidden">)用于保存课程编号。 <label> Tag Helper 并不能代替这个隐藏字段,因为那样的话就不能在点击 Save 时 POST 包含课程编号的数据。

在 Views/Course/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="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Credits)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Credits)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Department)
        </dt>
        <dd>
            @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/Course/Details.cshtml 中也作相同的改动(如你之前在 Delete.cshtml 中所做的改动一样)。

测试 Course 页面

运行 Create 页面(显示 COurse 索引页,并点击 Create New),然后输入一门新课程:

Course Create page

点击 Create。然后你的这门课程就被添加到 Course 的索引页的列表中了。Index 页中的部门名来自于导航属性,表明关联关系以正确建立。

运行 Edit 页面(在 Course 索引页中点击 Edit)。

Course Edit page

修改页面中的数据然后点击 Save。Course 索引页就会显示更新后的课程数据。

给 Instructors 添加一个 Edit 页

当你编辑教师记录时,你希望能够更新教师的办公室分配信息。Instructor 实体与 OfficeAssignment 实体之间存在一对零或一的关系,这意味着你的代码必须处理以下几种情况:

  • 如果办公室分配数据最初有值,而用户将其清除,则请删除 OfficeAssignment 实体。
  • 如果办公室分配数据最初为空,而用户输入了一个值,则创建一个新的 OfficeAssignment 实体。
  • 如果修改了办公室分配的值,那么更新已有的 OfficeAssignment 实体中的值。

更新 Instructors 控制器

在 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()
        .SingleOrDefaultAsync(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)
        .SingleOrDefaultAsync(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("Index");
    }
    return View(instructorToUpdate);
}

这段代码做了这些事:

  • 讲方法名改为 EditPost ,因为方法签名此时与 HttpGet 版本的 Edit 方法一样了(此处依旧将 ActionName 特性指定为 /Edit/ )。
  • 为导航属性 OfficeAssignment 从数据库中预加载当前的 Instructor 实体。这和你在 HttpGet 的 Edit 方法中做的一样。
  • 用来自模型绑定器的值更新检索到的 Instructor 实体。重载的 TryUpdateModel 方法能使你将需要包含的属性列入白名单。这能防止过度发布(Over-Posting),更多解释可以查看 第二篇教程。

    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 Edit 视图

在 Views/Instructors/Edit.cshtml 中添加一个新字段用来编辑办公室位置信息,放在 Save 按钮之前:

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

运行页面(选择 Instructors 标签并在教师旁点击 Edit)。修改 Office Location 并点击 Save。

Instructor Edit page

将课程分配信息添加到 Instructor Edit 页面

每个教师可以教授多门课程,现在你将通过使用一组复选框更改课程分配的功能来增强 Instructor 的 Edit 页,如下截图所示:

Instructor Edit page with courses

Course 和 Instructor 实体之间的关系是多对多关系。要添加和删除关系,你需要添加实体类到 InstructorCourses 连接实体集/从 InstructorCourses 连接实体集中移除。

能让你更新教师分配给哪些课程的用户界面是一组复选框。库中每一门课程都会显示有一个复选框,教师当前分配到的课程的复选框默认选中。用户可以选中或取消选中复选框来改变课程的分配。如果课程的数量太大,你可能需要使用不同的方法在视图中呈现数据,但你可以使用相同的方法来操作连接实体以创建或删除关系。

更新 Instructors 控制器

为了能给视图中的复选框列表提供数据,你必须使用视图模型类。

在 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()
        .SingleOrDefaultAsync(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 导航属性添加了预加载,并调用新的 PopulateAssignedCourseData 方法,以为使用 AssignedCourseData 视图模型类的复选框数组提供信息。

PopulateAssignedCourseData 方法中的代码读取所有 Course 实体,用于使用视图模型类加载课程列表。对于每一门课程,代码都会检查课程是否存在于教师的 Courses 导航属性之中。为了在检查课程是否被分配给教师的过程中尽可能高效,分配给教师的课程被放入一个 HashSet 集合中。如果课程已被分配给教师,则 Assigned 属性置为 true。视图将使用此属性来确定那些复选框必须显示为已选中的状态。最后,列表通过 ViewData 被传递给视图。

接下来,添加点击 Save 后的执行代码。用下面代码替换 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)
        .SingleOrDefaultAsync(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("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.SingleOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

由于方法签名现在有别于 HttpGet 版本的 Edit 方法,因此可以将方法名从 EditPost 改回 Edit。

由于没有 Course 实体的集合,模型绑定器不能自动更新 CourseAssignments 导航属性。所以你需要在新的 UpdateInstructorCourses 方法中实现 CourseAssignments 导航属性的更新。因此你需要从模型绑定器中排除 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.SingleOrDefault(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.SingleOrDefault(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.SingleOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

更新 Instructor 视图

在 Views/Instructors/Edit.cshtml 中,通过在 Office 字段的Office 元素之后添加以下代码,在复选框的数组中添加一个 Courses 字段。 div 元素为 保存 按钮。

[!NOTE] > Open the file in a text editor such as Notepad to make this change. If you use Visual Studio, line breaks will be changed in a way that breaks the code. If that happens, fix the line breaks so that they look like what you see here. The indentation doesn't have to be perfect, but the `@`, `@:`, `@:`, and `@:` lines must each be on a single line as shown or you'll get a runtime error. After editing the file in a text editor, you can open it in Visual Studio, highlight the block of new code, and press Tab twice to line up the new code with the existing code.-->
备注

在 Notepad 之类的文本编辑器中打开文件并做如上修改。如果你使用 Visual Studio,换行符会在某种程度上破坏代码。如果发生这种情况,请修复换行符,使其看起来如你在此处所看到的。缩进不需要很完美,但 @</tr><tr>、@:<td>、@:</td> 以及 @:</tr> 行必须放置在单独一行上,如图所示,不然会出现运行时错误。在文本编辑器中修改之后,你可以在 Visual Studio 中打开,高亮新代码块并按两次 Tab 键实使新老代码对齐。

<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 表格。在每一列上都有一个复选框,后面跟着一门包含课程编号和课程标题的文字。复选框都具有相同的名称(name 属性,「selectedCourses」),它将告诉模型帮顶起将它们视作一组。每一个复选框的值都被设置为 CourseID 的值。当页面被 POST,模型绑定器将一个数组传递给控制器,该数组只包含所有选中的复选框的 CourseID 值。

之前渲染的复选框带有 checked 属性,显示为选中的表示教师选择教授这门课程。

运行 Instructor 索引页,点击某个教师胖的 Edit 就能看到(该教师的)Edit 页面。

Instructor Edit page with courses

修改一些课程分配信息,然后点击 Save。你做的更新将反映在 Index 页上。

[!NOTE] > The approach taken here to edit instructor course data works well when there is a limited number of courses. For collections that are much larger, a different UI and a different updating method would be required.-->
备注

当课程数量有限时,在这里编辑教师课程数据会更好一些。对于大集合,需要用不同的 UI 和不同的更新方法。

更新 Delete 页面

在 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("Index");
}

这段代码做了如下改变:

  • 预加载 CourseAssignments 导航属性。你必须加上这段,不然 EF 不会知道关联的 CourseAssignments 实体,并且不会删除它们。为避免在此处读取它们,你需要到数据库中配置级联删除。

  • 如果要被删除的教师已被分配为某个部门的管理员,那么就从这些部门中删除该教师的分配信息。

将办公室地点和课程添加到 Create 页面

在 InstructorController.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("Index");
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

除了最初没有选择课程,这段代码与之前在 Edit 方法中看到的代码非常相似。HttpGet 请求的 Create 方法调用 PopulateAssignedCourseData 方法并不是因为可能会有课程被选择,而是为了给 foreach 循环提供一个空集合(避免在视图代码中出现空引用异常)。

HttpPost 请求的 Create 方法在检查校验错误并将教师插入数据库之前将每个被选择的课程添加到 CourseAssignments 导航属性中。即使模型出现错误,也会添加课程,以便当出现错误(例如用户键入了无效日期)且页面重新显示出错误信息时,所有课程选择都将被自动还原。

注意,为了能将课程添加到 CourseAssignments 导航属性中,你必须将属性初始化为一个空集合:

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

作为在控制器代码中执行此操作的替代方法,你可以通过修改 Instructor 模型中属性 Getter 访问器,当集合不存在的时候给它自动创建一个,这样该操作就能放入 Instructor 了,如下例所示:

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

如果你这么改 CourseAssignments 属性,可以删除控制器中显示属性的初始化代码。

在 Views/Instructor/Create.cshtml 中添加一个表示「办公室位置」的文本框,并在「出租日期」字段和提交(Submit)按钮之间添加课程复选框。和在 Edit 页面中的情况一样,如果你用 Notepad 这类文本软件来编辑可能会更好些。

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="col-md-2 control-label"></label>
    <div class="col-md-10">
        <input asp-for="OfficeAssignment.Location" class="form-control" />
        <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
    </div>
</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>

测试一下看看:运行 Create 页面,然后添加一个教师。

处理事务

如在 CRUD 教程 中所解释的,Entity Framework 会隐式地实现事务。对于需要更多控制的场景——比如,如果你想在事务中包含 Entity Framework 之外的操作——具体可以参见 事务。

总结

你已完成使用关联数据的全部介绍,在下一篇教程中你将学习如何处理并发冲突。

上一节 下一节

  • 改进文档
  • 0 评论
返回顶部 Copyright © 2015-2017 Microsoft
Generated by DocFX