Обновление связанных данных с помощью Entity Framework в приложении ASP.NET MVC
Это очередная статья из серии статей:
- Создание модели данных Entity Framework для приложения ASP.NET MVC
- Реализация базовой CRUD-функциональности с Entity Framework в приложении ASP.NET MVC
- Сортировка, фильтрация и разбиение по страницам с Entity Framework в приложении ASP.NET MVC
- Создание сложной модели данных для приложения ASP.NET MVC
- Создание сложной модели данных для приложения ASP.NET MVC, часть 2
- Загрузка данных с Entity Framework в приложении ASP.NET MVC
В предыдущих уроках мы отображали данные. Теперь вы будете их обновлять. Для большинства связей обновить связанные данные можно с помощью внешних ключей. Для связи многие-ко-многим EF не использует напрямую объединённую таблицу, поэтому вы должны вручную добавить и удалить сущности из соответствующих navigation properties.
Результаты представлены на иллюстрациях.
Редактирование страниц Create и Edit для Courses
При создании новой сущности курса, она должна быть связана с существующим факультетом. Чтобы обеспечить это, сгенерированный код включает в себя метода контроллера и представления Create и Edit с выпадающими списками для выделения факультета. Выпадающий список определяет свойство внешнего ключа Course.DepartmentID, всё, что нужно EF для загрузки Department navigation property соответствующими сущностями Department. Вы будете использовать сгенерированный код с небольшими изменениями для обработки ошибок и сортировки элементов выпадающего списка.
В CourseController.cs замените код методов Edit и Create:
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
public ActionResult Create(Course course)
{
try
{
if (ModelState.IsValid)
{
db.Courses.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
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]
public ActionResult Edit(Course course)
{
try
{
if (ModelState.IsValid)
{
db.Entry(course).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
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. Метод принимает параметр, позволяющий вызывающему опционально определить элемент, выбранный по умолчанию.
Метод HttpGet create вызывает метод PopulateDepartmentsDropDownList без указания выделенного элемента, так как еще не указан новый курс для факультета:
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
Метод HttpGet Edit определяет выбранный элемент, основываясь на ID факультета, связанный с редактируемым курсом:
public ActionResult Edit(int id)
{
Course course = db.Courses.Find(id);
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
Методы HttpPost для Create и Edit включают в себя подобный код:
catch (DataException)
{
//Log the error (add a variable name after DataException)
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 для ввода пользователем номера курса. Ранее объяснялось, что свойства первичных ключей не генерируются на представлении, но в данном случае первичный ключ несёт в себе смысл, поэтому необходимо дать пользователю возможность ввести его значение.
<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>
В 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>
Запустите проект и перейдите на страницу Create и введите данные для нового курса:
Нажмите Create. Отобразится страница Course Index со списком с добавленным курсом. Название факультета будет взято из navigation property, таким образом будет удостоверено, что связь между сущностями установлена корректно.
Откройте страницу Edit (откройте страницу Course Index и нажмите Edit на курсе).
Измените данные и нажмите Save. Отобразится страница Course Index с обновлёнными данными о курсе.
Добавление страницы Edit для Instructors
Когда вы редактируете запись о преподавателе, вы также можете обновить запись об его офисе. Сущность Instructor связана с OfficeAssignment как один-к-нулю-или-к-одному, что значит, что необходимо обрабатывать следующие ситуации:
- Если пользователь удалит привязку к офису, необходимо удалить сущность OfficeAssignment.
- Если пользователь введёт некорректное значение в свойство офиса, необходимо создать новую сущность OfficeAssignment.
- Если пользователь изменит значение записи об офисе, необходимо изменить значение соответствующей сущности OfficeAssignment.
В InstructorController . cs обратите внимание на метод HttpGet Edit:
public ActionResult Edit(int id)
{
Instructor instructor = db.Instructors.Find(id);
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)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
return View(instructor);
}
В этом коде не используется ViewBag и определяется eager loading для связанных сущностей OfficeAssignment и Course. (Courses понадобится позже.) Для метода Find нельзя определить eager loading, поэтому вместо него для выбора преподавателя используются методы Where и Single.
Замените код метода HttpPost Edit на следующий код, обрабатывающий редактирование записи об офисе:
[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection)
{
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
return View();
}
}
return View(instructorToUpdate);
}
Этот код выполняет следующие функции:
Возвращает сущность Instructor из базы данных используя eager loading для OfficeAssignment и Courses navigation properties. То же самое делали для метода HttpGet Edit.
Обновляет сущность Instructor исключая Courses navigation property:
If (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
Если проверка не прошла, TryUpdateModel возвращает false, и выполнение переходит на return View.
Если запись об офисе пуста, свойство Instructor.OfficeAssignment устанавливаается в null, таким образом таблица OfficeAssignment будет удалена.
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
- Сохраняет изменения в базу данных.
В Views \ Instructor \ Edit . cshtml после контейнеров div для поля HireDate добавьте новое поле для отображения адреса офиса:
<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>
Выберите вкладку Instructors и нажмите Edit на преподавателе.
Измените значение OfficeLocation и нажмите Save.
На странице Index появится новый адрес, и можно будет увидеть запись в таблице при открытии таблицы OfficeAssignment в ServerExplorer.
Вернитесь на страницу Edit, очистите OfficeLocation и нажмите Save. Страница Index отобразит пустой адрес и ServerExplorer отобразит, что запись была удалена.
На странице Edit введите новое значение в OfficeLocation и нажмите Save. На странице Index появится новое значение адреса, и ServerExplorer отобразит появление новой записи.
Добавление Course Assignments на страницу Instructor Edit
Преподаватели могут вести неограниченное количество курсов. Вы обновите страницу Instructor Edit путём добавления возможности назначения на курс:
Связь между Course и Instructor определена как многие-ко-многим, поэтому доступа к объединённой таблице или внешним ключам нет. Вместо этого небходимо оперировать Instructor.Courses navigation property.
Интерфейс, обеспечивающий возможность изменения привязки курсов к преподавателям, заключен в группе check boxes.
Для передачи данных в представление для генерации группы check boxes необходимо использовать класс модели представления. Создайте AssignedCourseData.cs в папке ViewModels со следующим содержимым:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.ViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
В InstructorController . cs, в методе HttpGet Edit вызовите новый метод, который будет обеспечивать представление информацией для генерации группы check boxes:
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;
}
Код нового метода загружает все сущности Course для создания списка курсов. Для каждого курса происходит проверка на существование в instructor's Courses navigation property. Чтобы эффективно узнать, связан ли курс с преподавателем, курсы, связанные с преподавателем, помещаются в коллекцию HashSet. Свойство курсов Assigned для назначенных курсов устанавливается в true. Представление использует это свойство для определения, отмечать ли check box или нет. После этого, список передаётся в свойстве ViewBag в представление.
Добавьте код обработчика кнопки Save: замените код метода HttpPost Edit кодом, вызывающим новый метод, обновляющий Courses navigation property для сущности Instructor.
[HttpPost]
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, "", null, new string[] { "Courses" }))
{
try
{
UpdateModel(instructorToUpdate, "", null, new string[] { "Courses" });
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)
{
//Log the error (add a variable name after DataException)
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);
}
}
}
}
Если нет выбранных check boxes, код в UpdateInstructorCourses инициализирует Courses navigation property пустой коллекцией:
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List();
return;
}
Код перебирает все записи курсов в базе данных и, если check box для курса отмечен, но курса нет в Instructor.Courses navigation property, курс добавляется в коллекцию в navigation property.
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
Если курс не выбран, но курс есть в Instructor.Courses navigation property, то запись о нём удаляется из navigation property.
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
В Views\Instructor\Edit.cshtml добавьте поле Courses с группой check boxes сразу после контейнеров div для OfficeAssignment:
<div class="editor-field">
<table style="width: 100%">
<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>
Код создаёт HTML-таблицу из трёх столбцов, в каждом из которых находится check box с заголовком из номера курса и названия. Все check boxes имеют одинаковое имя (“selectedCourses”), что символизирует их принадлежность к одному группе. Атрибут value каждого check box установлен в значение CourseID. Когда страница отправляет данные, в контроллер передаётся массив, состоящий из выделенных check boxes и значений CourseID.
Когда генерируются check boxes, те, что уже назначены преподавателю, имеют атрибут checked.
После изменения привязки курса к преподавателю нужно проверить изменения при возвращении к странице Index. Для этого надо добавить столбец в таблицу на этой странице. Для этого необязательно использовать объект ViewBag, потому что информация, которую необходимо отобразить, уже находится в Courses navigation property сущности Instructor, которая передаётся в представление в качестве модели.
В Views\Instructor\Index.cshtml добавьте заголовочную ячейку <th>Courses</th> сразу после <th>Office</th>:
<tr>
<th></th>
<th>Last Name</th>
<th>First Name</th>
<th>Hire Date</th>
<th>Office</th>
<th>Courses</th>
</tr>
После этого добавьте новую ячейку сразу после ячейки с адресом офиса:
<td>
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title <br />
}
}
</td>
Запустите проект и перейдите на страницу InstructorIndex:
Нажмите Edit на преподавателе.
Измените привязку курсов и нажмите Save. Изменения будут видны на странице Index.
Урок-введение в работу сданными закончен. Вы завершили работу над простыми операциями CRUD, но не имели дело с вопросами параллелизма. Следующий урок будет посвящен теме параллелизма, вопросам работы с ним.
--
Это перевод оригинальной статьи Updating Related Data with the Entity Framework in an ASP.NET MVC Application. Благодарим за помощь в переводе Александра Белоцерковского.