Обновление связанных данных с помощью Entity Framework в приложении MVC ASP.NET (6 из 10)

Том Дайкстра (Tom Dykstra)

Пример веб-приложения Университета Contoso демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.

Примечание

Если у вас возникла проблема, которую не удается устранить, скачайте завершенную главу и попробуйте воспроизвести проблему. Как правило, решение проблемы можно найти, сравнив код с готовым кодом. Сведения о некоторых распространенных ошибках и способах их устранения см. в статье Ошибки и обходные пути.

В предыдущем руководстве вы отображали связанные данные; В этом руководстве вы обновите связанные данные. Для большинства связей это можно сделать, обновив соответствующие поля внешнего ключа. Для связей "многие ко многим" Entity Framework не предоставляет таблицу соединения напрямую, поэтому необходимо явным образом добавлять сущности в соответствующие свойства навигации и удалять их из нее.

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

Снимок экрана: страница

Снимок экрана: страница

Настройка страниц создания и редактирования для курсов

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

В файле CourseController.cs удалите четыре Edit метода и и Create замените их следующим кодом:

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(
   [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
   Course course)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Courses.Add(course);
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
      ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
   }
   PopulateDepartmentsDropDownList(course.DepartmentID);
   return View(course);
}

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
    [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
    Course course)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(course).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
      ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
   }
   PopulateDepartmentsDropDownList(course.DepartmentID);
   return View(course);
}

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

Метод PopulateDepartmentsDropDownList получает список всех отделов, отсортированных по имени, создает SelectList коллекцию для раскрывающегося списка и передает коллекцию в представление в свойстве ViewBag . Этот метод принимает необязательный параметр selectedDepartment, позволяющий вызывающему коду указать элемент, который будет выбран при отрисовке раскрывающегося списка. Представление передаст имя DepartmentIDвспомогательному DropDownListобъекту, а вспомогателям будет известно, что он будет искать в ViewBag объекте именованный SelectListDepartmentIDобъект .

Метод HttpGetCreate вызывает PopulateDepartmentsDropDownList метод без задания выбранного элемента, так как для нового курса кафедра еще не создана:

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

Метод HttpGetEdit задает выбранный элемент на основе идентификатора отдела, который уже назначен редактируемому курсу:

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

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

catch (DataException /* dex */)
{
    //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
    ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);

Этот код гарантирует, что при повторном воспроизведении страницы для отображения сообщения об ошибке выбранный отдел остается выбранным.

В views\Course\Create.cshtml добавьте выделенный код, чтобы создать новое поле номера курса перед полем Title . Как описано в предыдущем руководстве, поля первичного ключа по умолчанию не сформированы, но этот первичный ключ имеет смысл, поэтому пользователь должен иметь возможность ввести значение ключа.

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Course</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.CourseID)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.CourseID)
            @Html.ValidationMessageFor(model => model.CourseID)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Credits)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Credits)
            @Html.ValidationMessageFor(model => model.Credits)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.DepartmentID, "Department")
        </div>
        <div class="editor-field">
            @Html.DropDownList("DepartmentID", String.Empty)
            @Html.ValidationMessageFor(model => model.DepartmentID)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

В views\Course\Edit.cshtml, Views\Course\Delete.cshtml и Views\Course\Details.cshtml добавьте поле номера курса перед полем Title . Так как это первичный ключ, он отображается, но его нельзя изменить.

<div class="editor-label">
    @Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
    @Html.DisplayFor(model => model.CourseID)
</div>

Запустите страницу Создание (отобразите страницу Индекс курса и щелкните Создать) и введите данные для нового курса:

Course_create_page

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

Course_Index_page_showing_new_course

Запустите страницу Правка (отобразите страницу Индекс курса и щелкните Изменить для курса).

Course_edit_page

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

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

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

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

Откройте файл InstructorController.cs и просмотрите HttpGetEdit метод :

public ActionResult Edit(int id = 0)
{
    Instructor instructor = db.Instructors.Find(id);
    if (instructor == null)
    {
        return HttpNotFound();
    }
    ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.InstructorID);
    return View(instructor);
}

Шаблонный код здесь не является тем, что вы хотите. Он настраивает данные для раскрывающегося списка, но вам нужно текстовое поле. Замените этот метод следующим кодом:

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Where(i => i.InstructorID == id)
        .Single();
    return View(instructor);
}

