Pull to refresh

Динамические формы в ASP.NET MVC

ASP *
Tutorial
Часто у пользователя требуется узнать информацию о нескольких дополнительных объектах, число которых заранее не известно. Для это используют динамические формы, поля которых создаются javascript кодом на клиентской машине. В asp.net mvc работая в связке Controller-View мы работаем с типизированными объектами. Преобразованием значений из requestа пришедшего с клиента в типизированный объект занимается класс ModelBinder. Для простых объектов это достаточно тривиальная задача. Но как правильно обработать динамические данные, имена параметров которые заранее неизвестны. Решением этой задачи и посвящен этот пост.



В asp.net MVC контроллер (Controller) уведомляет представление (View) о изменениях модели передавая ему типизированный объект данных:

public class Customer
{
  public int CustomerID { get; set; }  // Unique key
  public string Name { get; set; }
}


Передача данных во View:
[AcceptVerbs(HttpVerbs.Get)]
public ViewResult CustomerView()
{
  return View(new Customer { CustomerID = 14, Name = "Papa Carlo"});
}


В представлении GiftView.aspx мы отображаем значения этого объекта:
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyNamespace.Customer>" %>
<asp:Content ID="mainContent" ContentPlaceHolderID="MainContent" runat="server">
  ID : <%= Html.Encode("CustomerID") %><br/>
  Name : <%= Html.Encode("Name") %><br/>
</asp:Content>


Так же в asp.net MVC существует раздельное использование GET и POST запроса с помощью аттрибута AcceptVerbs перед методом обработки данных контроллером. Данные, которые поступают в POST запросе обрабатываются ModelBinder-ом и инициализируют объект, который поступает параметром в метод для обработки данных:

[AcceptVerbs(HttpVerbs.Get)]
public ActionResult EditCustomer(int idCustomer)
{
  //тут получаем объект Customer из источника данных по idCustomer
  return View(Customer);
}

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult EditCustomer(Customer customer)
{
  if (ModelState.IsValid)
    {
    //если валидация прошла то сохраняем 
    }
  return View(customer);
}

Где View для этого метода контроллера:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyNamespace.Customer>" %>
<asp:Content ID="mainContent" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm()) { %>
  <%= Html.Hidden("CustomerID") %><br/>
  Name : <%= Html.TextBox("Name") %><br/>
   <input type="submit" value="Save" />
  <% } %>
</asp:Content>


Это очень удобно в использовании, но есть одна проблема когда мы сталкиваемся с заполнением динамических форм. Для добавления дополнительных полей используется javascript, который их сгенерирует. Но как их потом правильно передать на сервер, чтобы получить типизированный объект? Есть несколько решений этой ситуации. Но для начала расширим наш объект, с которым мы работаем в форме:

public class Ownership
{
  public string Name { get; set; }
  public int Price {get; set; }
}

public class Customer
{
  ...
  public List<Ownership> Ownerships { get; set; }
}


Первый метод:
В методе-обработчике POST-запроса не будем принимать никаких параметров, а вручную обработаем свойство Request.Params (NameValueCollection).
Второй метод:
В методе-обработчике POST-запроса принимем параметр FormCollection, и опять же вручную обработаем этот параметр.
Третий метод:
Напишем свою версию обработки данных (ModelBinder) и добавим на обработку определенного типа данных.

Но всё же есть четвертый способ — предоставить правильно имена данных в представлении, корректно написать создание новых полей и предоставить работу по их обработке стандартному ModelBinderу, а на входе получить уже заполненый объект типа Customer с данными. Итак наше EditCustomer View:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MyNamespace.Customer>" %>
<asp:Content ID="mainContent" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm()) { %>
  <%= Html.Hidden("CustomerID") %><br/>
  Name : <%= Html.TextBox("Name") %><br/>

<div class="add-ownership" id="AddOwnership">Add</div>
<div id="Ownerships">
<% int index = 0;
 foreach (var ownership in Model.Ownerships)
{ %>
  <div class="ownership-container" id="OwnershipContainer<%= Html.Encode(index) %>">
  Name : <%= Html.TextBox("Ownerships[" + index.ToString() + "].Name")%>
  Price : <%= Html.TextBox("Ownerships[" + index.ToString() + "].Price")%>
  <div class="remove-ownership">Remove</div>
    <%} %>
  </div>
  <% 
  index++;
} %>
</div>
<input type="submit" value="Save" />
  <% } %>

//объявим javascript переменные для сохранения правильности счетчика (использование см. дальше)
<script type="text/javascript">
    var OwnershipsCount = <%= Model.Ownerships.Count.ToString() %>;
</script>
</asp:Content>


Теперь опишем правильно jquery для добавления и удаления дополнительных полей:
$().ready(function() {

///Add/Remove
$("#AddOwnership").click(AddOwnership);
$(".remove-ownership").click(RemoveOwnership);
});



Теперь опишем функцию на добавление:
function AddOwnership() 
{
//добавляем поля
  var OwnershipContainer = $("<div/>").attr("class", "ownership-container").attr("id", "OwnershipContainer" + OwnershipsCount).appendTo("#Ownerships");
  OwnershipContainer.text("  Name : ");
  $("<input/>").attr("type", "text").attr("name", "Ownerships[" + OwnershipsCount + "].Name").attr("id", "Ownerships[" + OwnershipsCount + "]_Name").appendTo(OwnershipContainer);
  OwnershipContainer.text("  Price : ");
  $("<input/>").attr("type", "text").attr("name", "Ownerships[" + OwnershipsCount + "].Price").attr("id", "Ownerships[" + OwnershipsCount + "]_Price").appendTo(OwnershipContainer);
  
  var RemoveButton = $("<div/>").attr("class", "remove-ownership").text("Remove").appendTo(OwnershipContainer);
//на нажатие этого элемента добавляем обработчик - функцию удаления
  RemoveButton.click(RemoveOwnership);
  OwnershipsCount++;
}


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

function RemoveOwnership() {
  var RecalculateStartNum = parseInt($(this).parent().attr("id").substring("OwnershipContainer".length));
  //удаляем этот div
  $(this).parent().remove();
  for (var i = RecalculateStartNum+1; i < OwnershipsCount; i++) 
  {
  //функция пересчета аттрибутов name и id
    RecalculateNamesAndIds(i);
  }
  OwnershipsCount--;
}

function RecalculateNamesAndIds(number) {
  var prevNumber = number - 1;
  $("#OwnershipContainer" + number).attr("id", "OwnershipContainer" + prevNumber);
  //скобки "[" и "]" которые присутствуют в id DOM-объекта в jquery селекторе необходим экранировать двойным обратным слэшем \\
  $("#Ownerships\\[" + number + "\\]_Name").attr("id", "Ownerships[" + prevNumber + "]_Name").attr("name", "Ownerships[" + prevNumber + "].Name");
  $("#Ownerships\\[" + number + "\\]_Price").attr("id", "Ownerships[" + prevNumber + "]_Price").attr("name", "Ownerships[" + prevNumber + "].Price");
}


Вот и всё. Таким образом именованные поля формы стандартный ModelBinder сможет корректно обработать.
Tags:
Hubs:
Total votes 34: ↑23 and ↓11 +12
Views 17K
Comments Comments 12