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


Обеспечение поддержки операций CRUD (создание, чтение, обновление и удаление) для записей форм данных

от Корпорации Майкрософт

Загрузить PDF-файл

Это шаг 5 бесплатного руководства по приложению NerdDinner , в которых показано, как создать небольшое, но полное веб-приложение с помощью ASP.NET MVC 1.

Шаг 5 показывает, как продолжить наш класс DinnersController, включив поддержку редактирования, создания и удаления Dinners с его помощью.

Если вы используете ASP.NET MVC 3, рекомендуем следовать руководствам по начало работы С MVC 3 или MVC Music Store.

NerdDinner, шаг 5. Создание, обновление, удаление форм

Мы представили контроллеры и представления, а также рассмотрели, как использовать их для реализации описания и сведений для Dinners на месте. Следующим шагом будет дальнейшее выполнение класса DinnersController и поддержка редактирования, создания и удаления Dinners с его помощью.

URL-адреса, обрабатываемые DinnersController

Ранее мы добавили методы действий в DinnersController, которые реализовали поддержку двух URL-адресов: /Dinners и /Dinners/Details/[id].

URL-адрес ГЛАГОЛ Назначение
/Ужины/ GET Отображение HTML-списка предстоящих ужинов.
/Dinners/Details/[id] GET Отображение сведений о конкретном ужине.

Теперь мы добавим методы действий для реализации трех дополнительных URL-адресов: /Dinners/Edit/[id], /Dinners/Create и /Dinners/Delete/[id]. Эти URL-адреса обеспечивают поддержку редактирования существующих ужинов, создания новых ужинов и удаления ужинов.

Мы будем поддерживать взаимодействие команд HTTP GET и HTTP POST с этими новыми URL-адресами. HTTP-запросы GET к этим URL-адресам будут отображать исходное HTML-представление данных (форма, заполненная данными Dinner в случае "изменить", пустая форма в случае "create" и экран подтверждения удаления в случае "удалить"). HTTP-запросы POST на эти URL-адреса будут сохранять, обновлять или удалять данные Dinner в нашем DinnerRepository (а затем в базу данных).

URL-адрес ГЛАГОЛ Назначение
/Dinners/Edit/[id] GET Отображение редактируемой HTML-формы, заполненной данными Dinner.
POST Сохраните изменения формы для определенного ужина в базе данных.
/Dinners/Create GET Отображение пустой HTML-формы, позволяющей пользователям определять новые ужины.
POST Создайте новый ужин и сохраните его в базе данных.
/Dinners/Delete/[id] GET Отображение экрана подтверждения удаления.
POST Удаляет указанный ужин из базы данных.

Изменение поддержки

Начнем с реализации сценария редактирования.

Метод действия "Изменение HTTP-GET"

Для начала мы реализуем поведение HTTP GET нашего метода действия редактирования. Этот метод вызывается при запросе URL-адреса /Dinners/Edit/[id] . Наша реализация будет выглядеть следующим образом:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

Приведенный выше код использует DinnerRepository для получения объекта Dinner. Затем он отрисовывает шаблон View с помощью объекта Dinner. Так как мы не передали имя шаблона во вспомогательный метод View(), он будет использовать путь по умолчанию на основе соглашения для разрешения шаблона представления: /Views/Dinners/Edit.aspx.

Теперь создадим этот шаблон представления. Для этого щелкните правой кнопкой мыши метод Edit и выберите команду контекстного меню "Добавить представление":

Снимок экрана: создание шаблона представления для добавления представления в Visual Studio.

В диалоговом окне "Добавление представления" мы укажем, что мы передаём объект Dinner в наш шаблон представления в качестве модели, и выберите автоматический шаблон "Изменить":

Снимок экрана: добавление представления для автоматического формирования шаблонов для редактирования шаблона.

При нажатии кнопки "Добавить" Visual Studio добавит новый файл шаблона представления "Edit.aspx" в каталог \Views\Dinners. Кроме того, откроется новый шаблон представления Edit.aspx в редакторе кода, заполненный начальной реализацией шаблона "Изменить", как показано ниже:

Снимок экрана: новый шаблон представления

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

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Edit: <%=Html.Encode(Model.Title)%>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Edit Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>  
    
    <% using (Html.BeginForm()) { %>

        <fieldset>
            <p>
                <label for="Title">Dinner Title:</label>
                <%=Html.TextBox("Title") %>
                <%=Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate))%>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*")%>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>               
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone #:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
        
    <% } %>
    
</asp:Content>

При запуске приложения и запросе URL-адреса "/Dinners/Edit/1" отображается следующая страница:

Снимок экрана: страница приложения My M V C.

Html-разметка, созданная нашим представлением, выглядит следующим образом. Это стандартный HTML с элементом <формы> , который выполняет HTTP POST на URL-адрес /Dinners/Edit/1 при нажатии кнопки "Сохранить" <input type="submit"/> . Для каждого редактируемого свойства выводится элемент html <input type="text"/> :

Снимок экрана: созданная разметка H T M L.

Вспомогательные методы Html.BeginForm() и Html.TextBox()

