Editing and binding nested lists with ASP.NET MVC 2

Dynamically editing lists of data and binding back to the model with MVC is a little complicated as the id’s of the form elements need to all tie up for binding to succeed. Recently I had a model, which contained a list of an object, which in turn contained another nested list. Getting this to easily bind back to the model when adding to the lists dynamically was a bit of a headache so I’ll explain how I did it.

This article is inspired by this article by Steve Sanderson, but I also explain how to adapt it to bind nested lists.

Download source

UPDATE 12/07/2011 – Okay after yesterdays update, client side validation didn’t work for the new elements added via Ajax. I took the code posted here and wrapped it up in an extension method that looks like this:

public static class HtmlClientSideValidationExtensions
{
    public static IDisposable BeginAjaxContentValidation(this HtmlHelper html, string formId)
    {
        MvcForm mvcForm = null;

        if (html.ViewContext.FormContext == null)
        {
            html.EnableClientValidation();
            mvcForm = new MvcForm(html.ViewContext);
            html.ViewContext.FormContext.FormId = formId;
        }

        return new AjaxContentValidation(html.ViewContext, mvcForm);
    }

    private class AjaxContentValidation : IDisposable
    {
        private readonly MvcForm _mvcForm;
        private readonly ViewContext _viewContext;

        public AjaxContentValidation(ViewContext viewContext, MvcForm mvcForm)
        {
            _viewContext = viewContext;
            _mvcForm = mvcForm;
        }

        public void Dispose()
        {
            if (_mvcForm != null) {
                _viewContext.OutputClientValidation();
                _viewContext.FormContext = null;
            }
        }
    }
}

I can then wrap this around the content I return via Ajax to get the validation elements to be rendered, then after applying to the DOM I call Sys.Mvc.FormContext._Application_Load(); to refresh validation. I’ve updated the post below to reflect these changes.

UPDATE: 11/07/2011 – As Zari pointed out in a comment, validation messages weren’t working for the nested items. This was due to a problem in the BeginCollectionItem method where I was passing the collection name to the GetIdsToResuse method before adding the container prefix, which stopped it from being able to find the ID to reuse, and instead generated a new GUID. When this was checked against the keys in the model state during validation it didn’t match and therefore no error was created. I moved my amendment to the start of the method which fixes this issue.

I was creating a simple CMS which has a page which can contain n zones, which in turn can contain n widgets. First I’m going to start with the page and zones.

I’m going to create some hard coded sample data and a simple view that is able to display this information. In my controller I’ve created an instance of my PageViewModel:

var viewModel = new PageViewModel()
{
    Name = "My Page",
    Zones = new List<ZoneViewModel>()
    {
        new ZoneViewModel()
        {
            Name = "Zone 1"
        },
        new ZoneViewModel()
        {
            Name = "Zone 2",
        }
    }
};

My view looks like this to display the name of the page, then an editor for the list of zones.


<% using (Html.BeginForm()) { %>
    <%: Html.EditorFor(m => m.Name)%>

    <fieldset>
        <legend>Zones</legend>
        <%: Html.EditorFor(m => m.Zones)%>
    </fieldset>

    <input type="submit" value="Save" />
<% } %>

To display the zones I’ve created an editor template for the ZoneViewModel which simply shows a textbox for the zone name.


<%: Html.EditorFor(m => m.Name) %>
<br />

If I run my solution I’m now presented with something like this:

If you look at the markup generated you can see the id and name attributes for the zones are made up of the Zones property of the PageViewModel, the index value for that zone, then the Name property for the ZoneViewModel. The name attribute is what is used to allow the ModelBinder to bind back to the model.

So now I have my list of zones I want to be able to add to it dynamically by clicking an ‘Add Zone’ button. The easiset way to do this is to create a partial view that displays my ZoneViewModel editor template, and return the output of this partial view via an ajax call.

First I have created my strongly typed partial view called NewZone:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ListBinding.Models.ZoneViewModel>" %>

<%: Html.EditorForModel() %>

Now I need the action method which I will call with ajax to return the HTML. You’ll notice the OutputCache attribute which stops Internet Explorer from caching the response.

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult GetNewZone()
{
    return PartialView("NewZone", new ZoneViewModel() );
}

So now all I need to do is create a button on my form, and use jQuery to make a call to my new contoller method. I’ve surrounded my list of zones in a DIV with an id of ‘zones’ which I will append the new HTML to. Here is my view with the updates:


