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

от Tom Dykstra)

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

Note

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

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

Note

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

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

Student_Details_page

Student_Edit_page

Student_Create_page

Student_delete_page

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

Шаблонный код для страницы учащихся Index оставил Enrollments свойство, так как оно содержит коллекцию. На Details странице отобразится содержимое коллекции в таблице HTML.

В контроллерс\студентконтроллер.КС метод действия для Details представления использует Find метод для получения одной Student сущности.

public ActionResult Details(int id = 0)
{
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

Значение ключа передается в метод в качестве id параметра и берется из данных маршрута в гиперссылке Details на странице индекса.

  1. Откройте виевс\студент\детаилс.кштмл. Каждое поле отображается с помощью 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. Запустите страницу, выбрав вкладку students (учащиеся ) и щелкнув ссылку Details (сведения ) для Александр Carson. Откроется список курсов и оценок для выбранного учащегося:

    Student_Details_page

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

  1. В контроллерс\студентконтроллер.КС замените 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 сущность, созданную связывателем модели ASP.NET MVC, в Students набор сущностей, а затем сохраняет изменения в базе данных. (Связыватель модели относится к функции MVC ASP.NET, которая упрощает работу с данными, передаваемыми с помощью формы. связыватель модели преобразует отправленные значения формы в типы 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 *whitelist* fields. It's also possible to use the `Exclude` parameter to *blacklist* 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. Запустите страницу, выбрав вкладку students (учащиеся ) и нажав кнопку создать.

    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

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

В контроллерс\студентконтроллер.КС HttpGet Edit метод (то есть без 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 измененный флаг заставляет Entity Framework создавать инструкции SQL для обновления строки базы данных. Будут обновлены все столбцы строки базы данных, включая те, которые не изменялись пользователем, а конфликты параллелизма игнорируются. (Вы узнаете, как работать с параллелизмом в последующем учебнике этой серии.)

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

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

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

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

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

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

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

Код в виевс\студент\едит.кштмл похож на тот, что вы видели в Create. cshtml, и никаких изменений не требуется.

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

Student_Edit_page

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

Students_Index_page_after_edit

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

В контроллерс\студентконтроллер.КС код шаблона для 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 метода 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 необходимо для удаления сущности.

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

  3. В виевс\студент\делете.кштмл добавьте сообщение об ошибке между h2 заголовком и h3 заголовком, как показано в следующем примере:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>
    

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

    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 Data Access.