В нашем шаблоне представления Edit.aspx используется несколько методов "Вспомогателя HTML": Html.ValidationSummary(), Html.BeginForm(), Html.TextBox() и Html.ValidationMessage(). Помимо создания разметки HTML для нас, эти вспомогательные методы обеспечивают встроенную поддержку обработки ошибок и проверки.

Вспомогательный метод Html.BeginForm()

Вспомогательный метод Html.BeginForm() — это то, что выводит элемент ФОРМЫ> HTML <в нашей разметке. В нашем шаблоне представления Edit.aspx вы заметите, что при использовании этого метода применяется оператор C# using. Открытая фигурная скобка указывает начало содержимого <формы> , а закрывающая фигурная скобка обозначает конец <элемента /form> :

<% using (Html.BeginForm()) { %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
         <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% } %>

Кроме того, если вы обнаружите, что подход оператора using неестественный для такого сценария, можно использовать сочетание Html.BeginForm() и Html.EndForm(), которое делает то же самое:

<% Html.BeginForm();  %>

   <fieldset>
   
      <!-- Fields Omitted for Brevity -->
   
      <p>
          <input type="submit" value="Save"/>
      </p>
   </fieldset>
   
<% Html.EndForm(); %>

Вызов Html.BeginForm() без параметров приведет к выводу элемента формы, который выполняет HTTP-POST, на URL-адрес текущего запроса. Поэтому в нашем представлении <"Изменить" создается элемент form action="/Dinners/Edit/1" method="post".> Кроме того, мы могли бы передать явные параметры в Html.BeginForm(), если бы мы хотели опубликовать публикацию на другой URL-адрес.

Вспомогательный метод Html.TextBox()

В нашем представлении Edit.aspx используется вспомогательный метод Html.TextBox() для вывода <элементов input type="text"/> :

<%= Html.TextBox("Title") %>

Приведенный выше метод Html.TextBox() принимает один параметр, который используется для указания атрибутов <id/name элемента input type="text"/> для вывода, а также свойства модели для заполнения значения текстового поля. Например, объект Dinner, переданный в представление Правка, имеет значение свойства "Title" как ".NET Futures", поэтому наш метод Html.TextBox("Title") вызывает выходные данные: <input id="Title" name="Title" type="text" value=".NET Futures" />.

Кроме того, можно использовать первый параметр Html.TextBox(), чтобы указать идентификатор или имя элемента, а затем явно передать значение для использования в качестве второго параметра:

<%= Html.TextBox("Title", Model.Title)%>

Часто требуется выполнить настраиваемое форматирование для выходного значения. Статический метод String.Format(), встроенный в .NET, полезен для этих сценариев. В нашем шаблоне представления Edit.aspx используется этот параметр для форматирования значения EventDate (типа DateTime), чтобы оно не отображало секунды для времени:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

Третий параметр Html.TextBox() можно использовать для вывода дополнительных атрибутов HTML. В приведенном ниже фрагменте кода показано, как отобразить дополнительный атрибут size="30" и атрибут class="mycssclass" в элементе <input type="text"/> . Обратите внимание, что мы экранируем имя атрибута класса с помощью символа "@", так как "class" является зарезервированным ключевое слово в C#:

<%= Html.TextBox("Title", Model.Title, new { size=30, @class="myclass" } )%>

Реализация метода действия изменения HTTP-POST

Теперь у нас реализована версия HTTP-GET нашего метода действия Изменить. Когда пользователь запрашивает URL-адрес /Dinners/Edit/1 , он получает HTML-страницу, как показано ниже:

Снимок экрана: выходные данные H TM L, когда пользователь запрашивает изменение ужина.

Нажатие кнопки "Сохранить" приводит к отправке формы по URL-адресу /Dinners/Edit/1 и отправляет <значения входных html-форм> с помощью команды HTTP POST. Теперь давайте реализуем поведение HTTP POST нашего метода действия редактирования, который будет обрабатывать сохранение ужина.

Начнем с добавления перегруженного метода действия "Изменить" в наш DinnersController с атрибутом AcceptVerbs, указывающим, что он обрабатывает сценарии HTTP POST:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {
   ...
}

Когда атрибут [AcceptVerbs] применяется к перегруженным методам действий, ASP.NET MVC автоматически обрабатывает отправку запросов к соответствующему методу действия в зависимости от входящей HTTP-команды. HTTP-запросы POST на URL-адреса /Dinners/Edit/[id] будут переходить к указанному выше методу Edit, а все остальные ЗАПРОСы HTTP-глаголов к URL-адресам /Dinners/Edit/[id] будут направляться в первый реализованный метод Edit (у которого не было атрибута [AcceptVerbs] ).