<% using (Html.BeginForm()) { %>
    <%: Html.EditorFor(m => m.Name)%>

    <input id="add-zone" type="button" value="Add Zone" />

    <fieldset>
        <legend>Zones</legend>
        <div id="zones">
            <%: Html.EditorFor(m => m.Zones)%>
        </div>
    </fieldset>

    <input type="submit" value="Save" />
<% } %>

Now all that is needed is the  jQuery to make the ajax call and append the result to my ‘zones’ DIV:

<script type="text/javascript">
    $(document).ready(function () {
        $("#add-zone").click(function () {
            $.ajax({
                url: "Home/GetNewZone",
                success: function (data) {
                    $("#zones").append(data);
                    Sys.Mvc.FormContext._Application_Load();
                }
            });
        });
    });
</script>

The call to Sys.Mvc.FormContext._Application_Load() refreshes the validation on the page to include the new fields.

Now when I run the solution and click the button I can see an extra row added to my list of zones:

Looking good, but lets have a look at the markup:

You can see that the newly added zone doesn’t have the necessary name attribute to allow the ModelBinder to automatically bind back to the model, so if I were to submit this form now any value added to the new textbox would not get bound back. When MVC initially created the form it automatically added the correct HTML prefix to the elements within the list, but as I’m adding HTML using ajax and jQuery this isn’t happening automatically.

So how can we easily add the corrrect HTML prefix? Luckily Steve Sanderson comes to the rescue with his HtmlPrefixScopeExtensions. It contains a BeginCollectionItem method which I can wrap around the code in my ZoneViewModel editor template giving it a collection name of ‘Zones’. The extension method then generates a HTML prefix for each form element using the collection name and a GUID, and creates a hidden field to tell MVC that the GUID is used for the zone collection index. As my ajax call to add a new zone also uses the WidgetViewModel editor template is will also use this helper method and get a HTML prefix that will allow the value to be bound back to my model.

The only change required is to add a using statement for this extension method in my WidgetViewModel editor template:

<%  using (Html.BeginAjaxContentValidation("form0"))
    {
        using (Html.BeginCollectionItem("Zones"))
        { %>
            <%: Html.EditorFor(m => m.Name) %>
            <%: Html.ValidationMessageFor(m => m.Name) %>
            <br />
<%      }
    }
%>

I’ve also added a using statement for my BeginAjaxContentValidation extension method which makes sure validation elements are created for the fields retrieved via the ajax call.

The resulting markup, after adding a new zone looks like this:

Now clicking the save button will bind correctly.

Now I want to be able to add any number of widgets to any zone.

First I create an Editor Template for the WidgetViewModel

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ListBinding.Models.WidgetViewModel>" %>

<%: Html.EditorFor(m => m.Name) %>
<br />

Then I need to amend my ZoneViewModel Editor Template to add a list of the widgets and a button to create a new one

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ListBinding.Models.ZoneViewModel>" %>
<%@ Import Namespace="ListBinding.Helpers" %>

<%  using (Html.BeginAjaxContentValidation("form0"))
    {
        using (Html.BeginCollectionItem("Zones"))
        { %>
            <%: Html.EditorFor(m => m.Name) %>
            <%: Html.ValidationMessageFor(m => m.Name) %>
            <br />
<%      }
    }
%>

<fieldset>
    <legend>Widgets</legend>
    <input type="button" value="Add Widget" /><br />
    <%: Html.EditorFor(m => m.Widgets) %>
</fieldset>

If I run the solution I can see the following

Each zone now has a list of widgets that are generated using the hard coded sample data I created. If you look at the markup you can see how MVC has named the widget elements based on it’s default binding method, but as we’re using the extension method to use a GUID the widgets won’t currently bind back to the model.

To get these inner items to bind correctly I’ll first add the BeginCollectionItem extension method to my WidgetViewModel Editor Template

<% using (Html.BeginAjaxContentValidation("form0"))
    {
        using (Html.BeginCollectionItem("Widgets"))
        { %>
            <%: Html.EditorFor(m => m.Name) %>
            <%: Html.ValidationMessageFor(m => m.Name) %>
            <br />
<%      }
    }
%>

If you now view the markup you’ll see the following

The name attributes for the widgets have been set, but not in the context of which zone they belong to. If I tried to bind this back to the model it would fail as it wouldn’t know which zone these widgets should be under. To fix this there are two things that need to happen. The name attribute of  the hidden input needs to be changed to reflect which zone, as does the name attribute of the input for each widget. I can do this easily with a small amendment to the HtmlPrefixScopeExtensions BeginCollection method.

