Saturday, January 30, 2016

Editing Collections in MVC

At work we've been rebuilding some of our core applications.  As a part of that effort I've been working on an internal MVC 4 solution used to provision our multi-tenant application.  The application fits nicely into the MVC pattern, it's a simple CRUD app.  The user fills in forms and posts them to the server.  Everything was great until I started working on more complex forms that are for aggregate root objects.  How do you edit the child collections on the parent object without round tripping to the server?  In this scenario we want to edit the child collections and post the object back to the server one time.

For development speed I didn't want to introduce the complexity and overhead of Angular 1.x or 2.x, and I didn't want to lose the strong typing and unobtrusive client side validation that .NET MVC provides.  While Angular has its place, it feels like overkill to need another client side framework to edit a few collections in a form.  To keep complexity low I wanted to find a pattern that used as little java-script as possible and would be usable throughout the application.

After digging into the issue I found several blog posts on the topic, among those the two below explain the principles of how this works.

Phil Hack - 2008 - Model Binding To A List

Ivan Zlatev - 2011 Series - Editing Variable Length Reorderable Collections in ASP.NET MVC – Part 1: ASP.NET MVC Views

The heart of the issue revolves around MVC model binding.  The default model binding uses the object and property name as the html id and name attribute.  For example, say we have an object called Asset with a property Url that is placed into our view using the EditorFor Html helper.

This would result in raw HTML that looks like this...

<div class="form-group">
    <label class="control-label col-md-2" for="Asset_Url">URL *</label>
    <div class="col-md-10">
        <input class="form-control text-box single-line" data-val="true" data-val-required="The URL * field is required." id="Asset_Url" name="Asset.Url" type="text" value="~/Content/DealerContent/23138/site.css">
        <span class="field-validation-valid text-danger" data-valmsg-for="Asset.Url" data-valmsg-replace="true"></span>

Notice the id, for, name, data-val-required and data-valmsg-for all our object name and property in them.  Also be aware that using EditorTemplates and nesting your model through the templates will affect this naming scheme.  For example in another of my apps I have an id that looks like this, "Client_ClientSecrets_0__Description". These are the attributes that MVC model binding and unobtrusive client side validation tie into. If you write out a collection you'll notice that an index position gets added into the attribute.

<div class="col-md-10">
    <input class="form-control text-box single-line" data-val="true" data-val-required="The URL * field is required." id="Assets_0__Url" name="Assets[0].Url" type="text" value="~/Content/DealerContent/23138/site.css">
    <span class="field-validation-valid text-danger" data-valmsg-for="Asset[0].Url" data-valmsg-replace="true"></span>

MVC model binding knows you have a collection when it picks up the index in the html attribute names. It's smart, but finicky. If your indexes get out of sequence when your form posts you'll find collections that are missing items.

To recap and boil the problem down, when editing collections client side the html input elements must be formatted as the model binder expects, and the collection index must stay in order.

 As a side note, we are also interested in keeping the unobtrusive client side validation working. The wiring for validation runs after the page loads, therefor items added dynamically into the page are unknown by by client side validation. To get around this we'll add in little extra jQuery magic.  Now onto the code.

One significant thing to note is that this solution doesn't use any C# html helpers.  We use a simple for loop with the editor template to write out our collection.  The for our partial template item, the editor template is reused.  If you want to avoid loading yet another JavaScript library, this may be a useful pattern!