Побочный раздел. Зачем различать команды HTTP?
Вы можете спросить: почему мы используем один URL-адрес и отличаем его поведение с помощью HTTP-команды? Почему бы просто не иметь два отдельных URL-адреса для обработки загрузки и сохранения изменений редактирования? Например: /Dinners/Edit/[id] для отображения начальной формы и /Dinners/Save/[id] для обработки записи формы, чтобы сохранить ее? Недостаток публикации двух отдельных URL-адресов заключается в том, что в случаях, когда мы публикуем /Dinners/Save/2, а затем необходимо повторно воспроизвести HTML-форму из-за ошибки ввода, конечный пользователь в конечном итоге получит URL-адрес /Dinners/Save/2 в адресной строке своего браузера (так как это БЫЛ URL-адрес формы, на который размещена форма). Если пользователь закладывает эту переигрованную страницу в свой список избранного браузера или копирует или вставит URL-адрес и отправляет его другу по электронной почте, он в конечном итоге сохранит URL-адрес, который не будет работать в будущем (так как этот URL-адрес зависит от значений записи). Предоставляя один URL-адрес (например, /Dinners/Edit/[id]) и различая его обработку HTTP-командой, конечные пользователи могут добавлять в закладки страницу редактирования и (или) отправлять URL-адрес другим пользователям.

Получение значений post формы

Существует множество способов доступа к опубликованным параметрам формы в нашем методе "Изменить" HTTP POST. Один простой подход заключается в том, чтобы использовать свойство Request в базовом классе Controller для доступа к коллекции форм и получения опубликованных значений напрямую:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    // Retrieve existing dinner
    Dinner dinner = dinnerRepository.GetDinner(id);

    // Update dinner with form posted values
    dinner.Title = Request.Form["Title"];
    dinner.Description = Request.Form["Description"];
    dinner.EventDate = DateTime.Parse(Request.Form["EventDate"]);
    dinner.Address = Request.Form["Address"];
    dinner.Country = Request.Form["Country"];
    dinner.ContactPhone = Request.Form["ContactPhone"];

    // Persist changes back to database
    dinnerRepository.Save();

    // Perform HTTP redirect to details page for the saved Dinner
    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

Приведенный выше подход является немного подробным, однако, особенно после добавления логики обработки ошибок.

Лучше всего использовать встроенный вспомогательный метод UpdateModel() в базовом классе Controller. Он поддерживает обновление свойств передаваемого объекта с помощью входящих параметров формы. Он использует отражение для определения имен свойств объекта, а затем автоматически преобразует и присваивает им значения на основе входных значений, отправленных клиентом.

Мы можем использовать метод UpdateModel() для упрощения действия изменения HTTP-POST с помощью следующего кода:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    UpdateModel(dinner);

    dinnerRepository.Save();

    return RedirectToAction("Details", new { id = dinner.DinnerID });
}

Теперь мы можем перейти по URL-адресу /Dinners/Edit/1 и изменить название ужина:

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

При нажатии кнопки "Сохранить" мы выполняем запись формы к нашему действию Изменить, и обновленные значения будут сохранены в базе данных. Затем мы будем перенаправлены по URL-адресу сведений для ужина (в котором будут отображаться только что сохраненные значения):

Снимок экрана: URL-адрес сведений для ужина.

Обработка ошибок редактирования

Текущая реализация HTTP-POST работает нормально, за исключением случаев, когда возникают ошибки.

Когда пользователь допустил ошибку при редактировании формы, необходимо убедиться, что форма переиграна с информативным сообщением об ошибке, которое поможет ему исправить ее. Сюда входят случаи, когда конечный пользователь отправляет неверные входные данные (например, неправильно сформированную строку даты), а также случаи, когда формат входных данных является допустимым, но есть нарушение бизнес-правил. При возникновении ошибок форма должна сохранять входные данные, введенные пользователем, чтобы им не приходилось вносить изменения вручную. Этот процесс должен повторяться столько раз, сколько потребуется, пока форма не будет успешно завершена.

ASP.NET MVC включает некоторые удобные встроенные функции, которые упрощают обработку ошибок и повторное воспроизведение форм. Чтобы увидеть эти функции в действии, обновим наш метод действия Изменить следующим кодом:

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {

        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {

        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Приведенный выше код аналогичен предыдущей реализации, за исключением того, что теперь мы упаковаем блок обработки ошибок try/catch вокруг нашей работы. Если исключение возникает при вызове UpdateModel() или при попытке сохранить DinnerRepository (что вызовет исключение, если объект Dinner, который мы пытаемся сохранить, является недопустимым из-за нарушения правила в нашей модели), будет выполнен блок обработки ошибок catch. В нем мы переберем все нарушения правил, существующие в объекте Dinner, и добавляем их в объект ModelState (который мы обсудим в ближайшее время). Затем мы переиграем представление.

Чтобы увидеть, что это работает, давайте повторно запустите приложение, измените ужин и измените его, чтобы у него было пустое название, eventDate "BOGUS", и использовать номер телефона Великобритании со значением страны или региона США. При нажатии кнопки "Сохранить" наш метод изменения HTTP POST не сможет сохранить ужин (из-за ошибок) и переиграет форму:

Снимок экрана: повторное воспроизведение формы из-за ошибок с использованием метода H T P PO S T Edit.

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

Как это произошло? Как текстовые поля Title, EventDate и ContactPhone выделены красным цветом и выводятся изначально введенные значения пользователя? И как сообщения об ошибках отображались в списке вверху? Хорошая новость заключается в том, что это произошло не по волшебство, а потому, что мы использовали некоторые встроенные функции ASP.NET MVC, которые упрощают проверку входных данных и обработку ошибок.

Общие сведения о ModelState и вспомогательных методах HTML проверки

Классы контроллеров имеют коллекцию свойств ModelState, которая позволяет указать, что в объекте модели, передаваемом в представление, существуют ошибки. Записи ошибок в коллекции ModelState идентифицируют имя свойства модели с проблемой (например, "Title", "EventDate" или "ContactPhone") и позволяют указать понятное для человека сообщение об ошибке (например, "Требуется название").

Вспомогательный метод UpdateModel() автоматически заполняет коллекцию ModelState при возникновении ошибок при попытке назначить значения формы свойствам объекта модели. Например, свойство EventDate нашего объекта Dinner имеет тип DateTime. Когда методу UpdateModel() не удалось присвоить ему строковое значение "BOGUS" в приведенном выше сценарии, метод UpdateModel() добавил запись в коллекцию ModelState, указывающую на ошибку назначения с этим свойством.

Разработчики также могут написать код для явного добавления записей ошибок в коллекцию ModelState, как мы делаем ниже в блоке обработки ошибок catch, который заполняет коллекцию ModelState записями на основе активных нарушений правил в объекте Dinner:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }
}

