65

If I want a set of inputs in a form to bind to a List in MVC 4, I know that the following naming convention for input name attributes will work:

<input name="[0].Id" type="text" />
<input name="[1].Id" type="text" />
<input name="[2].Id" type="text" />

But I am curious about how forgiving the model binder is. For example, what about the following:

<input name="[0].Id" type="text" />
<input name="[3].Id" type="text" />
<input name="[8].Id" type="text" />

How would the model binder handle this? Would it bind to a List of length 9 with nulls? Or would it still bind to a List of length 3? Or would it choke altogether?

Why I care

I want to implement a dynamic form in which the user may add rows to the form, and also may delete rows from the form. So if I a user deletes row 2 out of 8 total rows, I want to know if I'll need to renumber all of the subsequent inputs.

Eric
  • 5,842
  • 7
  • 42
  • 71
  • 1
    Take a look at this: http://stackoverflow.com/questions/7807127/non-sequential-list-binding-not-working. The question shows a syntax for non-sequential binding (and the answer confirms its availability in MVC2 and higher). I've used it on occasion. As to your original question: If I recall correctly, it will fail...but it's easy enough to try before you bother with manual indexed, non-sequential binding. – Tim M. Feb 11 '13 at 23:11
  • 1
    @TimMedora, Thanks - non-sequential binding was a helpful piece of terminology that I lacked. – Eric Feb 11 '13 at 23:22
  • 4
    The topic is surprisingly obscure. If you search around there are a few good blogs on the subject, and this book covers it well in one of the later chapters: http://www.amazon.com/Pro-ASP-NET-MVC-3-Framework/dp/1430234040 – Tim M. Feb 11 '13 at 23:29
  • 2
    Check this blog post from [Phil haack](http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx). It may help you – Karthik Chintala Feb 12 '13 at 04:31

5 Answers5

50

There is a specific wire format for use with collections. This is discussed on Scott Hanselman's blog here:

http://www.hanselman.com/blog/ASPNETWireFormatForModelBindingToArraysListsCollectionsDictionaries.aspx

Another blog entry from Phil Haack talks about this here:

http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx

Finally, a blog entry that does exactly what you want here:

http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
18

I followed this approach linked in the blogs above and added a few details that might be helpful to some - especially as I wanted to dynamically add any number of rows but did not want to use AJAX to do so (I wanted the form to only submit in the post). I also did not want to worry about maintaining sequential ids. I was capturing a list of start and end dates:

View Model:

public class WhenViewModel : BaseViewModel {
    public List<DateViewModel> Dates { get; set; }
    //... Other properties
}

Start / End Date View Model:

public class DateViewModel {
    public string DateID { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
}

Then using them in the page (with datepicker):

<div class="grid-8-12 clear" id="DatesBlock">
@{
    foreach (DateViewModel d in Model.Dates) {
        @:<div class="grid-5-12 left clear">
            @Html.Hidden("Dates.Index", d.DateID)
            @Html.Hidden("Dates[" + d.DateID + "].DateID", d.DateID) //ID again to populate the view model
            @Html.TextBox("Dates[" + d.DateID + "].StartDate", 
                          d.StartDate.Value.ToString("yyyy-MM-dd"))
        @:</div>
        @:<div class="grid-5-12">
            @Html.TextBox("Dates[" + d.DateID + "].EndDate", 
                          d.EndDate.Value.ToString("yyyy-MM-dd"))
        @:</div>

        <script type="text/javascript">
            $('input[name="Dates[@d.DateID].StartDate"]')
               .datepicker({ dateFormat: 'yy-mm-dd'});
            $('input[name="Dates[@d.DateID].EndDate"]')
               .datepicker({dateFormat: 'yy-mm-dd'});
        </script>
     }
}
</div>
<a href="#" onclick="AddDatesRow()">Add Dates</a>

As the blog post linked in the @ErikTheVikings post above describe, the collection is created by the repeated hidden element: @Html.Hidden("Dates.Index", d.DateID) for each entry in the collection on the page.

I wanted to arbitrarily add rows without using AJAX to post data back to the server which I did by creating a hidden div containing a template of one "row" / item in the collection:

Hidden "Template" row:

<div id="RowTemplate" style="display: none">
    <div class="grid-5-12 clear">
        @Html.Hidden("Dates.Index", "REPLACE_ID")
        @Html.Hidden("Dates[REPLACE_ID].DateID", "REPLACE_ID") 
        @Html.TextBox("Dates[REPLACE_ID].StartDate", "")
    </div>
    <div class="grid-5-12">
        @Html.TextBox("Dates[REPLACE_ID].EndDate", "")
    </div>
</div>

Then used jQuery which clones the template, provides a random id to use for a new row and appends the now visible cloned row to the containing div above:

jQuery to complete the process:

<script type="text/javascript">
    function AddDatesRow() {
        var tempIndex = Math.random().toString(36).substr(2, 5);
        var template = $('#RowTemplate');
        var insertRow = template.clone(false);
        insertRow.find('input').each(function(){ //Run replace on each input
            this.id = this.id.replace('REPLACE_ID', tempIndex);
            this.name = this.name.replace('REPLACE_ID', tempIndex);
            this.value = this.value.replace('REPLACE_ID', tempIndex);
        });
        insertRow.show();
        $('#DatesBlock').append(insertRow.contents());

        //Attach datepicker to new elements
        $('input[name="Dates['+tempIndex+'].StartDate"]')
            .datepicker({dateFormat: 'yy-mm-dd' });
        $('input[name="Dates['+tempIndex+'].EndDate"]')
            .datepicker({dateFormat: 'yy-mm-dd' });
    }
</script>

JSFiddle example of the result: http://jsfiddle.net/mdares/7JZh4/

Matthew
  • 9,851
  • 4
  • 46
  • 77
3

I have dynamic list that looks like this:

<ul id="okvedList" class="unstyled span8 editableList">
<li>
    <input data-val="true" data-val-required="The Guid field is required." id="Okveds_0__Guid" name="Okveds[0].Guid" type="hidden" value="2627d99a-1fcd-438e-8109-5705dd0ac7bb">
    --//--
</li>

so when I add or remove the row (li element) i have to reorder items

    this.reorderItems = function () {
        var li = this.el_list.find('li');

        for (var i = 0; i < li.length; i++) {
            var inputs = $(li[i]).find('input');

            $.each(inputs, function () {
                var input = $(this);

                var name = input.attr('name');
                input.attr('name', name.replace(new RegExp("\\[.*\\]", 'gi'), '[' + i + ']'));

                var id = input.attr('id');
                input.attr('id', id.replace(new RegExp('_.*__', 'i'), '_' + i + '__'));
            });
        }
    };

this list placed into simple Html.BeginFrom from clientside and like List in action parameter on serverside

Evgeny Popov
  • 73
  • 1
  • 8
1

I also facing similar problem in the past, and I use KnockoutJS to handle such scenario.

Basically, Knockout send the collection in a JSON string, and I deserialized them in my controller.

For more info : http://learn.knockoutjs.com/#/?tutorial=collections

andri
  • 1,021
  • 1
  • 9
  • 16
0

I got little problem, when i using Chrome Browser and click the back button, and i find the input with type="hidden" when dynamically set values didn't handled properly by the Chrome Browser.

maybe we can change

<input type="hidden" name="Detes.Index" value="2016/01/06" />

to

<div style="display: none">
    <input type="text" name="Detes.Index" value="2016/01/06" />
</div>

Form more info: Chrome doesn't cache hidden form field values for use in browser history http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/

Community
  • 1
  • 1
thomas wu
  • 1
  • 1