Dynamic repeating field groups in ASP.Net MVC (with just a dash of Knockout.JS)
Disclaimer: This is a guide to a particular method of achieving a particular functionality using ASP.net MVC and Knockout.JS. I believe that this method is useful for beginners and in simple production cases. But I should stress that it does have some shortcomings like many such solutions. I hope to address some of these in a future post.
You can download the full source for this post here: https://github.com/Roysvork/KoMvcRepeatingFieldGroup
Data entry forms in our web apps are rarely straightforward. I was recently asked about a common problem… how to get a repeating list of data posted back to an MVC controller. In order to help me explain exactly what I mean, consider the following viewmodel as an example:
public class AddApplicantViewModel { public string Firstname { get; set; } public string Surname { get; set; } public List<Positions> { get; set; } }
Here we have a simple viewmodel representing a potential job applicant, with a repeating group for their previous roles. The UI might look look something like this:
So essentially we want the user to be able to add details of their past positions, the number of which we will not know beforehand. In this simple example we’ve given them buttons to add\remove fieldsets as needed. Lets first take a look at what ASP.net MVC can do to help us out.
Creating a repeating editor template
You may be familiar with editor templates in asp.net mvc. In simple terms these are a neat way of abstracting away part of our form that we may want to reuse. To create an editor template:
- Create a folder in your solution under Views/Shared called ‘EditorTemplates‘
- Create a partial view in this folder, and name it in the form ‘TypeName.cshtml‘
- Edit the new view to define your reusable content
In our case my editor template is called Position.cshtml. It is important that the name exactly matches the type name otherwise our html helper won’t find it. So that’s all quite simple and all well and good. My editor template looks like this:
@model KoMvcRepeatingFieldGroup.Models.Position <span class="editor-label"> @Html.LabelFor(model => model.CompanyName) </span> <span class="editor-field"> @Html.TextBoxFor(model => model.CompanyName) </span> ... Other fields omitted for brevity ... <span class="editor-label"> @Html.LabelFor(model => model.EndDate) </span> <span class="editor-field"> @Html.TextBoxFor(model => model.EndDate) </span>
Now what some people don’t know, is that the EditorFor html helper will automatically iterate if you point it at a property whose type implements IEnumerable. So the syntax that follows would render a field group for each position in our model:
@Html.EditorFor(model => model.Positions)
When our view is rendered, the name attributes of our input elements will have been amended using an array style convention like thus:
<input data-bind="value: companyName" id="Positions_0__CompanyName" name="Positions[0].CompanyName" type="text" value=""> ... ... <input data-bind="value: companyName" id="Positions_1__CompanyName" name="Positions[1].CompanyName" type="text" value="">
When the resulting html form is posted back to an MVC controller, the default model binder will automatically wire up the list of positions in our viewmodel with the corresponding data from each group. That was pretty easy, nice one MVC.
Making it dynamic
The limitation of the above approach is that as everything is rendered server side, we either need to know up front how many repeating groups we will want to give the user, or post to an MVC controller and have the server re-render our view with the appropriate number of groups as needed. While this would work, it’s a bit clunky and frankly we can do better!
We can solve this problem using Knockout.JS. Knockout provides a declarative way of defining interactions between html elements, by allowing you to specify bindings between them and a javascript ViewModel.
Note that for this next part it’d be good for the reader to have a basic knowledge of MVVM and Knockout.JS itself, but you should hopefully be able to get enough information to continue by clicking the links in bold
Here’s our viewmodel:
function createViewModel() { var createPosition = function () { return { companyName: ko.observable(), jobTitle: ko.observable(), startDate: ko.observable(), endDate: ko.observable() }; }; var addPosition = function() { positions.push(createPosition()); }; var removePosition = function() { positions.pop(); }; var firstname = ko.observable(); var surname = ko.observable(); var positions = ko.observableArray([createPosition()]); return { firstname: firstname, surname: surname, positions: positions, addPosition: addPosition, removePosition: removePosition }; }
Normally what you would do in a knockout based solution is to serialize your viewmodel and then post that back to the server, usually as json. However I’m going to suggest something slightly unorthadox which I believe simplifies things (especially for beginners) and use knockout for the behavior of my UI only. We’ll rely on a standard form submission coupled with the array naming conventions we discussed above to get our data into our model.
Our knockout-enabled Add view now looks like this… note the data-bind attributes on our buttons and the data_bind properties we are passing to the html helpers. These will be rendered as data-bind attributes on the resulting html, underscores are used just because hyphens aren’t valid in c# identifiers:
<h2>Add Applicant</h2> @using (Html.BeginForm()) { <fieldset> <legend>Applicant Details</legend> <span class="editor-label"> @Html.LabelFor(model => model.Firstname) </span> <span class="editor-field"> @Html.TextBoxFor(model => model.Firstname, new { data_bind = "value: firstname" }) </span> <span class="editor-label"> @Html.LabelFor(model => model.Surname) </span> <span class="editor-field"> @Html.TextBoxFor(model => model.Surname, new { data_bind = "value: surname" }) </span> </fieldset> <fieldset> <legend>Previous roles</legend> @Html.EditorFor(model => model.Positions) <button data-bind="click: addPosition">Add one</button> <button data-bind="click: removePosition">Remove one</button> </fieldset> <input type="Submit" value="save"/> }
And of course we’ve also added attributes to our Editor Template (again, edited for succinctness):
@model KoMvcRepeatingFieldGroup.Models.Position <!-- ko foreach: positions --> <fieldset> <span class="editor-label"> @Html.LabelFor(model => model.CompanyName) </span> <span class="editor-field"> @Html.TextBoxFor(model => model.CompanyName, new { data_bind = "value: companyName, namePath: true" }) </span> ... ... </fieldset> <!-- /ko -->
A key thing to point out here is the use of the foreach binding. This tells knockout that we should render the contents of this block for each entry in the positions array.
A slight fiddle we need in this approach is to make sure we pre-populate our array of positions with a blank position before we render our view. I chose to do this in a constructor, but you could just as easily use an object initializer. We just need this blank value to seed our editor template, otherwise nothing will render and so knockout will have nothing to work with. It’s a small price to pay for the simplicity of the approach IMHO, although I’m sure others may disagree.
Make sure that you have a reference to both jQuery and Knockout.JS client libraries. You can get these through nuget if you need them. The final step is to actually wire up the bindings in a document.ready event in our Add view:
$(document).ready(function () { var viewModel = createViewModel(); ko.applyBindings(viewModel); });
If you fire this sucker up, then hopefully you should be able to click the Add and Remove buttons and see the result and appreciate how powerful Knockout.JS can be. And we’ve only scratched the surface. There’s one more piece of the puzzle before we can submit our form however.
The NamePath binding
Now we’ve got our dynamic behavior up and running, but go ahead and inspect one of the new fields in the DOM after adding clicking the add button. You’ll see that unfortunately, knockout has just cloned the initial editor template, field names and all:
For our form submission to work, the second position entry fields will have to have a name of Positions[1].CompanyName, the third Positions[2] and so on. Fortunately we can do this too.
The eagle eyed among you may have noticed a second binding in our editor template namePath: true. This is a custom knockout binding that I have written especially for this purpose. You can download the javascript file here: knockout.namepathbinding.js. Just add this to the project and reference from your Add view.
The only requirement is that your input elements be inside a fieldset, and that this fieldset should be the root element of each repeating group. The binding just needs this to be able to determine which group is which when it does it’s thing. That should be all for now. Fire up your project again, and if all is well then you may finally revel in your achievement…
Here’s proof that the model binder interprets things correctly (I omitted the dates in this case):
Considerations
- As it stands, this example will only work well when adding new records. I will cover how to make this work with existing records in a future post. Having said that though, the problem is less significant when editing an existing (known) number of records.
- As I mentioned before, you must have a fieldset as the root of your repeating groups in order for the namepath binding to work.
- Make sure that when building your viewmodel on the server, you need to ‘seed’ the positions list with an empty object in order for the view to render and give knockout a template to work with.
- In addition to the above point, you may experience unexpected behavior if there is more than one seed value. Again, I hope to address this in a future post.
- There are some more ‘correct’ ways of achieving this same behaviour, but this particular approach is quite a gentle introduction for beginners, as well as showing off the flexibility of knockout.
So there you have it. I’ve uploaded the full source to GitHub for your delectation. Feel free to post any questions in the comments!
Pete
References:
http://msdn.microsoft.com/en-us/library/ee407414.aspx
http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-3-default-templates.html
http://coding-in.net/asp-net-mvc-3-how-to-use-editortemplates/
http://knockoutjs.com/
http://knockoutjs.com/documentation/foreach-binding.html
http://knockoutjs.com/documentation/json-data.html
http://www.asp.net/mvc
http://learn.knockoutjs.com/
http://addyosmani.com/blog/understanding-mvvm-a-guide-for-javascript-developers/
http://designwithpc.com/post/23483418018/using-mvvm-model-view-view-model-in-javascript
Great article! Looking forward for next part (editing)
Great article, lots of great ideas. One observation, though: you can use the following construct to fix the id/name attributes on the client side:
data-bind=”value: ProductName, attr: { id : ‘Items_’ + $index() + ‘__ProductName’, name: ‘Items[‘ + $index() + ‘].ProductName’ }”
Of course, this is hardcoding the “Items_” part, but that can be easily fixed with another function / variable.
I added a helper method that can be used like this:
<input data-bind=”@Html.DataBinding(m => m.Items, it => it.ProductName)” type=”text” value=”” />
More details at http://mdpopescu.blogspot.com/2013/07/posting-dynamic-master-detail-forms.html
Nice! Does this still work with IE7? It’s not so relevant now I guess but still good to know.
Hmm… I have no idea, I’m using the most recent version of jQuery (2.0.2) and it doesn’t support IE7 or IE8 anymore.
Nice article…
Any date for the editing part?
Thanks
No exact date yet I’m afraid, have been distracted by other things. Watch this space though!
Whenever I add a new portion it doesn’t reflect in the object behind. For example when I add a new position, the fields display in the view but when I go to submit there’s only the one original Position and the others are ignored. I think it’s because when I go to create a new position the binding isn’t creating a new Position object and adding it to the list reflected in the actual model. Any suggestions?
I believe I’ve found the issue from the original post. The problem is that the library for the namepath functionality doesn’t appear to be working. I’ve added it as reference and made sure everything is listed in a field set and field sets are the repeating regions. still not working.
Bind_data is not working, binddata is working, but ko.observable for any value is not binding. plz anybody guide me.
—sumit k suman
Nice article. . .could you please post editing part. Thanx
Thank you for this.
I have one question though. The client side validation works for only the first set. On adding new positions, the controls are not validated. What could be the solution to this please?
How do i dynamicaly repeat a ListBox with items with this code?
I’ve tried and now i’m stuck anyone can help me out?
I like this a lot thanks – since starting with Knockout I really didn’t want to give up the Editor/Display Templates.
The name path binding seems to work too, but I had to remove -1 from:
var parentIndex = $(parent).index() -1;
Since writing this have you come across any better ways/methods to dynamically repeat?
Thanks again
Ian
How to add Dynamic repeating field groups for List inside List ? e.g. In List, Position model has one more List say List. I am stuck with this nested List. Please do the needful. Thanks in advance
This worked for me too – with the modification suggested by Ian Klek. Thanks a lot!
What about edit part? Could you give any ideas?
I was struggling to rebind viewmodel without success!