Интеграция вспомогательного средства HTML с ModelState

Вспомогательные методы HTML, такие как Html.TextBox(), проверка коллекции ModelState при отрисовке выходных данных. Если для элемента существует ошибка, они отрисовывают введенное пользователем значение и класс ошибок CSS.

Например, в нашем представлении "Изменить" мы используем вспомогательный метод Html.TextBox() для отрисовки EventDate нашего объекта Dinner:

<%= Html.TextBox("EventDate", String.Format("{0:g}", Model.EventDate)) %>

При отображении представления в сценарии ошибки метод Html.TextBox() проверил коллекцию ModelState, чтобы проверить наличие ошибок, связанных со свойством EventDate объекта Dinner. Определив ошибку, он отрисовал отправленные пользовательский ввод ("BOGUS") в качестве значения и добавил класс ошибки CSS в созданную разметку <input type="textbox"/> :

<input class="input-validation-error"id="EventDate" name="EventDate" type="text" value="BOGUS"/>

Вы можете настроить внешний вид класса ошибок css так, чтобы он выглядел так, как вам нужно. Класс ошибок CSS по умолчанию — input-validation-error — определен в таблице стилей \content\site.css и выглядит следующим образом:

.input-validation-error
{
    border: 1px solid #ff0000;
    background-color: #ffeeee;
}

Это правило CSS привело к выделению недопустимых входных элементов, как показано ниже:

Снимок экрана: выделенные недопустимые входные элементы.

Вспомогательный метод Html.ValidationMessage()

Вспомогательный метод Html.ValidationMessage() можно использовать для вывода сообщения об ошибке ModelState, связанного с определенным свойством модели:

<%= Html.ValidationMessage("EventDate")%>

Приведенные выше выходные данные кода: <span class="field-validation-error"> Значение "BOGUS" недопустимо</span>

Вспомогательный метод Html.ValidationMessage() также поддерживает второй параметр, позволяющий разработчикам переопределить отображаемое текстовое сообщение об ошибке:

<%= Html.ValidationMessage("EventDate","*") %>

Приведенный выше код выводит следующее: <span class="field-validation-error">*</span> вместо текста ошибки по умолчанию при наличии ошибки для свойства EventDate.

Вспомогательный метод Html.ValidationSummary()

Вспомогательный метод Html.ValidationSummary() можно использовать для отображения сводного сообщения об ошибке, сопровождаемого списком <ul><li/></ul> всех подробных сообщений об ошибках в коллекции ModelState:

Снимок экрана: список всех подробных сообщений об ошибках в коллекции ModelState.

Вспомогательный метод Html.ValidationSummary() принимает необязательный строковый параметр, который определяет сводное сообщение об ошибке для отображения над списком подробных ошибок:

<%= Html.ValidationSummary("Please correct the errors and try again.") %>

При необходимости можно использовать CSS, чтобы переопределить, как выглядит список ошибок.

Использование вспомогательного метода AddRuleViolations

Наша первоначальная реализация изменения HTTP-POST использовала оператор foreach в блоке catch, чтобы выполнить цикл по нарушениям правил объекта Dinner и добавить их в коллекцию ModelState контроллера:

catch {
        foreach (var issue in dinner.GetRuleViolations()) {
            ModelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
        }

        return View(dinner);
    }

Мы можем сделать этот код немного чище, добавив класс ControllerHelpers в проект NerdDinner и реализуя в нем метод расширения AddRuleViolations, который добавляет вспомогательный метод в класс ASP.NET MVC ModelStateDictionary. Этот метод расширения может инкапсулировать логику, необходимую для заполнения ModelStateDictionary списком ошибок RuleViolation:

public static class ControllerHelpers {

