Поделиться через


Реализация основных функций 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 автоматически создает для вас в контроллерах и представлениях.

Примечание.

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

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

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

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

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

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

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

Шаблонный код страницы "Учащиеся" 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 метод действия следующим кодом, чтобы добавить 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*.
  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);
    }
    

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

    Students_Index_page_with_new_student

Обновление страницы 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. Затем при вызове SaveChangesEntity 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, и никаких изменений не требуется.

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

Student_Edit_page

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

Students_Index_page_after_edit

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

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

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

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

  1. Замените 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 в представление.

  2. Замените 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 создается инструкция SQL DELETE. Вы также изменили имя метода действия с 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. Не используйте ссылки на удаление, так как они создают дыры безопасности в блоге Стивена Уолтера.

  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. Нажмите Удалить. Отображается страница 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.