public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
    if (html.ViewData["ContainerPrefix"] != null)
    {
        collectionName = string.Concat(html.ViewData["ContainerPrefix"], ".", collectionName);
    }

    var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
    string itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();

    var htmlFieldPrefix = string.Format("{0}[{1}]", collectionName, itemIndex);

    html.ViewData["ContainerPrefix"] = htmlFieldPrefix;

    // autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
    html.ViewContext.Writer.WriteLine(string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />", collectionName, html.Encode(itemIndex)));

    return BeginHtmlFieldPrefixScope(html, htmlFieldPrefix);
}

All I’m doing here is adding the htmlFieldPrefix into ViewData, so that when second time this method is called, which will be the internal collections, it checks ViewData and amends the collection name and prefix accordingly. This way it will successfully bind back to the model. The resulting HTML looks like this:

Now the final step is to dynamically add new widgets to each zone. I can can do this in the same way as adding a zone by returning a partial view to a jQuery ajax command.

I create a partial view called NewWidget which is simply just an EditorForModel. I then add a DIV with the ID ‘widgets’ to my ZoneViewModel:

...

<fieldset>
    <legend>Widgets</legend>
    <div class="widgets">
        <input type="button" value="Add Widget" /><br />
        <%: Html.EditorFor(m => m.Widgets) %>
    </div>
</fieldset>

Now I can add a new method to my controller to return the NewWidget partial view:

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult GetNewWidget()
{
    return PartialView("NewWidget", new WidgetViewModel());
}

Finally I can add the jQuery to call this method and update the DOM. I’m using the jQuery live function so that when I add new zones via the ‘Add Zone’ button, it will attach this handler to the new ‘Add Widget’ button.

$(".add-widget").live("click", function () {
    var addButton = $(this);

    $.ajax({
        url: "Home/GetNewWidget",
        success: function (data) {
            addButton.closest(".widgets").append(data);
            Sys.Mvc.FormContext._Application_Load();
        }
    });
});

Here there may be multiple DIV tags containing widgets on the page, so I use the jQuery closest method to find the closest instance of a DIV with the class ‘widgets’, which searches from the button, then in an upward direction through the DOM. Now I can use the button to add a new widget to a zone.

Here you can see the newly added widget doesn’t have the necessary prefixes to enable it to be bound back. This is because the inner collection was using the container prefix that I added to ViewData in the helper class, but as I’m adding a new widget via a separate jQuery call it doesn’t exist in ViewData. What I need to do is somehow get the correct prefix into ViewData before rending the partial view.

To do this I’m using a HTML 5 data attribute on my ‘Add Widget’ button that contains the prefix for that zone. I chose to use the button as I’m already accessing the button in my jQuery function already, so it seemed an obvious choice. The new markup looks like this:

<fieldset>
    <legend>Widgets</legend>
    <div class="widgets">
        <input type="button" data-containerPrefix="<%= ViewData["ContainerPrefix"] %>" value="Add Widget" /><br />
        <%: Html.EditorFor(m => m.Widgets) %>
    </div>
</fieldset>

Here I’m putting the value from ViewData into the DOM, so that I can pass it with my ajax request to add a new widget. The markup for the button now looks like this:

Now I can change my AJAX call to include this value as a parameter:

$(".add-widget").live("click", function () {
    var addButton = $(this);

    $.ajax({
        url: "Home/GetNewWidget",
        data: { "containerPrefix": addButton.data("containerPrefix") },
        success: function (data) {
            addButton.closest(".widgets").append(data);
            Sys.Mvc.FormContext._Application_Load();
        }
    });
});

Then change the action method to add this value into ViewData:

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult GetNewWidget(string containerPrefix)
{
    ViewData["ContainerPrefix"] = containerPrefix;
    return PartialView("NewWidget", new WidgetViewModel());
}

Now when I add new zones and widgets everything binds nicely back.

Download source

Posted on by Joe in Ajax, ASP.NET, C#, JavaScript, jQuery, MVC