   public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors) {
   
       foreach (RuleViolation issue in errors) {
           modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
       }
   }
}

Затем мы можем обновить наш метод действия "Изменить HTTP-POST", чтобы использовать этот метод расширения для заполнения коллекции ModelState нарушениями правил ужина.

Завершить изменение реализаций метода действия

Приведенный ниже код реализует всю логику контроллера, необходимую для нашего сценария редактирования:

//
// GET: /Dinners/Edit/2

public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(dinner);
}

//
// POST: /Dinners/Edit/2

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formValues) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

Хорошая особенность реализации edit заключается в том, что ни класс Контроллер, ни шаблон Представления не должны знать ничего о конкретных правилах проверки или бизнес-правилах, применяемых нашей моделью Dinner. Мы можем добавить в модель дополнительные правила в будущем, и для их поддержки не нужно вносить какие-либо изменения в код в наш контроллер или представление. Это обеспечивает гибкость для легкого развития требований к приложениям в будущем с минимальными изменениями кода.

Создание поддержки

Мы завершили реализацию поведения "Изменить" класса DinnersController. Теперь давайте перейдем к реализации поддержки "Создать", которая позволит пользователям добавлять новые ужины.

Метод действия создания HTTP-GET

Для начала мы реализуем поведение HTTP GET нашего метода действия создания. Этот метод будет вызываться, когда кто-то посещает URL-адрес /Dinners/Create . Наша реализация выглядит следующим образом:

//
// GET: /Dinners/Create

public ActionResult Create() {

    Dinner dinner = new Dinner() {
        EventDate = DateTime.Now.AddDays(7)
    };

    return View(dinner);
}

Приведенный выше код создает новый объект Dinner и назначает его свойству EventDate одну неделю в будущем. Затем отображается представление, основанное на новом объекте Dinner. Так как мы не передали имя во вспомогательный метод View(), он будет использовать путь по умолчанию на основе соглашения для разрешения шаблона представления: /Views/Dinners/Create.aspx.

Теперь создадим этот шаблон представления. Это можно сделать, щелкнув правой кнопкой мыши в методе Создать действие и выбрав команду контекстного меню "Добавить представление". В диалоговом окне "Добавление представления" мы укажем, что мы передаём объект Dinner в шаблон представления и автоматически создадим шаблон":

Снимок экрана: добавление представления для создания шаблона представления.

При нажатии кнопки "Добавить" Visual Studio сохранит новое представление Create.aspx на основе шаблонов в каталог \Views\Dinners и откроет его в интегрированной среде разработки:

Снимок экрана: ID E для изменения кода.

Давайте внесите несколько изменений в созданный по умолчанию файл шаблонов по умолчанию и измените его так, чтобы он выглядел следующим образом:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
     Host a Dinner
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Host a Dinner</h2>

    <%=Html.ValidationSummary("Please correct the errors and try again.") %>
 
    <% using (Html.BeginForm()) {%>
  
        <fieldset>
            <p>
                <label for="Title">Title:</label>
                <%= Html.TextBox("Title") %>
                <%= Html.ValidationMessage("Title", "*") %>
            </p>
            <p>
                <label for="EventDate">EventDate:</label>
                <%=Html.TextBox("EventDate") %>
                <%=Html.ValidationMessage("EventDate", "*") %>
            </p>
            <p>
                <label for="Description">Description:</label>
                <%=Html.TextArea("Description") %>
                <%=Html.ValidationMessage("Description", "*") %>
            </p>
            <p>
                <label for="Address">Address:</label>
                <%=Html.TextBox("Address") %>
                <%=Html.ValidationMessage("Address", "*") %>
            </p>
            <p>
                <label for="Country">Country:</label>
                <%=Html.TextBox("Country") %>
                <%=Html.ValidationMessage("Country", "*") %>
            </p>
            <p>
                <label for="ContactPhone">ContactPhone:</label>
                <%=Html.TextBox("ContactPhone") %>
                <%=Html.ValidationMessage("ContactPhone", "*") %>
            </p>            
            <p>
                <label for="Latitude">Latitude:</label>
                <%=Html.TextBox("Latitude") %>
                <%=Html.ValidationMessage("Latitude", "*") %>
            </p>
            <p>
                <label for="Longitude">Longitude:</label>
                <%=Html.TextBox("Longitude") %>
                <%=Html.ValidationMessage("Longitude", "*") %>
            </p>
            <p>
                <input type="submit" value="Save"/>
            </p>
        </fieldset>
    <% } 
%>
</asp:Content>

И теперь, когда мы запускаем наше приложение и перейдем к URL-адресу "/Dinners/Create" в браузере, он будет отображать пользовательский интерфейс, как показано ниже в нашей реализации действия Создать:

Снимок экрана: создание реализации действия при запуске приложения и доступе к Dinners U R L.

Реализация метода действия СОЗДАНИЯ HTTP-POST

У нас реализована версия HTTP-GET нашего метода действия Create. Когда пользователь нажимает кнопку "Сохранить", он выполняет запись формы по URL-адресу /Dinners/Create и отправляет значения формы ввода> HTML <с помощью команды HTTP POST.

