Реализация базовых функций CRUD с помощью Entity Framework в приложении MVC ASP.NET (2 из 10)

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

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

Примечание

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

В предыдущем руководстве вы создали приложение MVC, которое хранит и отображает данные с помощью Entity Framework и SQL Server LocalDB. В этом руководстве вы изучите и настроите код CRUD (создание, чтение, обновление, удаление), который шаблон MVC автоматически создает для вас в контроллерах и представлениях.

Примечание

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

В этом руководстве вы создадите следующие веб-страницы:

Снимок экрана: страница сведений о студенте университета Contoso.

Снимок экрана: страница редактирования учащегося университета Contoso.

Снимок экрана: страница создания учащегося contoso university.

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

Создание страницы сведений

Шаблонный код для страницы Students 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 параметра и поступает из данных маршрута в гиперссылке Сведения на странице Индекс.

  1. Откройте 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>
    
  2. 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неотложную загрузку, поэтому при первой попытке получить доступ к свойству в базу данных отправляется запрос для получения данных. Дополнительные сведения о отложенной загрузке и нетеряющейся загрузке см. в руководстве по чтению связанных данных далее в этой серии.)

  3. Запустите страницу, выбрав вкладку Учащиеся и щелкнув ссылку Сведения для Александра Карсона. Откроется список курсов и оценок для выбранного учащегося:

    Student_Details_page

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

  1. В файле Controllers\StudentController.cs замените HttpPost``Create метод action следующим кодом, чтобы добавить 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 созданную связывателем модели ASP.NET MVC, в 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*.
  1. Запустите страницу, выбрав вкладку Учащиеся и нажав кнопку Создать.

    Student_Create_page

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

    Students_Create_page_error_message

    В следующем выделенном коде показана проверка проверки модели.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(student);
    }
    

    Измените дату на допустимое значение, например 01.09.2005, и нажмите кнопку Создать , чтобы увидеть, что новый учащийся появится на странице Индекс .

    Students_Index_page_with_new_student

Обновление страницы "Изменение POST"

В файле Controllers\StudentController.csHttpGetEdit метод (без атрибутаHttpPost) использует Find метод для получения выбранной Student сущности, как показано в методе Details . Изменять этот метод не нужно.

Однако замените HttpPostEdit метод действия следующим кодом, чтобы добавить 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);
}

Этот код аналогичен тому, который вы видели в методе HttpPostCreate . Однако вместо добавления сущности, созданной связывателем модели, в набор сущностей этот код устанавливает для сущности флаг, указывающий, что она была изменена. При вызове метода SaveChanges флаг Modified приводит к тому, что Entity Framework создает инструкции SQL для обновления строки базы данных. Будут обновлены все столбцы строки базы данных, включая столбцы, которые пользователь не изменял, а конфликты параллелизма игнорируются. (Вы узнаете, как обрабатывать параллелизм, в следующем руководстве этой серии.)

Состояния сущностей и методы Attach и SaveChanges

Контекст базы данных отслеживает состояние синхронизации сущностей в памяти с соответствующими им строками в базе данных. Данные отслеживания определяют, что происходит при вызове метода SaveChanges. Например, при передаче новой сущности в метод Add состояние этой сущности устанавливается в Added. Затем при вызове метода SaveChanges контекст базы данных выдает команду SQL INSERT .

Сущность может находиться в одном изследующих состояний:

  • Added. Сущность еще не существует в базе данных. Метод SaveChanges должен выдавать INSERT оператор .
  • Unchanged. С этой сущностью не нужно выполнять никакие действия с помощью метода SaveChanges. Это начальный статус сущности, который она имеет при чтении из базы данных.
  • Modified. Были изменены значения некоторых или всех свойств сущности. Метод SaveChanges должен выдавать UPDATE оператор .
  • Deleted. Сущность отмечена для удаления. Метод SaveChanges должен выдать DELETE оператор .
  • Detached. Сущность не отслеживается контекстом базы данных.

В классическом приложении изменения состояния обычно осуществляются автоматически. В классическом приложении вы считываете сущность и вносите изменения в некоторые значения ее свойств. В этом случае состояние сущности автоматически изменится на Modified. Затем при вызове SaveChangesEntity Framework создает инструкцию SQL UPDATE , которая обновляет только фактические свойства, которые вы изменили.