19 Responses to Editing and binding nested lists with ASP.NET MVC 2

  1. MAAC

    Hi Joe. The download link doesn’t work!

  2. Joe

    Thanks for that, now fixed. The link at the top worked but not the one at the bottom.

    Cheers
    Joe

  3. Hammar

    Great article!

    Just what I needed for my timesheet application.

    Rgds
    Mattias

  4. Joe

    Mattias

    Glad it helped you!

    Joe

  5. Hammar

    Hi Again Joe!

    Just noticed a small thing with the jQuery ajax call in your sample.

    It seems if you click “Add new Zone” repeatedly it will generate the same guids (or infact the exact same html respose) due to the jQuery cache feature. It won’t hit the controller GetNewZone method.

    I think you will have to add the following:
    $.ajax({
    url: “Home/GetNewZone”,
    cache: false
    success: function (data) {
    $(“#zones”).append(data);
    }
    });

    Rgds
    Mattias

  6. Joe

    Mattias

    Sorry about that, I only tested my sample in Firefox and forgot about the issue where Internet Explorer caches ajax requests.

    You can also add the OutputCache attribute to the action methods called via jQuery:

    [OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]

    I’ve updated the post and sample. Thanks for your input.

    Joe

  7. Faisal Shehzad

    Hi Joe,

    I cannot thank you enough for your blog post, it is exactly what I have been pulling my hair out at for the last 3 hours.

    Thank you so much.

    Faisal

  8. Joe

    Faisal

    Glad it helped. It’s all fairly straight forward when you understand how it all works.

    Cheers
    Joe

  9. Zari

    Hi Joe!

    Everything works fine with binding the model back with 2-level nested list.

    But there is small issue with displaying the validation error messages for the input fields of the 2nd-level nested list (widget few model in your example).

    For example add:
    [Required(ErrorMessage = "*")]
    to the “Name” property of each of your models

    Then add:
    @Html.ValidationSummary(false)
    to the form

    Then add:
    @Html.ValidationMessageFor(model => model.Name)
    near each model’s name editor

    When you add a zone with empty name and a new widget with an empty name and submit the form you will see that:
    1. ValidationSummary will display that zone name is empty
    2. Widget name is empty
    BUT
    ValidationMessageFor will add a label with the error to the Zone name only. There will be no error message for Widget name (despite that ValidationSummary displayed error messages for both Zone and Widget)

    What is the reason of it and is it possible to fix the ValidationMessageFor?to display the appropriate error message near the 2nd level nested input?

  10. Joe

    Hi Zari

    Thanks for your comment. You’re right, I messed up :)

    The issue was in the HtmlPrefixExtensions; I was passing the collection name to the GetIdsToReuse method before prefixing it with the ContainerPrefix. Due to that it wasn’t reusing the GUIDs for the nested items, and was generating a new GUID which caused validation to fail.

    I’ve fixed the source code and post, and tested with client side validation and everything seems to be working okay.

    Cheers
    Joe

  11. Joe

    A follow up from yesterdays comment. Client side validation didn’t work for new fields added with Ajax.

    I’ve implemented an extension method to output the necessary validation elements up updated the Ajax calls to refresh the validation.

  12. Zari

    Thanks, Joe!

    Works great now.

    Nested lists is a great feature for any site’s control panel.

  13. Kostas

    Very helpful tutorial. Thank you. Is there any chance to show us how can you save the the object with the nested lists using Entity Framework. Is there any work to be done before calling “EntityState.Modified” on the Page object state?

  14. Krishna

    Thanks Joe,

    Your post helped me figure the issue I was having with nested collections. Exactly what I needed.

  15. James

    Thanks Joe!

    This article is an absolute WINNER!! Binding a two level list was exactly my task and has caused ~90% hairloss. I was handling/trying it clientside building/trying to build and *maintain* the input names in js but to no avail, not consistently anyway when deleting/adding/re-ordering the inner list items. Now i can enjoy deleting heaps of code.

    I want to send some beer, pls email me details where it can be sent.

  16. Fritz Stugren

    This is great!

    I know you wrote this two years ago but I’m happy I found it!

    I have been using Sanderson’s helper for dynamic lists for a while for a MVC4 web site I am working on and I got to a “design place” where I can’t avoid trying to get a nested list if I want to get things working nice. I was thinking about extending the BeginCollectionItem to support embedded indexes and I found your post. Good stuff, thanks!

  17. Anand

    Dear Joe,
    Thanks you very much for writing such detailed article !!!!!

    I am trying simililar functionality in MVC4, but Ajax and Helpers are not working the way you have explained in MVC2.
    Can you please suggest me the changes required for MVC4.

  18. Chinna

    Thanks Joe,

    Really it helps lot.

  19. James

    Hi Anand, i got it working pretty much as explained in MVC4. What issues are you having – if you’re still having them, i realise this is a late reply…

    again, this article saved the day. It’s just epic.

Add a Comment