Этот код удаляет инструкцию ViewBag и добавляет неотложную загрузку для связанной OfficeAssignment сущности. Вы не можете выполнить неотложную загрузку Find с помощью метода , поэтому Where вместо этого используются методы и Single для выбора инструктора.

Замените HttpPostEdit метод следующим кодом. который обрабатывает обновления назначений Office:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formCollection)
{
   var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Where(i => i.InstructorID == id)
       .Single();

   if (TryUpdateModel(instructorToUpdate, "",
      new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
   {
      try
      {
         if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
         {
            instructorToUpdate.OfficeAssignment = null;
         }

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

         return RedirectToAction("Index");
      }
      catch (DataException /* dex */)
      {
         //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
         ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
      }
   }
   ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", id);
   return View(instructorToUpdate);
}

Этот код выполняет следующее:

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

  • Обновляет извлеченную сущность Instructor, используя значения из связывателя модели. Используемая перегрузка TryUpdateModel позволяет добавить в список надежных свойств, которые вы хотите включить. Это предотвращает чрезмерную публикацию, как описано во втором руководстве.

    if (TryUpdateModel(instructorToUpdate, "",
          new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
    
  • Если расположение кабинета отсутствует, задает для свойства Instructor.OfficeAssignment значение NULL, что приводит к удалению связанной строки в таблице OfficeAssignment.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • Сохраняет изменения в базу данных.

В файле Views\Instructor\Edit.cshtml после div элементов поля Дата найма добавьте новое поле для изменения расположения офиса:

<div class="editor-label">
    @Html.LabelFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.OfficeAssignment.Location)
    @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>

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

Changing_the_office_location

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

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

Снимок экрана: страница

Связь между Course сущностями и Instructor является "многие ко многим", что означает, что у вас нет прямого доступа к таблице соединения. Вместо этого вы будете добавлять сущности в свойство навигации и удалять их из него Instructor.Courses .

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

Чтобы предоставить данные в представлении для списка флажков, нужно использовать класс модели представления. Создайте файл AssignedCourseData.cs в папке ViewModels и замените существующий код следующим кодом:

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

В Файле InstructorController.cs замените HttpGetEdit метод следующим кодом. Изменения выделены.

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = db.Courses;
    var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID));
    var viewModel = new List<AssignedCourseData>();
    foreach (var course in allCourses)
    {
        viewModel.Add(new AssignedCourseData
        {
            CourseID = course.CourseID,
            Title = course.Title,
            Assigned = instructorCourses.Contains(course.CourseID)
        });
    }
    ViewBag.Courses = viewModel;
}

Код добавляет безотложную загрузку для свойства навигации Courses и вызывает новый метод PopulateAssignedCourseData для предоставления сведений массиву флажков с помощью класса модели представления AssignedCourseData.

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

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

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

         UpdateInstructorCourses(selectedCourses, instructorToUpdate);

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

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

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

   var selectedCoursesHS = new HashSet<string>(selectedCourses);
   var instructorCourses = new HashSet<int>
       (instructorToUpdate.Courses.Select(c => c.CourseID));
   foreach (var course in db.Courses)
   {
      if (selectedCoursesHS.Contains(course.CourseID.ToString()))
      {
         if (!instructorCourses.Contains(course.CourseID))
         {
            instructorToUpdate.Courses.Add(course);
         }
      }
      else
      {
         if (instructorCourses.Contains(course.CourseID))
         {
            instructorToUpdate.Courses.Remove(course);
         }
      }
   }
}

Так как представление не содержит коллекцию сущностей Course , связыватель модели не может автоматически обновить свойство навигации Courses . Вместо использования связывателя модели для обновления свойства навигации Courses это можно сделать в новом UpdateInstructorCourses методе. Поэтому нужно исключить свойство Courses из привязки модели. Для этого не требуется вносить изменения в код, вызывающий TryUpdateModel , так как вы используете перегрузку списка безопасных файлов и Courses не входите в список включения.

Если проверка поля не были выбраны, код в инициализирует UpdateInstructorCourses свойство навигации Courses с пустой коллекцией:

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

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

Если флажок для курса был установлен, но курс отсутствует в свойстве навигации Instructor.Courses, этот курс добавляется в коллекцию в свойстве навигации.

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

Если флажок для курса не был установлен, но курс присутствует в свойстве навигации Instructor.Courses, этот курс удаляется из свойства навигации.

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

В Views\Instructor\Edit.cshtml добавьте поле Courses с массивом полей проверка, добавив следующий выделенный код сразу после div элементов OfficeAssignment поля:

@model ContosoUniversity.Models.Instructor

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Instructor</legend>

        @Html.HiddenFor(model => model.InstructorID)

        <div class="editor-label">
            @Html.LabelFor(model => model.LastName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.FirstMidName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.FirstMidName)
            @Html.ValidationMessageFor(model => model.FirstMidName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.HireDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.HireDate)
            @Html.ValidationMessageFor(model => model.HireDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.OfficeAssignment.Location)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.OfficeAssignment.Location)
            @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
        </div>

        <div class="editor-field">
    <table>
        <tr>
            @{
                int cnt = 0;
                List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;

                foreach (var course in courses) {
                    if (cnt++ % 3 == 0) {
                        @:  </tr> <tr> 
                    }
                    @: <td> 
                        <input type="checkbox" 
                               name="selectedCourses" 
                               value="@course.CourseID" 
                               @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) /> 
                        @course.CourseID @:  @course.Title
                    @:</td>
                }
                @: </tr>
            }
    </table>
</div>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Этот код создает таблицу HTML с тремя столбцами. Каждый столбец содержит флажок, за которым идет заголовок с номером и названием курса. Все поля проверка имеют одинаковое имя ("selectedCourses"), которое сообщает связыватель модели о том, что они должны рассматриваться как группа. Атрибуту value каждого поля проверка задано значение CourseID. При публикации страницы связыватель модели передает массив контроллеру, который состоит из значений CourseID только для выбранных проверка полей.

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

После изменения заданий курса вы хотите иметь возможность проверить изменения при возвращении сайта на страницу Index . Поэтому необходимо добавить столбец в таблицу на этой странице. В этом случае вам не нужно использовать ViewBag объект , так как информация, которую вы хотите отобразить, уже находится в свойстве Courses навигации сущности Instructor , передаваемой на страницу в качестве модели.

В views\Instructor\Index.cshtml добавьте заголовок Courses сразу после заголовка Office , как показано в следующем примере:

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

Затем добавьте новую ячейку сведений сразу после ячейки сведений о расположении офиса:

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th>Courses</th>
    </tr>
    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.InstructorID == ViewBag.InstructorID)
        {
            selectedRow = "selectedrow";
        } 
        <tr class="@selectedRow" valign="top">
            <td>
                @Html.ActionLink("Select", "Index", new { id = item.InstructorID }) | 
                @Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) | 
                @Html.ActionLink("Details", "Details", new { id = item.InstructorID }) | 
                @Html.ActionLink("Delete", "Delete", new { id = item.InstructorID })
            </td>
            <td>
                @item.LastName
            </td>
            <td>
                @item.FirstMidName
            </td>
            <td>
                @String.Format("{0:d}", item.HireDate)
            </td>
            <td>
                @if (item.OfficeAssignment != null)
                { 
                    @item.OfficeAssignment.Location  
                }
            </td>
            <td>
                @{
                foreach (var course in item.Courses)
                {
                    @course.CourseID @:  @course.Title <br />
                }
                }
            </td>
        </tr> 
    }
</table>

@if (Model.Courses != null)
{ 
    <h3>Courses Taught by Selected Instructor</h3> 
    <table>
        <tr>
            <th></th>
            <th>ID</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == ViewBag.CourseID)
            {
                selectedRow = "selectedrow";
            } 
        
            <tr class="@selectedRow">

                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr> 
        }

    </table> 
}
@if (Model.Enrollments != null)
{ 
    <h3>Students Enrolled in Selected Course</h3> 
    <table>
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        { 
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr> 
        }
    </table> 
}

Запустите страницу Индекс преподавателя , чтобы просмотреть курсы, назначенные каждому преподавателю:

Instructor_index_page

Нажмите кнопку Изменить для преподавателя, чтобы открыть страницу Правка.

Instructor_edit_page_with_courses

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

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

Обновление метода Delete

Измените код в методе HttpPost Delete, чтобы при удалении преподавателя была удалена запись о назначении офиса (если она есть):

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
   Instructor instructor = db.Instructors
     .Include(i => i.OfficeAssignment)
     .Where(i => i.InstructorID == id)
     .Single();

   instructor.OfficeAssignment = null;
   db.Instructors.Remove(instructor);
   db.SaveChanges();
   return RedirectToAction("Index");
}

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

Итоги

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

Ссылки на другие ресурсы Entity Framework можно найти в конце последнего руководства в этой серии.