How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET

Imagine you have a form, which allows a user to add n entries. In my case, a user was creating a Building and was defining each Room. Each room had a “Name” and “Area”;

public class Building
{
    [Required]
    public string Name { get; set; }
    public List<Room> Rooms { get; set; }

    public Building()
    {
        Rooms = new List<Room>();
    }
}

public class Room
{
    [Required]
    public string Name { get; set; }
    [Range(1,200)]
    public int Area { get; set; }
}

I had EditorTemplates defined for both Building and Room;

// Views\Building\EditorTemplates\Building.cshtml
@model DynamicListBinding.Models.Building

<div class="form-group">
    @Html.LabelFor(x => x.Name)
    @Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
    @Html.ValidationMessageFor(x => x.Name)
</div>

// Views\Building\EditorTemplates\Room.cshtml
@model DynamicListBinding.Models.Room

<div class="panel panel-default">
    <div class="panel-body">
        <div class="form-group">
            @Html.LabelFor(x => x.Name)
            @Html.TextBoxFor(x => x.Name, new { @class = "form-control" })
            @Html.ValidationMessageFor(x => x.Name)
        </div>

        <div class="form-group">
            @Html.LabelFor(x => x.Area)
            @Html.TextBoxFor(x => x.Area, new { @class = "form-control" })
            @Html.ValidationMessageFor(x => x.Area)
        </div>
    </div>
</div>

… and my Create.cshtml view looked like this;

// Views\Building\Create.cshtml
@model DynamicListBinding.Models.Building
@{
    ViewBag.Title = "Create";
}

@using (Html.BeginForm())
{ 
    <h2>Create</h2>

    <h3>Building</h3>
    @Html.EditorFor(x => x)

    <h3>Rooms</h3>
    @Html.EditorFor(x => x.Rooms)

    <input type="submit" />
}

In fact, you can download the skeleton of this solution from here (or just browse the repository on GitHub here).

Now, I didn’t know how many rooms each building had. My options were:

  1. Give the user an ample amount (say 20) of “Room” entries on the page to start off with, and hope the user wasn’t creating a mansion or castle.
  2. Load additional “Room” entries on-demand using AJAX.

If you’re wanting to use #1, unfortunately you’re in the wrong place, as this article explains how to go about #2. Sorry about that.

What makes #2 hard is how the DefaultModelBinder requires my list of rooms to be named (very specifically), like so;

<input type="text" name="Rooms[0].Name" />
<input type="number" name="Rooms[0].Area" />

<input type="text" name="Rooms[1].Name" />
<input type="number" name="Rooms[1].Area" />

<input type="text" name="Rooms[2].Name" />
<input type="number" name="Rooms[2].Area" />

There are 2 main problems with this:

  1. Imagine I allow my users to delete Rooms as well, and let’s say they delete Rooms[1]. The HTML becomes like so;
    <input type="text" name="Rooms[0].Name" />
    <input type="number" name="Rooms[0].Area" />
    
    <input type="text" name="Rooms[2].Name" />
    <input type="number" name="Rooms[2].Area" />
    

    Because of how DefaultModelBinder works, Room[2], will disappear from my model upon submission, as DefaultModelBinder requires index’s to be consecutive, and stops when it reaches a non-existent index.

  2. When loading additional fields in via AJAX, I need to be able to tell my endpoint what the next index is (because DefaultModelBinder requires index’s to be consecutive).

With these issues in mind, it’s very hard to allow your list of entries to be dynamically added and, potentially, deleted, whilst keeping these indexes sequential.

To make this less hard, Microsoft allow you to provide a .Index field (they just don’t tell anyone this…), which allows you to use any index you want; it doesn’t have to be sequential, and hell, it doesn’t even have to be a number.

We’d then be able to allow our users to delete fields, and the following HTML submission would now work;

<input type="hidden" name="Rooms.Index" value="0" />
<input type="text" name="Rooms[0].Name" />
<input type="number" name="Rooms[0].Area" />

