Ioannis Panagopoulos blog

Tutorials on HTML5, Javascript, WinRT and .NET

ASP.NET MVC Binding to Lists–Enumerables on POST with JQuery

by Ioannis Panagopoulos

In this post we see how we can bind to editable IEnumerables-Lists in an ASP.NET MVC view and get the updated values in our HttpPost action. Moreover, we see how we can use JQuery to dynamically add/remove items from the list and have our updates transferred to the HttpPost action.

We start by defining a simple class named Client which will be our list element:

 

public class Client
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

We implement an Action method that creates a list of Client objects:

public ViewResult Simple()
{
    List<Client> Clients = new List<Client>();
    Clients.Add(new Client { FirstName = "Giannis", LastName = "Panagopoulos" });
    Clients.Add(new Client { FirstName = "Kostas", LastName = "Papadopoulos" });
    Clients.Add(new Client { FirstName = "Petros", LastName = "Georgiadis" });
    return View(Clients);
}

The simple view renders the list as follows (with input elements of type text for each one of the list elements)

<%using (Html.BeginForm()){ %>
    <%int idx=0;
      foreach (var c in Model) { %>
        <div id="Client<%:idx %>">
            <input type="hidden" name="Clients.index" value="<%:idx %>" />
            <%:Html.TextBox("Clients["+idx.ToString()+"].FirstName",c.FirstName) %>
            <%:Html.TextBox("Clients["+idx.ToString()+"].LastName",c.LastName) %>
        </div>
      <%  idx++; } %>
    <input type="submit" value="Submit" />
<%} %>

The HttpPost action that will handle the POST on submit button:

[HttpPost]
public ActionResult ImprovedServerRoundtrip(List<Client> Clients)
{
    return View("Result", Clients);
}