Теперь реализуем поведение HTTP POST нашего метода действия создания. Начнем с добавления перегруженного метода действия "Создать" в наш DinnersController с атрибутом AcceptVerbs, указывающим, что он обрабатывает сценарии HTTP POST:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {
    ...
}

Существует множество способов доступа к опубликованным параметрам формы в нашем методе Create с поддержкой HTTP-POST.

Один из подходов заключается в создании нового объекта Dinner и последующем использовании вспомогательного метода UpdateModel() (как это было с действием Изменить), чтобы заполнить его опубликованными значениями формы. Затем мы можем добавить его в наш DinnerRepository, сохранить его в базе данных и перенаправить пользователя к нашему действию Сведения, чтобы показать только что созданный Dinner с помощью приведенного ниже кода:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create() {

    Dinner dinner = new Dinner();

    try {
    
        UpdateModel(dinner);

        dinnerRepository.Add(dinner);
        dinnerRepository.Save();

        return RedirectToAction("Details", new {id=dinner.DinnerID});
    }
    catch {
    
        ModelState.AddRuleViolations(dinner.GetRuleViolations());

        return View(dinner);
    }
}

Кроме того, можно использовать подход, при котором метод действия Create() принимает объект Dinner в качестве параметра метода. ASP.NET MVC автоматически создаст для нас экземпляр нового объекта Dinner, заполнит его свойства с помощью входных данных формы и передаст его в наш метод действия:

//
//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {

    if (ModelState.IsValid) {

        try {
            dinner.HostedBy = "SomeUser";

            dinnerRepository.Add(dinner);
            dinnerRepository.Save();

            return RedirectToAction("Details", new {id = dinner.DinnerID });
        }
        catch {        
            ModelState.AddRuleViolations(dinner.GetRuleViolations());
        }
    }
    
    return View(dinner);
}

Приведенный выше метод действия проверяет, был ли объект Dinner успешно заполнен значениями post формы, проверяя свойство ModelState.IsValid. При возникновении проблем с преобразованием входных данных возвращается значение false (например, строка "BOGUS" для свойства EventDate) и при наличии каких-либо проблем, которые наш метод действия переиграет форму.

Если входные значения допустимы, метод действия пытается добавить и сохранить новый элемент Dinner в DinnerRepository. Он заключает эту работу в блок try/catch и повторно воспроизводит форму при наличии каких-либо нарушений бизнес-правил (что приведет к возникновению исключения методом dinnerRepository.Save().

Чтобы увидеть это поведение обработки ошибок в действии, можно запросить URL-адрес /Dinners/Create и указать сведения о новом ужине. Неправильные входные данные или значения приведут к повторному воспроизведению формы создания с выделенными ошибками, как показано ниже:

Снимок экрана: форма, переигровываемая с выделенными ошибками.

Обратите внимание, что наша форма создания учитывает те же правила проверки и бизнес-правила, что и форма редактирования. Это связано с тем, что наши правила проверки и бизнес-правила были определены в модели и не были внедрены в пользовательский интерфейс или контроллер приложения. Это означает, что позже мы можем изменить или усовершенствовать наши правила проверки или бизнес-правила в одном месте и применить их в нашем приложении. Нам не придется изменять код в наших методах действия "Изменить" или "Создать", чтобы автоматически учитывать новые правила или изменения существующих.

Когда мы исправим входные значения и снова нажмем кнопку "Сохранить", наше добавление к DinnerRepository будет выполнено успешно, и в базу данных будет добавлен новый Dinner. Затем мы будем перенаправлены по URL-адресу /Dinners/Details/[id] , где нам будут представлены сведения о вновь созданном ужине:

Снимок экрана: только что созданный ужин.

Удаление поддержки

Теперь добавим поддержку delete в dinnersController.

Метод действия HTTP-GET Delete

Начнем с реализации поведения HTTP GET метода действия удаления. Этот метод вызывается, когда кто-то посещает URL-адрес /Dinners/Delete/[id] . Ниже приведена реализация:

//
// HTTP GET: /Dinners/Delete/1

public ActionResult Delete(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
         return View("NotFound");
    else
        return View(dinner);
}

Метод действия пытается получить удаляемый ужин. Если dinner существует, он отрисовывает представление на основе объекта Dinner. Если объект не существует (или уже удален), он возвращает представление, которое отображает шаблон представления NotFound, созданный ранее для нашего метода действия Details.

Мы можем создать шаблон представления "Удалить", щелкнув правой кнопкой мыши метод действия Удалить и выбрав команду контекстного меню "Добавить представление". В диалоговом окне "Добавление представления" мы укажем, что мы передаем объект Dinner в шаблон представления в качестве модели, и создадим пустой шаблон:

Снимок экрана: создание шаблона представления

При нажатии кнопки "Добавить" Visual Studio добавит новый файл шаблона представления "Delete.aspx" в каталог \Views\Dinners. Мы добавим в шаблон html-код и код, чтобы реализовать экран подтверждения удаления, как показано ниже:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Delete Confirmation:  <%=Html.Encode(Model.Title) %>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>
        Delete Confirmation
    </h2>

    <div>
        <p>Please confirm you want to cancel the dinner titled: 
           <i> <%=Html.Encode(Model.Title) %>? </i> 
        </p>
    </div>
    
    <% using (Html.BeginForm()) {  %>
        <input name="confirmButton" type="submit" value="Delete" />        
    <% } %>
     
</asp:Content>

В приведенном выше коде отображается название удаляемого ужина и выводится <элемент формы> , который выполняет post, на URL-адрес /Dinners/Delete/[id], если пользователь нажимает кнопку "Удалить".

При запуске приложения и доступе к URL-адресу "/Dinners/Delete/[id]" для допустимого объекта Dinner отображается пользовательский интерфейс, как показано ниже:

Снимок экрана: подтверждение удаления ужина в методе действия H T T P G E T Delete.

Side Topic: Почему мы делаем POST?
Вы можете спросить: почему мы создали <форму> на экране подтверждения удаления? Почему бы просто не использовать стандартную гиперссылку для ссылки на метод действия, который выполняет фактическую операцию удаления? Причина в том, что мы хотим быть осторожными, чтобы защититься от веб-обходчиков и поисковых систем, обнаруживших наши URL-адреса и непреднамеренно вызывая удаление данных при переходе по ссылкам. URL-адреса на основе HTTP-GET считаются "безопасными" для доступа и обхода контента, и они не должны следовать http-POST. Хорошее правило заключается в том, что вы всегда помещаете деструктивные операции или операции изменения данных за запросы HTTP-POST.

Реализация метода действия удаления HTTP-POST

Теперь у нас есть версия HTTP-GET нашего метода действия Delete, которая отображает экран подтверждения удаления. Когда пользователь нажимает кнопку "Удалить", он выполнит запись формы по URL-адресу /Dinners/Dinner/[id] .

Теперь реализуем поведение HTTP POST метода действия удаления с помощью приведенного ниже кода:

// 
// HTTP POST: /Dinners/Delete/1

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Delete(int id, string confirmButton) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (dinner == null)
        return View("NotFound");

    dinnerRepository.Delete(dinner);
    dinnerRepository.Save();

    return View("Deleted");
}

