Руководство. Обновление связанных данных — ASP.NET MVC с помощью EF Core

В предыдущем руководстве вы отобразили связанные данные. В этом руководстве описано обновление связанных данных путем обновления полей внешнего ключа и свойств навигации.

На следующих рисунках изображены некоторые из страниц, с которыми вы будете работать.

Course Edit page

Edit Instructor page

Изучив это руководство, вы:

  • Настройка страниц курсов
  • Добавление страницы редактирования данных о преподавателях
  • Добавление курсов на страницу редактирования
  • Обновление страницы удаления
  • Добавление расположения кабинета и курсов на страницу создания

Необходимые компоненты

Настройка страниц курсов

Создаваемая сущность Course должна иметь связь с существующей кафедрой. Чтобы упростить эту задачу, шаблонный код включает методы контроллеров, а также представления "Create" (Создание) и "Edit" (Редактирование) с раскрывающимся списком для выбора кафедры. Раскрывающийся список задает свойство внешнего ключа Course.DepartmentID, и это все, что нужно Entity Framework для загрузки свойства навигации Department с соответствующей сущностью Department. Вы будете использовать этот шаблонный код, немного его изменив, чтобы добавить обработку ошибок и сортировку раскрывающегося списка.

Удалите CoursesController.csчетыре метода создания и редактирования и замените их следующим кодом:

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

После метода HttpPost Edit создайте метод, загружающий сведения о кафедре для раскрывающегося списка.

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 коллекцию SelectList с именем "DepartmentID".

Метод 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);
}

Методы HttpPost для Create и Edit также содержат код, который задает выбранный элемент, когда они повторно отображают страницу после ошибки. Это гарантирует, что при повторном отображении страницы для вывода сообщения об ошибке сохраняется выбор кафедры.

Добавление .AsNoTrackin в методы Details и Delete

Чтобы оптимизировать производительность страниц "Details" (Сведения) и "Delete" (Удаление) курса, добавьте вызовы AsNoTracking в методы Details и HttpGet Delete.

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

Изменение представлений курса

Добавьте Views/Courses/Create.cshtmlв раскрывающийся список "Выбор отдела", измените подпись с DepartmentID на Отдел и добавьте сообщение проверки.

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

Представление "Edit" (Редактирование) уже содержит скрытое поле (<input type="hidden">) для номера курса. Добавление вспомогательной функции тегов <label> не устраняет потребность в этом скрытом поле, так как не приводит к включению номера курса в передаваемые данные, когда пользователь нажимает кнопку Save (Сохранить) на странице Edit (Редактирование).

Добавьте 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.

Тестирование страниц курса

Запустите приложение, выберите вкладку Courses (Курсы), щелкните Create New (Создать) и введите данные для нового курса:

Course Create page

Нажмите кнопку Создать. Отображается страница индекса курсов, где в список добавлен новый курс. Название кафедры в списке страницы индекса поступает из свойства навигации, показывая, что связь установлена правильно.

Нажмите кнопку Edit (Изменить) на странице индекса курсов.

Course Edit page

Измените данные на странице и нажмите кнопку Save (Сохранить). Отображается страница индекса курсов с обновленными данными о курсах.

Добавление страницы редактирования данных о преподавателях

При редактировании записи преподавателя может потребоваться обновить назначенный преподавателю кабинет. Сущность Instructor имеет связь "один к нулю или к одному" с сущностью OfficeAssignment, что означает, что код должен обрабатывать следующие ситуации.

  • Если пользователь сбрасывает назначение кабинета, которое изначально имело некоторое значение, удалите сущность OfficeAssignment.

  • Если пользователь вводит значение для назначения кабинета, которое изначально было пустым, создайте сущность OfficeAssignment.

  • Если пользователь изменяет значение для назначения кабинета, измените значение в существующей сущности OfficeAssignment.

Обновление контроллера преподавателей

В InstructorsController.cs измените код метода HttpGet Edit, чтобы он загружал свойство навигации OfficeAssignment сущности Instructor и вызывал 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 указывает, что URL-адрес /Edit/ по-прежнему используется).

  • Получает текущую сущность Instructor из базы данных, используя безотложную загрузку для свойства навигации OfficeAssignment. Это аналогично тому, что вы сделали в методе 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;
    }
    
  • Сохраняет изменения в базу данных.

Обновление представления редактирования преподавателя

Добавьте Views/Instructors/Edit.cshtmlновое поле для редактирования расположения office в конце перед кнопкой "Сохранить ":

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

Запустите приложение, выберите вкладку Instructors (Преподаватели), а затем щелкните Edit (Изменить) для преподавателя. Измените значение Office Location (Расположение кабинета) и нажмите кнопку Save (Сохранить).

Instructor Edit page

Добавление курсов на страницу редактирования

