Editing Variable Length Reorderable Collections in ASP.NET MVC – Part 2: jQuery Templates


In Part 1 of this series I discussed the problems that we face when implementing an editor for a variable length, reorderable collection and reuse our server-side ASP.NET MVC views, model binding and validation. And while the solution provided in Part 1 is nice and clean (and I personally use it in a few projects) one field for improvement in true Agile Blogging fashion is to find a way to remove the AJAX request we use for fetching new editor rows.

One way to bring this completely to the client side is by the use of jQuery Template (or any other JavaScript templating engine/library).

A reminder of what our editor looks like before I begin and a second remind that the source code is available on GitHub:

To start with I have added a second link to our sample’s home page titled “Edit with jQuery templates”:

Corresponding controller actions:

public ActionResult EditJQueryTemplate()
{
    return View(CurrentUser);
}

[HttpPost]
public ActionResult EditJQueryTemplate(User user)
{
    if (!this.ModelState.IsValid)
        return View(user);

    CurrentUser = user;
    return RedirectToAction("Display");
}
And a new editor view (*EditJQueryTemplate.cshtml*) which is exactly the same as the one from Part 1 apart from the core editor bit:
<legend>My Favourite Movies</legend>

@if (Model.FavouriteMovies == null || Model.FavouriteMovies.Count == 0) {
    <p>None.</p>
}
<ul id="moviesEditor" style="list-style-type: none">
    @if (Model.FavouriteMovies != null) {
        foreach (Movie movie in Model.FavouriteMovies) {
            Html.RenderPartial("MovieEntryEditor", movie);
        }
    }
</ul>
<script src="@Url.Content("~/Scripts/jQuery.tmpl.min.js")" type="text/javascript"></script>

<script type="text/x-jquery-tmpl" id="movieTemplate">
    @Html.CollectionItemJQueryTemplate("MovieEntryEditor", new Movie())
</script>

<script type="text/javascript">
    $(function () {
        $("#moviesEditor").sortable();
    });

    var viewModel = {
        addNew: function () {
            $("#moviesEditor").append($("#movieTemplate").tmpl({ index: viewModel._generateGuid() }));
        },

        _generateGuid: function () {
            // Source: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/105074#105074
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }
    };
</script>
            
<a id="addAnother" href="#" onclick="viewModel.addNew();">Add another</a>
Note the new *@Html.CollectionItemJQueryTemplate* helper, which takes a collection item instance with default values and the unchanged partial view *MovieEntryEditor* from Part 1:
@model CollectionEditing.Models.Movie
           
<li style="padding-bottom:15px">
    @using (Html.BeginCollectionItem("FavouriteMovies")) {
        <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

        @Html.LabelFor(model => model.Title)
        @Html.EditorFor(model => model.Title)
        @Html.ValidationMessageFor(model => model.Title)

        @Html.LabelFor(model => model.Rating)
        @Html.EditorFor(model => model.Rating)
        @Html.ValidationMessageFor(model => model.Rating)

        <a href="#" onclick="$(this).parent().remove();">Delete</a>
    }
</li>

Basically the helper renders the partial with an ${$index} jQuery template placeholder for which we supply a GUID at run-time on the client-side when we render the template. The output of the helper looks like this:

<script type="text/x-jquery-tmpl" id="movieTemplate">
<li style="padding-bottom:15px">

    <input autocomplete="off" name="FavouriteMovies.Index" type="hidden" value="${index}" />

    <img src="/Content/images/draggable-icon.png" style="cursor: move" alt=""/>

    <label>Title</label>
    <input name="FavouriteMovies[${index}].Title" type="text" value="" />

    <label>Rating</label>
    <input name="FavouriteMovies[${index}].Rating" type="text" value="0" />

    <a href="#" onclick="$(this).parent().remove();">Delete</a>
</li>
</script>

Everytime the “Add another” button is pressed this template is rendered using a new GUID index value supplied by the viewModel class.

Here is the code of the @Html.CollectionItemJQueryTemplate helper:

public static MvcHtmlString CollectionItemJQueryTemplate<TModel, TCollectionItem>(this HtmlHelper<TModel> html, 
                                                                                    string partialViewName, 
                                                                                    TCollectionItem modelDefaultValues)
{
    ViewDataDictionary<TCollectionItem> viewData = new ViewDataDictionary<TCollectionItem>(modelDefaultValues);
    viewData.Add(JQueryTemplatingEnabledKey, true);
    return html.Partial(partialViewName, modelDefaultValues, viewData);
}

It renders the supplied partial view, but before that it sets a flag on the ViewContext’s ViewData dictionary to indicate that the view is to be rendered as jQuery collection item template. This is then handled by the @Html.BeginCollectionItem by generating an index template placeholder based .Index hidden field for model binding instead of one with an actual GUID value:

public static IDisposable BeginCollectionItem<TModel>(this HtmlHelper<TModel> html, string collectionName)
{
    string collectionIndexFieldName = String.Format("{0}.Index", collectionName);

    string itemIndex = null;
    if (html.ViewData.ContainsKey(JQueryTemplatingEnabledKey)) {
        itemIndex = "${index}";
    } else {
        itemIndex = GetCollectionItemIndex(collectionIndexFieldName);
    }

    string collectionItemName = String.Format("{0}[{1}]", collectionName, itemIndex);

    TagBuilder indexField = new TagBuilder("input");
    indexField.MergeAttributes(new Dictionary<string, string>() {
        { "name", collectionIndexFieldName },
        { "value", itemIndex },
        { "type", "hidden" },
        { "autocomplete", "off" }
    });
// snip---

That’s it! We are still reusing our ASP.NET views, model binding and validation, but no longer require an AJAX request to fetch a new editor row!.

In Part 3 I will look at Knockout JS templating, data binding and moving variable length reorderable collection editing completely on the client side with the MVVM pattern and using JavaScript form serialization and JSON data submission and model binding.

Next: Part 3 and Knockout JS client-side collection editing, JSON model binding and client-side validation.

[top]

comments powered byDisqus