Отключенный характер веб-приложений не допускает эту непрерывную последовательность. DbContext, который считывает сущность, удаляется после отрисовки страницы. При вызове HttpPostEdit метода действия выполняется новый запрос, и у вас есть новый экземпляр DbContext, поэтому при Modified. вызове SaveChangesEntity Framework обновляет все столбцы строки базы данных, так как контекст не может узнать, какие свойства были изменены.

Если требуется, чтобы инструкция SQL Update обновляла только поля, которые пользователь фактически изменил, можно сохранить исходные значения каким-то образом (например, скрытые поля), чтобы они были доступны при вызове HttpPostEdit метода . Затем можно создать Student сущность с помощью исходных значений, вызвать Attach метод с этой исходной версией сущности, обновить значения сущности до новых значений, а затем вызвать для получения SaveChanges. дополнительных сведений см . статьи Состояния сущностей и СохранениеИзменения и локальные данные в Центре разработчика данных MSDN.

Код в Views\Student\Edit.cshtml аналогичен тому, что вы видели в Файле Create.cshtml, и никаких изменений не требуется.

Запустите страницу, выбрав вкладку Учащиеся и щелкнув гиперссылку Изменить .

Student_Edit_page

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

Students_Index_page_after_edit

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

В файле Controllers\StudentController.cs код шаблона для метода использует Find метод для HttpGetDelete извлечения выбранной Student сущности, как показано в методах Details и Edit . Тем не менее, чтобы реализовать настраиваемое сообщение об ошибке при сбое вызова метода SaveChanges, необходимо добавить некоторые функции в этот метод и соответствующее ему представление.

Как и в случае с операциями обновления и создания, операции удаления требуют двух методов действия. Метод, вызываемый в ответ на запрос GET, отображает представление, которое дает пользователю возможность утвердить или отменить операцию удаления. Если пользователь подтверждает ее, создается запрос POST. В этом случае вызывается метод , HttpPostDelete а затем этот метод фактически выполняет операцию удаления.

Вы добавите try-catch в метод блок HttpPostDelete для обработки ошибок, которые могут возникнуть при обновлении базы данных. При возникновении HttpPostDelete ошибки метод вызывает HttpGetDelete метод , передавая ему параметр, указывающий на то, что произошла ошибка. Затем HttpGet Delete метод повторно отображает страницу подтверждения вместе с сообщением об ошибке, предоставляя пользователю возможность отменить или повторить попытку.

  1. Замените HttpGetDelete метод действия следующим кодом, который управляет отчетами об ошибках:

    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 при вызове HttpGetDelete метода без предыдущего сбоя. При вызове методом HttpPostDelete в ответ на ошибку обновления базы данных параметр имеет значение true и в представление передается сообщение об ошибке.

  2. Замените HttpPostDelete метод действия (с именем 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 создается инструкция SQL DELETE. Вы также изменили имя метода действия с DeleteConfirmed на Delete. Шаблонный код с именем HttpPostDelete метода DeleteConfirmed , чтобы дать методу уникальную сигнатуру HttpPost . ( Среда CLR требует, чтобы перегруженные методы имели разные параметры метода.) Теперь, когда сигнатуры уникальны, можно придерживаться соглашения MVC и использовать то же имя для HttpPost методов и HttpGet delete.

    Если повышение производительности в приложении большого объема является приоритетом, можно избежать ненужных SQL-запросов для извлечения строки, заменив строки кода, вызывающие Find методы и Remove , следующим кодом, как показано в желтом выделении:

    Student studentToDelete = new Student() { StudentID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;
    

    Этот код создает экземпляр сущности, Student используя только значение первичного ключа, а затем задает для состояния сущности значение Deleted. Это все, что платформе Entity Framework необходимо для удаления сущности.

    Как уже отмечалось, HttpGetDelete метод не удаляет данные. Выполнение операции удаления в ответ на запрос GET (или, если на то пошло, выполнение любой операции редактирования, операции создания или любой другой операции, которая изменяет данные) создает угрозу безопасности. Дополнительные сведения см . в разделе ASP.NET совет MVC No 46— не используйте удаление ссылок, так как они создают дыры в системе безопасности в блоге Стивена Уолтера.

  3. В 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>
    

    Запустите страницу, выбрав вкладку Учащиеся и щелкнув гиперссылку Удалить :

    Student_Delete_page

  4. Щелкните Delete (Удалить). Отображается страница 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 карте содержимого доступа к данным.