<input type="hidden" name="Rooms.Index" value="2" />
<input type="text" name="Rooms[2].Name" />
<input type="number" name="Rooms[2].Area" />

However, our AJAX endpoint for adding new fields still needs to have some idea which index’s have been used, so it doesn’t generate additional fields with the same name. This approach also introduces the difficulty of using @Html.EditorFor(), whilst being able to output the .Index field.

To save the day, enter a HtmlHelper extension, EditorForMany();

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;

public static class HtmlHelperExtensions
{
    /// <summary>
    /// Generates a GUID-based editor template, rather than the index-based template generated by Html.EditorFor()
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <param name="html"></param>
    /// <param name="propertyExpression">An expression which points to the property on the model you wish to generate the editor for</param>
    /// <param name="indexResolverExpression">An expression which points to the property on the model which holds the GUID index (optional, but required to make Validation* methods to work on post-back)</param>
    /// <param name="includeIndexField">
    /// True if you want this helper to render the hidden &lt;input /&gt; for you (default). False if you do not want this behaviour, and are instead going to call Html.EditorForManyIndexField() within the Editor view. 
    /// The latter behaviour is desired in situations where the Editor is being rendered inside lists or tables, where the &lt;input /&gt; would be invalid.
    /// </param>
    /// <returns>Generated HTML</returns>
    public static MvcHtmlString EditorForMany<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> propertyExpression, Expression<Func<TValue, string>> indexResolverExpression = null, bool includeIndexField = true) where TModel : class
    {
        var items = propertyExpression.Compile()(html.ViewData.Model);
        var htmlBuilder = new StringBuilder();
        var htmlFieldName = ExpressionHelper.GetExpressionText(propertyExpression);
        var htmlFieldNameWithPrefix = html.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName);
        Func<TValue, string> indexResolver = null;

        if (indexResolverExpression == null)
        {
            indexResolver = x => null;
        }
        else
        {
            indexResolver = indexResolverExpression.Compile();
        }
                        
        foreach (var item in items)
        {
            var dummy = new { Item = item };
            var guid = indexResolver(item);
            var memberExp = Expression.MakeMemberAccess(Expression.Constant(dummy), dummy.GetType().GetProperty("Item"));
            var singleItemExp = Expression.Lambda<Func<TModel, TValue>>(memberExp, propertyExpression.Parameters);

            if (String.IsNullOrEmpty(guid))
            {
                guid = Guid.NewGuid().ToString();
            }
            else
            {
                guid = html.AttributeEncode(guid);
            }

            if (includeIndexField)
            {
                htmlBuilder.Append(_EditorForManyIndexField<TValue>(htmlFieldNameWithPrefix, guid, indexResolverExpression));
            }

            htmlBuilder.Append(html.EditorFor(singleItemExp, null, String.Format("{0}[{1}]", htmlFieldName, guid)));
        }

        return new MvcHtmlString(htmlBuilder.ToString());
    }

    /// <summary>
    /// Used to manually generate the hidden &lt;input /&gt;. To be used in conjunction with EditorForMany(), when "false" was passed for includeIndexField. 
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    /// <param name="html"></param>
    /// <param name="indexResolverExpression">An expression which points to the property on the model which holds the GUID index (optional, but required to make Validation* methods to work on post-back)</param>
    /// <returns>Generated HTML for hidden &lt;input /&gt;</returns>
    public static MvcHtmlString EditorForManyIndexField<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> indexResolverExpression = null)
    {
        var htmlPrefix = html.ViewData.TemplateInfo.HtmlFieldPrefix;
        var first = htmlPrefix.LastIndexOf('[');
        var last = htmlPrefix.IndexOf(']', first + 1);

        if (first == -1 || last == -1)
        {
            throw new InvalidOperationException("EditorForManyIndexField called when not in a EditorForMany context");
        }

        var htmlFieldNameWithPrefix = htmlPrefix.Substring(0, first);
        var guid = htmlPrefix.Substring(first + 1, last - first - 1);

        return _EditorForManyIndexField<TModel>(htmlFieldNameWithPrefix, guid, indexResolverExpression);
    }
        
    private static MvcHtmlString _EditorForManyIndexField<TModel>(string htmlFieldNameWithPrefix, string guid, Expression<Func<TModel, string>> indexResolverExpression)
    {
        var htmlBuilder = new StringBuilder();
        htmlBuilder.AppendFormat(@"<input type=""hidden"" name=""{0}.Index"" value=""{1}"" />", htmlFieldNameWithPrefix, guid);

        if (indexResolverExpression != null)
        {
            htmlBuilder.AppendFormat(@"<input type=""hidden"" name=""{0}[{1}].{2}"" value=""{1}"" />", htmlFieldNameWithPrefix, guid, ExpressionHelper.GetExpressionText(indexResolverExpression));
        }

        return new MvcHtmlString(htmlBuilder.ToString());
    }
}

