Реализация основных функций CRUD с помощью Entity Framework в ASP.NET приложении MVC (2 из 10)
Пример веб-приложения Contoso University демонстрирует создание ASP.NET приложений MVC 4 с помощью Entity Framework 5 Code First и Visual Studio 2012. Сведения о серии руководств см. в первом руководстве серии.
Примечание.
Если вы не сможете устранить проблему, скачайте завершенную главу и попытайтесь воспроизвести проблему. Как правило, решение проблемы можно найти, сравнивая код с завершенным кодом. Некоторые распространенные ошибки и способы их устранения см. в статье об ошибках и обходных решениях.
В предыдущем руководстве вы создали приложение MVC, которое хранит и отображает данные с помощью Entity Framework и SQL Server LocalDB. В этом руководстве вы просмотрите и настройте код CRUD (создание, чтение, обновление, удаление), который шаблон MVC автоматически создает для вас в контроллерах и представлениях.
Примечание.
Чтобы создать уровень абстракции между контроллером и уровнем доступа к данным, часто реализуют шаблон репозитория. Чтобы сделать эти учебники простыми, вы не будете реализовывать репозиторий до тех пор, пока не будет описано в этом руководстве.
В этом руководстве вы создадите следующие веб-страницы:
Создание страницы сведений
Шаблонный код страницы "Учащиеся" Index
оставил свойство Enrollments
, так как это свойство содержит коллекцию. Details
На странице вы увидите содержимое коллекции в HTML-таблице.
В Controllers\StudentController.cs метод действия для Details
представления использует Find
метод для извлечения одной Student
сущности.
public ActionResult Details(int id = 0)
{
Student student = db.Students.Find(id);
if (student == null)
{
return HttpNotFound();
}
return View(student);
}
Значение ключа передается методу в качестве id
параметра и поступает из данных маршрута в гиперссылке "Сведения " на странице индекса.
Откройте Views\Student\Details.cshtml. Каждое поле отображается с помощью вспомогательного
DisplayFor
средства, как показано в следующем примере:<div class="display-label"> @Html.DisplayNameFor(model => model.LastName) </div> <div class="display-field"> @Html.DisplayFor(model => model.LastName) </div>
EnrollmentDate
После поля и непосредственно перед закрывающимfieldset
тегом добавьте код для отображения списка регистраций, как показано в следующем примере:<div class="display-label"> @Html.LabelFor(model => model.Enrollments) </div> <div class="display-field"> <table> <tr> <th>Course Title</th> <th>Grade</th> </tr> @foreach (var item in Model.Enrollments) { <tr> <td> @Html.DisplayFor(modelItem => item.Course.Title) </td> <td> @Html.DisplayFor(modelItem => item.Grade) </td> </tr> } </table> </div> </fieldset> <p> @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) | @Html.ActionLink("Back to List", "Index") </p>
Этот код циклически обрабатывает сущности в свойстве навигации
Enrollments
. Для каждойEnrollment
сущности в свойстве отображается название курса и оценка. Название курса извлекается изCourse
сущности, которая хранится вCourse
свойстве навигации сущностиEnrollments
. Все эти данные извлекаются из базы данных автоматически при необходимости. (Иными словами, вы используете отложенную загрузку здесь. Вы не указали требуемую загрузку дляCourses
свойства навигации, поэтому при первом попытке получить эти свойства запрос отправляется в базу данных для получения данных. Дополнительные сведения о отложенной загрузке и активной загрузке см. в руководстве по чтению связанных данных далее в этой серии.)Запустите страницу, выбрав вкладку "Учащиеся" и щелкнув ссылку "Сведения " для Александра Карсона. Откроется список курсов и оценок для выбранного учащегося:
Обновление страницы создания
В Controllers\StudentController.cs замените
HttpPost``Create
метод действия следующим кодом, чтобы добавитьtry-catch
блок и атрибут Bind в шаблонный метод:[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create( [Bind(Include = "LastName, FirstMidName, EnrollmentDate")] Student student) { try { if (ModelState.IsValid) { db.Students.Add(student); 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."); } return View(student); }
Этот код добавляет
Student
сущность, созданную привязкой модели MVC ASP.NET кStudents
набору сущностей, а затем сохраняет изменения в базе данных. (Привязыватель модели ссылается на функциональность ASP.NET MVC, которая упрощает работу с данными, отправленными формой; привязка модели преобразует опубликованные значения форм в типы CLR и передает их методу действия в параметрах. В этом случае привязка модели создает экземплярStudent
сущности для использования значений свойств изForm
коллекции.)Этот
ValidateAntiForgeryToken
атрибут помогает предотвратить атаки на подделку запросов между сайтами.
> [!WARNING]
> Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
>
> [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
>
> Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
>
> ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)
>
> The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
>
> It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
>
> Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.
Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).
The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:
[!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]
*Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.
No changes are required in *Create.cshtml*.
Запустите страницу, выбрав вкладку "Учащиеся" и нажав кнопку "Создать".
Некоторые проверки данных работают по умолчанию. Введите имена и недопустимую дату и нажмите кнопку "Создать ", чтобы увидеть сообщение об ошибке.
В следующем выделенном коде показана проверка модели.
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Student student) { if (ModelState.IsValid) { db.Students.Add(student); db.SaveChanges(); return RedirectToAction("Index"); } return View(student); }
Измените дату на допустимое значение, например 9.1.2005, и нажмите кнопку "Создать", чтобы увидеть, что новый учащийся появится на странице "Индекс".
Обновление страницы Edit POST
В Controllers\StudentController.cs Edit
HttpGet
метод (без HttpPost
атрибута) использует Find
метод для получения выбранной Student
сущности, как показано в методе.Details
Изменять этот метод не нужно.
Однако замените HttpPost
Edit
метод действия следующим кодом, чтобы добавить try-catch
блок и атрибут Bind:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
Student student)
{
try
{
if (ModelState.IsValid)
{
db.Entry(student).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.");
}
return View(student);
}
Этот код похож на то, что вы видели в методе HttpPost
Create
. Однако вместо добавления сущности, созданной привязкой модели к набору сущностей, этот код задает флаг сущности, указывающий, что он был изменен. При вызове метода SaveChanges флаг "Изменено" приводит к созданию инструкций SQL Для создания инструкций SQL для обновления строки базы данных. Все столбцы строки базы данных будут обновлены, включая те, которые пользователь не изменил, и конфликты параллелизма игнорируются. (Вы узнаете, как обрабатывать параллелизм в следующем руководстве в этом серии.)
Состояния сущностей и методы Attach и SaveChanges
Контекст базы данных отслеживает состояние синхронизации сущностей в памяти с соответствующими им строками в базе данных. Данные отслеживания определяют, что происходит при вызове метода SaveChanges
. Например, при передаче новой сущности методу Add для этой сущности задано Added
состояние этой сущности. Затем при вызове метода SaveChanges контекст базы данных выдает команду SQL INSERT
.
Сущность может находиться в одном из следующих состояний:
Added
. Сущность еще не существует в базе данных. МетодSaveChanges
должен выдавать инструкциюINSERT
.Unchanged
. С этой сущностью не нужно выполнять никакие действия с помощью методаSaveChanges
. Это начальный статус сущности, который она имеет при чтении из базы данных.Modified
. Были изменены значения некоторых или всех свойств сущности. МетодSaveChanges
должен выдавать инструкциюUPDATE
.Deleted
. Сущность отмечена для удаления. МетодSaveChanges
должен выдавать инструкциюDELETE
.Detached
. Сущность не отслеживается контекстом базы данных.
В классическом приложении изменения состояния обычно осуществляются автоматически. В классическом типе приложения вы считываете сущность и вносите изменения в некоторые из его значений свойств. В этом случае состояние сущности автоматически изменится на Modified
. Затем при вызове SaveChanges
Entity Framework создает инструкцию SQL UPDATE
, которая обновляет только фактические свойства, которые вы изменили.
Отключенный характер веб-приложений не разрешает эту непрерывную последовательность. DbContext, который считывает сущность, удаляется после отрисовки страницы. HttpPost
Edit
При вызове метода действия создается новый запрос, и у вас есть новый экземпляр DbContext, поэтому при вызове SaveChanges
необходимо вручную задать состояние Modified.
сущности, entity Framework обновляет все столбцы строки базы данных, так как контекст не имеет способа знать, какие свойства вы изменили.
Если вы хотите, чтобы инструкция SQL Update
обновляла только измененные пользователем поля, можно сохранить исходные значения каким-то образом (например, скрытые поля), чтобы они были доступны при HttpPost
Edit
вызове метода. Затем можно создать Student
сущность с помощью исходных значений, вызвать Attach
метод с этой исходной версией сущности, обновить значения сущности до новых значений, а затем вызвать SaveChanges.
дополнительные сведения: состояния сущностей и SaveChanges и локальные данные в Центре разработчиков данных MSDN.
Код в Views\Student\Edit.cshtml похож на то, что вы видели в Create.cshtml, и никаких изменений не требуется.
Запустите страницу, выбрав вкладку "Учащиеся" , а затем щелкните гиперссылку "Изменить ".
Измените определенные данные и нажмите кнопку Save (Сохранить). На странице индекса отображаются измененные данные.
Обновление страницы удаления
В Controllers\StudentController.cs код шаблона для HttpGet
Delete
метода использует Find
метод для получения выбранной Student
сущности, как показано в Details
и Edit
методах. Тем не менее, чтобы реализовать настраиваемое сообщение об ошибке при сбое вызова метода SaveChanges
, необходимо добавить некоторые функции в этот метод и соответствующее ему представление.
Как и в случае с операциями обновления и создания, операции удаления требуют двух методов действия. Метод, который вызывается в ответ на запрос GET, отображает представление, которое дает пользователю возможность утвердить или отменить операцию удаления. Если пользователь подтверждает ее, создается запрос POST. Когда это произойдет, метод вызывается, HttpPost
Delete
а затем этот метод фактически выполняет операцию удаления.
Вы добавите try-catch
блок в HttpPost
Delete
метод для обработки любых ошибок, которые могут возникнуть при обновлении базы данных. Если возникает ошибка, HttpPost
Delete
метод вызывает HttpGet
Delete
метод, передавая его параметр, указывающий на то, что произошла ошибка. Затем HttpGet Delete
метод переиграет страницу подтверждения вместе с сообщением об ошибке, что дает пользователю возможность отменить или повторить попытку.
Замените
HttpGet
Delete
метод действия следующим кодом, который управляет отчетами об ошибках:public ActionResult Delete(bool? saveChangesError=false, int id = 0) { if (saveChangesError.GetValueOrDefault()) { ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator."; } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }
Этот код принимает необязательный логический параметр, указывающий, был ли он вызван после сбоя сохранения изменений. Этот параметр возникает
false
приHttpGet
Delete
вызове метода без предыдущего сбоя. При вызовеHttpPost
Delete
метода в ответ на ошибку обновления базы данных параметр передаетсяtrue
в представление.Замените
HttpPost
Delete
метод действия (именованныйDeleteConfirmed
) приведенным ниже кодом, который выполняет фактическую операцию удаления и перехватывает ошибки обновления базы данных.[HttpPost] [ValidateAntiForgeryToken] public ActionResult Delete(int id) { try { Student student = db.Students.Find(id); db.Students.Remove(student); db.SaveChanges(); } catch (DataException/* dex */) { // uncomment dex and log error. return RedirectToAction("Delete", new { id = id, saveChangesError = true }); } return RedirectToAction("Index"); }
Этот код извлекает выбранную сущность, а затем вызывает метод Remove , чтобы задать состояние
Deleted
сущности. При вызове методаSaveChanges
создается инструкция SQLDELETE
. Вы также изменили имя метода действия сDeleteConfirmed
наDelete
. Шаблонный код с именемHttpPost
Delete
метода, который даетHttpPost
методуDeleteConfirmed
уникальную подпись. ( Среда CLR требует перегруженных методов иметь разные параметры метода.) Теперь, когда подписи уникальны, вы можете придерживаться соглашения MVC и использовать то же имя дляHttpPost
методов иHttpGet
удаления.Если повышение производительности в приложении с большим объемом является приоритетом, можно избежать ненужного SQL-запроса, чтобы получить строку, заменив строки кода, вызывающие
Find
иRemove
методы следующим кодом, как показано в желтом выделении:Student studentToDelete = new Student() { StudentID = id }; db.Entry(studentToDelete).State = EntityState.Deleted;
Этот код создает
Student
экземпляр сущности, используя только значение первичного ключа, а затем задает состояниеDeleted
сущности. Это все, что платформе Entity Framework необходимо для удаления сущности.Как отмечалось,
HttpGet
Delete
метод не удаляет данные. Выполнение операции удаления в ответ на запрос GET (или для этого, выполнение любой операции редактирования, операции создания или любой другой операции, которая изменяет данные) создает риск безопасности. Дополнительные сведения см. в разделе ASP.NET Совет MVC #46. Не используйте ссылки на удаление, так как они создают дыры безопасности в блоге Стивена Уолтера.В Views\Student\Delete.cshtml добавьте сообщение об ошибке между заголовком
h2
иh3
заголовком, как показано в следующем примере:<h2>Delete</h2> <p class="error">@ViewBag.ErrorMessage</p> <h3>Are you sure you want to delete this?</h3>
Запустите страницу, выбрав вкладку "Учащиеся" и щелкнув гиперссылку "Удалить ":
Нажмите Удалить. Отображается страница Index (Указатель), на которой удаленный учащийся будет отсутствовать. (Вы увидите пример кода обработки ошибок в действии Руководство по обработке параллелизма далее в этой серии.)
Обеспечение того, чтобы подключения к базе данных не оставались открытыми
Чтобы убедиться, что подключения к базе данных правильно закрыты и ресурсы, которые они удерживают, должны убедиться, что экземпляр контекста удален. Именно поэтому шаблонный код предоставляет метод Dispose в конце StudentController
класса в StudentController.cs, как показано в следующем примере:
protected override void Dispose(bool disposing)
{
db.Dispose();
base.Dispose(disposing);
}
Базовый Controller
класс уже реализует IDisposable
интерфейс, поэтому этот код просто добавляет переопределение в Dispose(bool)
метод для явного удаления экземпляра контекста.
Итоги
Теперь у вас есть полный набор страниц, выполняющих простые операции CRUD для Student
сущностей. Вы использовали вспомогательные средства MVC для создания элементов пользовательского интерфейса для полей данных. Дополнительные сведения о вспомогательных элементах MVC см. в разделе "Отрисовка формы с помощью вспомогательных элементов HTML" (страница предназначена для MVC 3, но по-прежнему актуальна для MVC 4).
В следующем руководстве вы развернете функциональные возможности страницы индекса, добавив сортировку и разбиение по страницам.
Ссылки на другие ресурсы Entity Framework можно найти на карте содержимого доступа к данным ASP.NET.