Asp.net-mvc – ASP.NET MVC 3 – Add/Remove from Collection Before Posting

asp.net-mvcrazor

I have a model that contains a collection, such as this:

class MyModel
{
    public List<MySubModel> SubModels { get; set; }
}

In the view, I want to dynamically add/remove from this list using Javascript before submitting. Right now I have this:

$("#new-submodel").click(function () {
    var i = $("#submodels").children().size();
    var html = '<div>\
                    <label for="SubModels[' + i + '].SomeProperty">SomeProperty</label>\
                    <input name="SubModels[' + i + '].SomeProperty" type="textbox" />\
                </div>'
    $("#submodels").append(html);
});

This works, but it's ugly. And, if I want to show those labels/textboxes for the existing items, there's no clean way to do that either (without duplicating).

I feel like I should be able to use Razor helpers or something to do this. Any ideas? Help me stay DRY.

Best Answer

You approach may lead to unexpected errors if you when you are removing or adding the divs. For example you have 4 items, you remove the first item, then $('#submodels').children().size() will return 3, but your last inserted div has the name attribute value set SubModels[3].SomeProperty which results in a conflict. And if your posted values contain SubModels[1] but not SubModels[0] the default model binder will fail to bind the list (it will bind it as null). I had to learn this the hard way...

To eliminate the aforementioned problem (and your's) I suggest you do something like this:

$("#addBtn").click(function() {
  var html = '<div class="submodel">\
                <label>SomeProperty</label>\
                <input type="textbox" />\
              </div>'; // you can convert this to a html helper!
  $("#submodels").append(html);
  refreshNames(); // trigger after html is inserted
});

$(refreshNames); // trigger on document ready, so the submodels generated by the server get inserted!

function refreshNames() {
  $("#submodels").find(".submodel").each(function(i) {
     $(this).find("label").attr('for', 'SubModels[' + i + '].SomeProperty');
     $(this).find("label").attr('input', 'SubModels[' + i + '].SomeProperty');
  });
}

Then your view (or even better an EditorTemplate for the SubModel type) can also generate code like:

 <div class="submodel">
    @Html.LabelFor(x => x.SomeProperty);
    @Html.EditorFor(x => x.SomeProperty);
 </div>

It would also be possible to convert the code generation to a html helper class, and use it in the EditorTemplate and in the JavaScript code