The trick for the correct mapping of the list elements in the POST action’s List is to provide for each element, a hidden field, that has the name of the list followed by the word index (in our case Clients.index) and a unique key (in our case an index number). Now you need to name all your input elements that refer to the properties of list-elements with the name of the list followed by the key enclosed in [] and the property name (in our example the Clients[1].FirstName refers to the FirstName property of the object with key 1 in the list. If you follows this rule, the mapping will occur seamlessly.

This solution is ok if you do not want to allow adding/removing elements from the list.

Now we use JQuery to allow additions/deletions of elements in the list at the client’s side. Our action methods remain the same and our view is extended as follows:

<script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
<div>
<%using (Html.BeginForm()){ %>
    <% int idx=0;
       foreach (var c in Model){ %>
       <div id="Client<%:idx %>">
           <input type="hidden" name="Clients.index" value="<%:idx %>" />
           <%:Html.TextBox("Clients["+idx.ToString()+"].FirstName",c.FirstName) %>
           <%:Html.TextBox("Clients["+idx.ToString()+"].LastName",c.LastName) %>
           <span onclick="removeClient('Client<%:idx %>')">X</span>
       </div>
    <%  idx++;} %>
    <input type="button" id="btnAddNewClient" value="Add client" />
    <div id="NewClients"></div>
    <input type="submit" value="Submit" />
<%} %>
</div>
<script type="text/javascript">
    function removeClient(DivName) {$("#" + DivName).remove();}
    var num = <%:Model.Count() %>;
    $("#btnAddNewClient").click(function () {
        var client = "Clients[" + num + "]";
        $("#NewClients").append("<div id='Client"+num+"'>"+"
                         "<input type='hidden' name='Clients.index' value=" + num + " />" +
                         "<input type='text' name='" + client + ".FirstName' />" +
                         "<input type='text' name='" + client + ".LastName' />" +
                         "<span onclick=\"removeClient('Client"+num+"')\">X</span>"+
                         "</div>");
        num++;
    });
</script>

We have added a script to the end of the view, that generates the same html as the one generated at the server for each of the elements in the list. Although this solves the problem, it replicates the view generation code for each element twice (once in the foreach statement and once within the script). If the Client object’s properties and/or the view logic for each element is more complex this can turn into a maintenance nightmare.

Therefore this solution is only recommended for quite simple list-element structures.

Another approach that avoids the view replication is to ask the server to render the view for the new list element through javascript with an AJAX call. This means that we create a partial view for the Client class:

<div id="Client<%:ViewData["key"] %>">
    <input type="hidden" name="Clients.index" value="<%:ViewData["key"] %>" />
    <%:Html.TextBox("Clients[" + ViewData["key"].ToString() + "].FirstName",
                     Model==null?"":Model.FirstName)%>
    <%:Html.TextBox("Clients[" + ViewData["key"].ToString() + "].LastName",
                     Model==null?"":Model.LastName)%>
    <span onclick="removeClient('Client<%:ViewData["key"] %>')">X</span>
</div>

And an action that generates this partial view:

public ViewResult RenderSingleClient(int key)
{
    // This key is passed to the view in order to put the index for
    // each element in the enumeration
    ViewData["key"] = key;
    return View();
}

The improved version of the view is as follows:

<script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
<div>
<%using (Html.BeginForm()){ %>
    <%int idx=0;
      foreach (var c in Model){
          ViewData["key"]=idx; %>
        <%Html.RenderPartial("RenderSingleClient", c);%>
      <%  idx++;} %>
    <input type="button" id="btnAddNewClient" value="Add client" />
    <div id="NewClients"></div>
    <input type="submit" value="Submit" />
<%} %>
</div>
<script type="text/javascript">
    function removeClient(DivName) {$("#" + DivName).remove();}
    var num = <%:Model.Count() %>;
    $("#btnAddNewClient").click(function () {
        var client = "Clients[" + num + "]";
        $.get( '/Clients/RenderSingleClient?key='+num,
                function(data) {$("#NewClients").append(data);});
        num++;
    });
</script>

Now both the foreach statement and the script rely on the the same control to render the view for the Client objects. The foreach statement uses the RenderPartial method with argument the current list-element and also sets the ViewData[“key”] value for the key. The script makes a get request to the RenderSignleClient action method with the appropriate key to get the html for the new list-element. We now have a single place where we write how the Client object will be rendered which is a great improvement from the previous version. The drawback is the additional round trip to the server since it is unnecessary application logic–wise and only necessary coding–wise.

Therefore this method is recommended if your view is quite complex and you can live with the extra round-trip since as you will see in the following you loose the ability to use Html helpers for the sake of avoiding the extra request to the server.

The last approach uses JQuery templates. Therefore you must first download and include the file that enables JQuery templates at: http://ajax.microsoft.com/ajax/jquery.templates/beta1/jquery.tmpl.js

Now all you have to do is create a JQuery template in the view and use the template to create both existing and new Client elements. The view is as follows:

<script src="../../Scripts/jquery-1.4.1.js" type="text/javascript"></script>
<script src="../../Scripts/jquery.tmpl.js" type="text/javascript"></script>
<div>
<%using (Html.BeginForm()){ %>
    <div id="ExistingClients"></div>
    <input type="button" id="btnAddNewClient" value="Add client" />
    <div id="NewClients"></div>
    <input type="submit" value="Submit" />
<%} %>
</div>
<script id="ClientTemplate" type="text/x-jquery-tmpl">
    <div id="Client${idx}">
        <input type="hidden" name="Clients.index" value="${idx}" />
        <input type="text" name="Clients[${idx}].FirstName" value="${FirstName}" />
        <input type="text" name="Clients[${idx}].LastName" value="${LastName}" />
        <span onclick="removeClient('Client${idx}')">X</span>
    </div>
</script>
<script type="text/javascript">
    <%int idx=0;
      foreach (var c in Model){
        ViewData["key"]=idx; %>
        $("#ClientTemplate").tmpl([{FirstName : "<%: c.FirstName %>",  
                                    LastName : "<%:c.LastName %>" ,
                                    idx: <%:idx.ToString() %>}]).appendTo("#ExistingClients");
        <% idx++;%>
    <%} %>
</script>
<script type="text/javascript">
    function removeClient(DivName) {$("#" + DivName).remove();}
    var num = <%:Model.Count() %>;
    $("#btnAddNewClient").click(function () {
        $("#ClientTemplate").tmpl([{FirstName : "",  
                                    LastName : "" ,
                                    idx: num}]).appendTo("#NewClients");
        num++;
    });
</script>

Note the <script> that contains the template and the way it is rendered both in the foreach statement (server-side) and the Javascript code (client-side). This approach does not require the additional request to the server and still maintains a single place where the view of the list-elements is defined. You loose though the ability to use the traditional HtmlHelper methods.

Therefore this approach is the most efficient if your view logic for the list-elements is not complex enough to require heavy use of HtmlHelper goodies.

This concludes our small trip to mapping enumerabes in ASP.NET MVC. The project containing all the approaches presented may be downloaded from

 

Shout it

blog comments powered by Disqus
hire me