How does MVC 4 List Model Binding work?
Solution 1:
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/
Solution 2:
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/