Now, I’ll save how this works for another article; all we care about in this one is that it does. To use it, I have to do 2 things;

  1. Add a property to the model, which the EditorForMany helper will store the generated index in. Without this, the Html.Validation* methods will not work (see here for a deep-dive into “why” for the curious).
    public class Room
    {
        [Required]
        public string Name { get; set; }
        [Range(1,200)]
        public int Area { get; set; }
        public string Index { get; set; }
    }
  2. Substitute my Html.EditorFor(x => x.Rooms) in Create.cshtml with:
    @Html.EditorForMany(x => x.Rooms, x => x.Index);
    

… and all of our problems are solved! You’ll see that Html.EditorForMany() uses GUIDs rather than numbers for indexes. This removes the need for us to tell our AJAX endpoint which indexes as been used; as our AJAX endpoint will instead just generate a new GUID. Html.EditorForMany() also takes care of seamlessly producing the .Index field for us as well.

All that’s left to do is to get our AJAX endpoint up and running. To do this, I define a new action on my BuildingController;

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult AddRoom()
{
    var building = new Building();
    building.Rooms.Add(new Room());

    return View(building);
}

… and create a view Views\Building\AddRoom.cshml;

@model DynamicListBinding.Models.Building
@{
    Layout = null;
}

@Html.EditorForMany(x => x.Rooms, x => x.Index)

… and lo-and-behold, after adding the necessary JavaScript to AJAX in a new Room entry at the click of a button (to see what exactly what I changed, see the diff on GitHub), my form now works like a dream.

Note that there is a second way to use Html.EditorForMany(); which is included for scenarios where the hidden <input /> generated by Html.EditorForMany() would be an invalid child of the HTML parent (e.g. within elements such as a <tbody> or <ul>). Here, you’d pass false as the 3rd parameter to Html.EditorForMany(), then within your editor view call Html.EditorForManyIndexField(x => x.Index) to include the hidden <input /> in a valid place in the DOM.

In closing, I’d first like to point you to a blog post on haaked.com, and an answer by DaveMorganTexas on Stack Overflow; which helped me massively on this. I’d also like to thank the people who have commented on this article to highlight issues and improvements which I’ve incorporated over time.

You can browse the GitHub repository of the code used in this article, and download the final code as a zip from here.