Версия HTTP-POST метода действия Delete пытается получить удаляемый объект dinner. Если ему не удается найти его (так как он уже удален), отрисовывается наш шаблон NotFound. Если он находит Ужин, он удаляет его из DinnerRepository. Затем отрисовывается шаблон "Удаленный".

Чтобы реализовать шаблон "Удалено", щелкните правой кнопкой мыши метод действия и выберите контекстное меню "Добавить представление". Мы назовем наше представление "Удалено" и сделаем его пустым шаблоном (а не будем принимать строго типизированный объект модели). Затем мы добавим в него html-содержимое:

<asp:Content ID="Title" ContentPlaceHolderID="TitleContent" runat="server">
    Dinner Deleted
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Dinner Deleted</h2>

    <div>
        <p>Your dinner was successfully deleted.</p>
    </div>
    
    <div>
        <p><a href="/dinners">Click for Upcoming Dinners</a></p>
    </div>
    
</asp:Content>

И теперь, когда мы запустите наше приложение и получите доступ к URL-адресу "/Dinners/Delete/[id]" для допустимого объекта Dinner, он отобразит наш экран подтверждения удаления Dinner, как показано ниже:

Снимок экрана с подтверждением удаления ужина в методе действия H T T P PO S T Delete.

При нажатии кнопки "Удалить" выполняется http-POST по URL-адресу /Dinners/Delete/[id] , который удалит ужин из базы данных и отобразит шаблон представления "Удалено":

Снимок экрана: шаблон удаленного представления.

Безопасность привязки модели

Мы рассмотрели два разных способа использования встроенных функций привязки модели ASP.NET MVC. Первый использует метод UpdateModel() для обновления свойств существующего объекта модели, а второй использует поддержку ASP.NET MVC для передачи объектов модели в качестве параметров метода действия. Оба этих метода являются очень мощными и чрезвычайно полезными.

Эта власть также несет с собой ответственность. Важно всегда быть параноику в отношении безопасности при принятии любых пользовательских данных, и это также верно при привязке объектов к форме входных данных. Следует соблюдать осторожность, чтобы всегда кодировать любые введенные пользователем значения в ФОРМАТЕ HTML и JavaScript, а также будьте осторожны с атаками путем внедрения кода SQL (обратите внимание: мы используем LINQ to SQL для нашего приложения, которое автоматически кодирует параметры для предотвращения таких атак). Никогда не следует полагаться только на проверку на стороне клиента и всегда использовать проверку на стороне сервера для защиты от хакеров, пытающихся отправить вам фиктивные значения.

Один дополнительный элемент безопасности, который следует учитывать при использовании функций привязки ASP.NET MVC, — это область привязываемых объектов. В частности, необходимо убедиться, что вы понимаете последствия для безопасности свойств, которые вы разрешаете для привязки, и разрешить обновлять только те свойства, которые действительно должны быть обновлены конечным пользователем.

