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.
Model binding is a mechanism ASP.NET MVC uses to create parameter objects defined in controller action methods. The parameters can be of any type, from simple to complex ones. It simplifies working with data sent by the browser because data is automatically assigned to the specified model.
MVC doesn't use data bindings like old web api. You have to use model bindings in a MVC or MVVM approach.
Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders.
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/
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With