21 thoughts on “How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET

  1. Thanks a million for the index info:

    Just wanted to add that if your need to dynamically create a small class then you could always throw in a hidden field like:

    then on the button click get the value and dynamically generate your form input without any extra ajax calls or extra helper methods like so:

    $(‘#newBtn’).click(function () {
    var trackNum = parseInt($(‘#PatientsIndexTrack’).val());
    var newTrack = trackNum + 1;
    var $parent = $(“”, { class: “alert alert-dismissible alert-outline” });
    $parent.append(“×”);
    $parent.append(“”);
    $parent.append(“”);
    $(‘.SectionToAppendData).append($parent);
    $(‘#PatientsIndexTrack’).val(newTrack);
    });

    It will bind into your model on submit AND if you iterate through your model like show on the view @for (int i = 0; i < Model.MyDynamicField.Count; i++) then the numbers will compress back down with no spaces. (If that makes sense to anyone)

    Anyway thank you for getting me started,

  2. @Laura: I’ve updated the code used in the EditorForMany() extension method, so that it no longer breaks for nested-lists; thanks for bringing it to my attention :).

  3. Hi Travis, whilst this works great for adding items, if you page supports removing items as well, and had items 0, 1, 2, 3, 4, then deleted item 2, item 3 and 4 would disappear from the POST submission (unless you re-aligned the successive indexes each time you removed an element). This is the behaviour I discuss in my post around “What makes #2 hard…”. Using the “.Index” property and something akin to GUIDs as the index field avoids this behaviour.

    Cheers,
    Matt

  4. Hi,
    I’ve got this up and running and works well however trying to use the seperate index field rendering and this is invalid as it doesn’t have an overload that supports 2 parameters:
    Html.EditorForManyIndexField(x => x.Rooms, x => x.Index)

    Has this been used successfully by anyone?

  5. Hi Paul, whoops; that’s a mistake in the article. EditorForManyIndexField expects a single parameter, which is a lambda pointing to the appropriate index field (e.g. @Html.EditorForManyIndexField(x => x.Index). (I’ve updated the article accordingly).

    Regards,
    Matt

  6. Are there any examples of how you would implement removing from the list?

  7. The code works very nice, is it possible to add “Remove” button/hyperlink which will delete the row selected by the user.

  8. Hey @matt, I was wondering how I could expand your solution further?

    Let’s say that each room had a property that was a drop down list which would be a FK into another model/table. Something like a staff member.

    Then if we took it one step further and say that for every room we add that we need to add zero -> many guests.

    Our models would be something like:
    Room {
    [Required]
    public string Name { get; set; }
    [Range(1,200)]
    public int Area { get; set; }
    [Required]
    public int StaffId {get;set;}
    [Required]
    public Staff Staff {get;set;}
    public List Guests {get;set;}
    }

    Staff {
    [Required]
    public int StaffId{get;set;}
    [Required]
    public string Name{get;set;}
    }

    Guest {
    [Required]
    public int GuestId {get;set;}
    [Required]
    public string Name {get;set;}
    }

    I would have no idea how to expand your solution to support the FK drop down lists and the child relationships. This question applies to what I am doing, but I figured a solution to this would guide me where I need to go.

  9. Hey Kirk,

    I’m not too sure what specific part on the expansion is causing you problems. It’s quite a broad subject, so if you could be more specific, I could provide more tailored help for that specific part.

    In general though;

    1. To support multi-guests-per-room, you’d have to add another AJAX endpoint (e.g. AddGuest) to generate additional guest form elements. That AddGuest endpoint would have to accept the “current” (e.g. what room the user wants to add guests to) as a parameter, so it could return the correct form element names.
    2. To add a drop-down of Staff, I would amend Room to include the following properties;

      public List<Staff> AvailableStaff { get; set; } // A list of all staff in the DB. Populated by the controller
      public int? SelectedStaffId { get; set; } // The ID of the selected staff member (if set!)
      public IEnumerable<SelectListItem> AvailableStaffSelectListItems 
      {
          get
          { 
              return AvailableStaff.Select(x => new SelectListItem
              {
                  Text = x.Name,
                  Value = x.StaffId
              });
           }
      }
      

      … then render the drop down list in your Room.cshtml view via…

      @Html.DropDownListFor(x => x.SelectedStaffId, Model.AvailableStaffSelectListItems)

      https://msdn.microsoft.com/en-us/library/system.web.mvc.html.selectextensions.dropdownlistfor(v=vs.118).aspx

    I haven’t tested the above; but the gist is there. Let me know how you get on.

    Regards,
    Matt

  10. Thank you so much for pointing me in the right direction. This is indeed a very challenging problem, but I believe you are solving it nicely. I instead am generating the new rooms on the JS side using HTML templates and then reworking the indexes – very dirty but it works.

    Thanks again, my issue was that I had named them “item[x].field” and therefore, needed a special VM with a List which was named “item”. Took over a day to do this.

Leave a Reply

Your email address will not be published. Required fields are marked *