По умолчанию метод UpdateModel() попытается обновить все свойства объекта модели, соответствующие значениям входящих параметров формы. Аналогичным образом, объекты, передаваемые в качестве параметров метода действия, также по умолчанию могут иметь все свои свойства, заданные с помощью параметров формы.

Блокировка привязки для каждого использования

Вы можете заблокировать политику привязки для каждого использования, предоставив явный "список включения" свойств, которые можно обновить. Это можно сделать, передав дополнительный параметр массива строк методу UpdateModel(), как показано ниже:

string[] allowedProperties = new[]{ "Title","Description", 
                                    "ContactPhone", "Address",
                                    "EventDate", "Latitude", 
                                    "Longitude"};
                                    
UpdateModel(dinner, allowedProperties);

Объекты, передаваемые в качестве параметров метода действия, также поддерживают атрибут [Bind], который позволяет указать "список включения" разрешенных свойств, как показано ниже:

//
// POST: /Dinners/Create

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create( [Bind(Include="Title,Address")] Dinner dinner ) {
    ...
}

Блокировка привязки на основе типа

Вы также можете заблокировать правила привязки для каждого типа. Это позволяет указать правила привязки один раз, а затем применить их во всех сценариях (включая сценарии параметров UpdateModel и метода действия) во всех контроллерах и методах действий.

Вы можете настроить правила привязки для каждого типа, добавив атрибут [Bind] к типу или зарегистрировав его в файле Global.asax приложения (полезно для сценариев, в которых вы не владеете типом). Затем можно использовать свойства Include и Exclude атрибута Bind, чтобы управлять свойствами, которые можно привязать для конкретного класса или интерфейса.

Мы будем использовать этот метод для класса Dinner в приложении NerdDinner и добавим в него атрибут [Bind], который ограничивает список привязываемых свойств следующими:

[Bind(Include="Title,Description,EventDate,Address,Country,ContactPhone,Latitude,Longitude")]
public partial class Dinner {
   ...
}

Обратите внимание, что мы не разрешаем управлять коллекцией RSVP с помощью привязки, а также не разрешаем задавать свойства DinnerID или HostedBy с помощью привязки. По соображениям безопасности мы будем управлять этими свойствами только с помощью явного кода в наших методах действий.

Wrap-Up CRUD

ASP.NET MVC включает ряд встроенных функций, которые помогают реализовать сценарии публикации форм. Мы использовали различные эти функции для предоставления поддержки пользовательского интерфейса CRUD поверх нашего DinnerRepository.

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

Это обеспечит чистоту нашей архитектуры приложений и упростит тестирование. Мы можем добавить дополнительные бизнес-правила на уровень модели в будущем и не вносить изменения кода в контроллер или представление для их поддержки. Это обеспечит нам большую гибкость в развитии и изменении нашего приложения в будущем.

Наш DinnersController теперь включает описания и сведения о ужине, а также создание, изменение и удаление поддержки. Полный код для класса можно найти ниже:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/

    public ActionResult Index() {

        var dinners = dinnerRepository.FindUpcomingDinners().ToList();
        return View(dinners);
    }

    //
    // GET: /Dinners/Details/2

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    //
    // GET: /Dinners/Edit/2

    public ActionResult Edit(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);
        return View(dinner);
    }

    //
    // POST: /Dinners/Edit/2

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, FormCollection formValues) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        try {
            UpdateModel(dinner);

            dinnerRepository.Save();

            return RedirectToAction("Details", new { id= dinner.DinnerID });
        }
        catch {
            ModelState.AddRuleViolations(dinner.GetRuleViolations());

            return View(dinner);
        }
    }

    //
    // GET: /Dinners/Create

    public ActionResult Create() {

        Dinner dinner = new Dinner() {
            EventDate = DateTime.Now.AddDays(7)
        };
        return View(dinner);
    }

    //
    // POST: /Dinners/Create

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Create(Dinner dinner) {

        if (ModelState.IsValid) {

            try {
                dinner.HostedBy = "SomeUser";

                dinnerRepository.Add(dinner);
                dinnerRepository.Save();

                return RedirectToAction("Details", new{id=dinner.DinnerID});
            }
            catch {
                ModelState.AddRuleViolations(dinner.GetRuleViolations());
            }
        }

        return View(dinner);
    }

    //
    // HTTP GET: /Dinners/Delete/1

    public ActionResult Delete(int id) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");
        else
            return View(dinner);
    }

    // 
    // HTTP POST: /Dinners/Delete/1

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Delete(int id, string confirmButton) {

        Dinner dinner = dinnerRepository.GetDinner(id);

        if (dinner == null)
            return View("NotFound");

        dinnerRepository.Delete(dinner);
        dinnerRepository.Save();

        return View("Deleted");
    }
}

Следующий шаг

Теперь у нас есть базовая поддержка CRUD (создание, чтение, обновление и удаление) в классе DinnersController.

Теперь рассмотрим, как можно использовать классы ViewData и ViewModel для включения еще более полного пользовательского интерфейса в наших формах.