Преподаватели могут вести любое число курсов. Теперь вы усовершенствуете страницу редактирования преподавателя, добавив возможность изменять назначения курсов с помощью группы флажков, как показано на следующем снимке экрана.

Instructor Edit page with courses

Между сущностями Course и Instructor действует связь "многие ко многим". Для добавления и удаления связей можно добавлять сущности в список объединенного набора сущностей CourseAssignments и удалять их из него.

Элементы пользовательского интерфейса, позволяющие изменять назначенные преподавателю курсы, представляют собой группу флажков. Отображается флажок для каждого курса в базе данных, и флажки установлены для тех курсов, которые назначены текущему преподавателю. Пользователь может устанавливать и снимать флажки, изменяя назначения курсов. Если бы количество курсов было значительно больше, возможно, вам потребовалось бы использовать другой метод отображения данных в этом представлении, но вы бы использовали тот же самый способ управления сущностью объединения для создания и удаления связей.

Обновление контроллера преподавателей

Чтобы указать в представлении данные для списка флажков, используйте класс моделей представления.

Создайте AssignedCourseData.cs в папке SchoolViewModels и замените существующий код следующим кодом:

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 и вызывает новый метод PopulateAssignedCourseData, который предоставляет сведения для массива флажков, используя класс моделей представления AssignedCourseData.

Код в методе PopulateAssignedCourseData считывает все сущности Course, чтобы загрузить список курсов, используя класс модели представления. Для каждого курса код проверяет, существует ли этот курс в свойстве навигации Courses преподавателя. Чтобы создать эффективную подстановку при проверке того, назначен ли курс преподавателю, назначаемые курсы помещаются в коллекцию HashSet. У курсов, назначенных преподавателю, для свойства Assigned задается значение true. Представление будет использовать это свойство, чтобы определить, какие флажки нужно отображать как выбранные. Наконец, список передается в представление в ViewData.

Добавьте код, выполняемый, когда пользователь нажимает кнопку Save (Сохранить). Замените метод EditPost на следующий код и добавьте новый метод, который обновляет свойство навигации Courses для сущности Instructor.

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

Обновление представлений преподавателя

В файле Views/Instructors/Edit.cshtml добавьте поле Courses (Курсы) с массивом флажков, добавив приведенный ниже код сразу после элементов div для поля Office (Кабинет) и перед элементом div для кнопки Save (Сохранить).

Примечание.

При вставке кода в 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 только для выбранных флажков.

Флажкам, назначенным преподавателю, заданы атрибуты checked (установлены), поэтому при первичной отрисовке флажков курсов они отображаются установленными.

Запустите приложение, выберите вкладку Instructors (Преподаватели), а затем щелкните Edit (Изменить) для преподавателя, чтобы открыть страницу Edit (Редактирование).

Instructor Edit page with courses

Измените некоторые назначения курсов и нажмите кнопку "Save" (Сохранить). Вносимые вами изменения отражаются на странице индекса.

Примечание.

Описываемый здесь подход к редактированию данных курсов для преподавателя эффективен при ограниченном числе курсов. Для коллекций большего размера следовало бы применять другой пользовательский интерфейс и другой метод обновления.

Обновление страницы удаления

Удалите InstructorsController.csDeleteConfirmed метод и вставьте следующий код в его место.

[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 в представлении (в противном случае код представления выдаст исключение пустой ссылки).

Метод HttpPost Create добавляет каждый выбранный курс в свойство навигации CourseAssignments до того, как выполнить поиск ошибок проверки и добавить нового преподавателя в базу данных. Курсы добавляются даже при наличии ошибок модели, поэтому когда имеются такие ошибки (например, пользователь ввел недопустимую дату) и страница отображается повторно с сообщением об ошибке, все выбранные курсы восстанавливаются автоматически.

Обратите внимание, что для добавления курсов в свойство навигации CourseAssignments нужно инициализировать это свойство как пустую коллекцию:

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

Это можно сделать не только в коде контроллера, но и в модели Instructor, изменив метод получения свойств для автоматического создания коллекции, если она не существует, как показано в следующем примере:

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

При подобном изменении свойства CourseAssignments можно удалить код явной инициализации свойства в контроллере.

Добавьте Views/Instructor/Create.cshtmlтекстовое поле расположения office и проверка boxes для курсов перед кнопкой "Отправить". Как и в случае со страницей редактирования, исправьте форматирование, если 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, ознакомьтесь с разделом Транзакции.

Получение кода

Скачайте или ознакомьтесь с готовым приложением.

Следующие шаги

Изучив это руководство, вы:

  • Настройка страниц курсов
  • Добавление страницы редактирования преподавателей
  • Добавление курсов на страницу редактирования
  • Обновление страницы удаления
  • Добавление расположения кабинета и курсов на страницу создания

В следующем учебнике описано, как обрабатывать конфликты параллелизма.