Editing Variable Length Reorderable Collections in ASP.NET MVC – Part 3: Knockout JS

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.

In Part 2 of this series I began moving some aspects of the collection editing to the client-side with jQuery Templates, but we realized that it’s not good enough, because instead of dealing with data we are juggling with our ASP.NET MVC views generated HTML.

In this final Part 3 of the series I want to move the collection editing completely to the client-side using the Knockout JS library JavaScript library. I will look at:

  • Knockout JS data-binding and tempalting
  • JSON Form (data really – no form) submission
  • jQuery Client-Side Validation

I will re-implement our sample collection editor and it is going to look exactly the same as it did before. Before I begin just a reminder that all of the source code is available on GitHub as a Visual Studio solution.

What is Knockout JS?

I am going to quote its creator (Steven Sanderson) from his introductory post, where he provides a very good overview:

Knockout is a JavaScript library that makes it easier to create rich, desktop-like user interfaces with JavaScript and HTML, using observers to make your UI automatically stay in sync with an underlying data model. It works particularly well with the MVVM pattern, offering declarative bindings somewhat like Silverlight but without the browser plugin.

I suggest you read the above linked post or checkout the Knockout JS web site to learn more about it, but to quickly summarize how it works:

  • A JavaScipt view model is defined that holds all the data. All the data properties are initialized as (or wrapped in) observable values and arrays.
  • HTML elements can be data-bound to a property (similar to WinForms/XAML(WPF/Silverlight)) from the view model using the data-bind attribute syntax. We can bind an input’s value, a span’s text, etc. to a view model property.
  • Whenever a value/or array changes the data-binding kicks in and the HTML element are automatically updated.
  • Knockout uses jQuery and supports templating via jQuery Templates.

Our Sample

To understand this lets jump straight into our sample app.

I have added a third edit option to our sample:

which surprise, surprise looks exactly the same as in the previous iteration. Apart from one extra feature – the favourite movies counter I’ve added to give you a hint of the power of Knockout JS:

View and ViewModel

Let’s firstly look at the first iteration of our view model implementation:

<script type="text/javascript">
var viewModel = {
    Id: ko.observable(@Model.Id),
    Name: ko.observable("@Model.Name"),
    FavouriteMovies: ko.observableArray(@Html.Json(@Model.FavouriteMovies) || []),

    maxMovies: 10,
    
    addMovie: function() {
        viewModel.FavouriteMovies.push(@Html.Json(new Movie()));
    },

    removeMovie: function(movie) {
        ko.utils.arrayRemoveItem(viewModel.FavouriteMovies, movie);
    }
};

$(function () {
    ko.applyBindings(viewModel);
});
</script>

We:

  • Serialize our ASP.NET MVC User Model to JSON (Id, Name, FavouriteMovies). Html.Json is a custom Html helper that simply wraps a JSON serializer – you can see it in the source code.
  • Wrap its values in Knockout observables and then once the page is loaded we initialize Knockout JS with our model
  • Define some add/remove movie methods to edit the FavouriteMovies collection

What’s crucial here is that we are not dealing with nor manipulating HTML markup, which is something that we were doing heavily in the previous parts.

When processed server-side the above view model will look like roughly like this in the browser:

var viewModel = {
        Id: ko.observable(1),
        Name: ko.observable("Ivan Zlatev"),
        FavouriteMovies: ko.observableArray([{"Title":"Movie 1","Rating":5},
                                                           {"Title":"Movie 2","Rating":10},
                                                           {"Title":"Movie 3","Rating":12}] || []),

        maxMovies: 10,

        addMovie: function() {
            viewModel.FavouriteMovies.push({"Title":null,"Rating":0});
        },

        removeMovie: function(movie) {
            ko.utils.arrayRemoveItem(viewModel.FavouriteMovies, movie);
        }
}

Let’s look at the first iteration of our edit view (EditKnockoutJS.cshtml) to better understand how does Knockout help us:

@model CollectionEditing.Models.User
@{ ViewBag.Title = "Edit My Account With Knockout JS"; }

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jQuery.tmpl.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout-1.2.1.debug.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/knockout.jQueryUI-sortable.js")" type="text/javascript"></script>

<h2>Edit</h2>

@using (Html.BeginForm("EditKnockoutJS", "User", FormMethod.Post, new { id = "profileEditorForm" })) {
    @Html.ValidationSummary(false)
    <fieldset>
        <legend>My Details</legend>

        <input name="userId" type="hidden" data-bind="value: Id"/>

        <label for="name">Name:</label>
        <input type="text" id="name" name="name" data-bind="value: Name" />
    </fieldset>
    
    <fieldset>
        <legend>My Favourite Movies</legend>

        <ul id="moviesEditor" style="list-style-type: none"
            data-bind="template: { name: 'moviesTemplate', data: FavouriteMovies }, 
                       sortableList: viewModel.FavouriteMovies" >
        </ul>
        <p>You have <span data-bind="text: FavouriteMovies().length"></span> favourite movies.</p>

        <script id="moviesTemplate" type="text/x-jquery-template">
            {{each(i, movie) $data}}
                <li data-bind="sortableItem: movie">
                    <div data-bind="template: { name: 'movieTemplate', data: movie }"></div>
                </li>
            {{/each}}
        </script>

        <script id="movieTemplate" type="text/x-jquery-template">
            <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

            <label>Title:</label>
            <input type="text" data-bind="value: Title"/>

            <label>Rating:</label>
            <input type="text" data-bind="value: Rating"/>

            <a href="#" data-bind="click: function() { viewModel.removeMovie(this); }">Delete</a>
        </script>
    
        <button data-bind="click: addMovie, enable: FavouriteMovies().length < maxMovies">Add another</button>
    </fieldset>

    <p>
        <input type="submit" value="Save" />
        <a href="/">Cancel</a>
    </p>
}

In our view we use Knockout JS to data-bind our HTML input fields to the data in the viewModel (notice the “data-bind” attributes).

For the collection editor:

  • Similar to Part 2, we have a movie jQuery template, which defines the HTML fields of a movie, but this time we don’t use any ASP.NET MVC helpers.
  • We make the HTML list reorderable via Drag and Drop with the two special sortableList and sortableItem bindings.This is a custom Knockout JS binding I’ve created and hosted on GitHub here. Behind the scenes it magically creates a JQuery UI Sortable keeps our viewModel.FavouriteMovies collection in sync when the widget changes.
  • We limit the number of favourite movies a user can add via the “Add Another” button to ten by using a “enabled” binding with a boolean expression. Behind the scenes Knockout JS will manage the state of the button and toggle between enabled/disabled depending on the evaluated value of the expression.
  • Finally we data-bind a span element to display the current number of favourite movies the user has added. Again, this is something that Knockout will keep up to date for us.

Right now with this viewModel and that view we have a working drag and drop add/remove favourite movies editor. What we are still missing however is:

  • Form submission and server-side handling
  • Form validation

Form Submission and Handling

Let’s add some more code to our view model and view to handle form submission.

Firstly we are going to implement a save function in our view model. Because we are no longer using ASP.NET model views, we can’t simply submit the form as is – the “standard” form values based ASP.NET MVC model binding won’t work properly and especially so for the collection editing part.

Thankfully starting from version 3 ASP.NET MVC supports out of the box JSON model binding. It kicks in if the HTTP request Content-Type is set to “application/json”. So instead of using form submission we are going to post our view model data via an AJAX post request to our MVC action, containing the data in JSON serialized form:

 var viewModel = {
       ... snip ...

        saveFailed: ko.observable(false),

        // Returns true if successful
        save: function()
        {
            var saveSuccess = false;
            viewModel.saveFailed(false);

            // Executed synchronously for simplicity
            jQuery.ajax({
                type: "POST",
                url: "@Url.Action("EditKnockoutJS", "User")",
                data: ko.toJSON(viewModel),
                dataType: "json",
                contentType: "application/json",
                success: function(returnedData) {
                    saveSuccess = returnedData.Success || false;
                    viewModel.saveFailed(!saveSuccess);
                },
                async: false
            });

            return saveSuccess;
        }
};

Then we will suppress the default form submission behaviour and replace it with our own:

<p>
    <input type="submit" value="Save" 
             data-bind="click: function() { viewModel.save(); return false; }"/>
    <a href="/">Cancel</a>
</p>
    
<p data-bind="visible: saveFailed" class="error">A problem occurred saving the data.</p>

Note that I have added a new “saveFailed” property in the viewModel which is set by save(). I have also used a “visible” data binding on the value of that property to display the error message paragraph only if the save operation has failed.

Now let’s also look at the MVC action to handle the form submission:

[HttpPost]
public ActionResult EditKnockoutJS(User user)
{
    FormResponseData responseData = new FormResponseData() {
        Success = false
    };

    if (this.ModelState.IsValid) {
        CurrentUser = user;
        responseData.Success = true;
    }

    return Json(responseData);
}

class FormResponseData {
    public bool Success { get; set; }
}

Pretty simple action, which just validates the Model using our validation rules/data annotations and returns a FormReponseData object serialized to JSON, which contains a Success flag expected by the client side.

We can now persist our view model and also use our server-side model validation to prevent the client from doing nasty things:

We are not done just yet. Because we no longer use the ASP.NET html helpers we no longer pull the server-side validation error messages and are currently left with the generic “Something went wrong.” error message. This isn’t exactly nice nor user friendly. We can solve this by the use of client-side JavaScript based validation. Remember that we should never ever rely solely on the client-side (and we don’t in our case).

Client-Side Validation

Unfortunately at the time of writing I couldn’t find anything that validates against the Knockout JS viewModel instead of the HTML markup, so I had to resort to jQuery Validation for the client-side validation. The problem with that is that we need to ensure that we have set proper unique “name” attributes on our form fields and especially so for our collection editor elements.

Fortunately Knockout JS already has a binding for that called the “uniqueName binding”, so all we have to do is add that to our favourite movie collection editor fields like I’ve showed below and don’t worry about generating them ourselves. This is great, because as you saw in Part 2 injecting a unique name during jQuery template evaluation is relatively tricky.

<script id="movieTemplate" type="text/x-jquery-template">
    <img src="@Url.Content("~/Content/images/draggable-icon.png")" style="cursor: move" alt=""/>

    <label>Title:</label>
    <input type="text" data-bind="value: Title, uniqueName: true" class="required"/>

    <label>Rating:</label>
    <input type="text" data-bind="value: Rating, uniqueName: true" class="required"/>

    <a href="#" data-bind="click: function() { viewModel.removeMovie(this); }">Delete</a>
</script>

There are multiple ways we can use jQuery Validation and I have decided to go for the simplest declarative one, where we decorate our fields with extra attributes to describe the value constraints that we want to enforce. Let’s update the above fields to make the Title and Rating required:

<label>Title:</label>
<input type="text" data-bind="value: Title, uniqueName: true" class="required"/>

<label>Rating:</label>
<input type="text" data-bind="value: Rating, uniqueName: true" class="required"/>

Finally let’s plug-in jQuery Validation. To do that we will remove the previous submit button handler and use jQuery Validation instead:

... snip... 

        <input type="submit" value="Save"/>

... snip... 

<script type="text/javascript">
    $(function () {
        ko.applyBindings(viewModel);

        $("#profileEditorForm").validate({
            submitHandler: function(form) {
                if(viewModel.save())
                    window.location.href = "/";

                return false;
            }
        });
    });
</script>

Which gives us nice per-field validation error messages as we type:

Wrapping up

Download all of the source code from GitHub as a Visual Studio solution.

In this part we looked at Knockout JS and how great it is in terms of helping us avoid manipulation of HTML markup among many other things. This is generally something that can get really messy quickly and become hard to maintain and track. We also looked at how it helps us neatly solve our collection editing problem without having to worry about practically anything. In the expense of the sacrifice of our Html helpers and partial views, etc we get a lot of flexibility on the client side to add more interactivity in a neat and clean way.

Final Words

If you have a simple collection editing scenario and you are heavily reliant on your ASP.NET MVC views, partial views, editors and displays the approach documented in Part 1 should be sufficient. And if pulling in new entries/rows via AJAX is not an option (e.g. for speed, performance or any other reasons) you can pull in Part 2. However IMHO the better solution most of the time is to use Knockout JS and what I have described in this part as it opens the doors for more complex client-side interactions in a neat and clean way.

Thank you for reading the series. I will appreciate your feedback in the comments!

Be Sociable, Share!
  • James

    Great stuff thanks for the post. Wouldn’t it be great if we could render our JS view model (and potentially our input form) using a helper wrapping our server-side data model? Maybe if I get some spare time on my next project.

    Thanks again.

  • http://www.internetworkconsulting.net shawn z

    When setting up an MVC 3 application,
    the foreign keys that should allow drop down lists to select an item
    do not get rendered as drop downs, but as static inputs. This can be
    resolved by creating a custom display and view for that field.

    We will need to start by creating a
    custom partial view that will live in
    “~/Views/Shared/DisplayTemplates/UserGuid.cshtml”, and
    “~/Views/Shared/EditTemplates/UserGuid.cshtml”. The code for one
    is located below:

    @model
    Guid

    @{

    incMvcSite.Models.MvcSiteDB
    db = new
    incMvcSite.Models.MvcSiteDB();

    incMvcSite.Models.SecUser
    usr = db.SecUsers.Single(u => u.Guid == Model);

    }

    @usr.Display

    This is a display for template that
    will look up the item in the referenced table and display it. We
    also need an edit for template as follows:

    @model
    Guid

    @{

    incMvcSite.Models.MvcSiteDB
    db = new
    incMvcSite.Models.MvcSiteDB();

    SelectList
    items = newSelectList(db.SecUsers.OrderBy(i
    => i.Display).ToList(), “Guid”,
    “Display”,
    Model);

    }

    @Html.DropDownList(“”,
    items)

    The edit for template is implemented as
    a drop down list. Originally, we has used static HTML code, but the
    problem will appear of implementing a “prefix”. Static HTML code
    does get handled by HTML helpers, so it’s recommended that you use
    the HTML.DropDownList().

    To force the MVC framework to use the
    new Display and Edit for templates, we need to annote our model item
    an add the following line:

    [UIHint("UserGuid")]

    This
    will cause MVC to use the Display and Edit templates named
    “UserGuid”, which are just partial views.

  • Henry Rodriguez

    Congratulations for that post have been usefull for me, i want to participate recomending this blog: 
    http://tony.webatures.com/2012/01/a-simple-validation-framework-with-knockoutjs/ where there an excelenet implementation of a validation framework over Ko

  • Erich

    Thank you for this great post, Is there any reason why your example wouldn’t work with the lastest knockout.js (2.1.0) release? It works great with the 1.2.1 release, but I can’t seem to get it to run with the lastest

    • Erich

      I didn’t see this before, but once I update the jquery reference to the lastest release, I was able to work with the lastest release of knockout as well. Thanks again.

      • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

        It seems to work here – except for the delete button… which doesn’t seem to with knockout 2.1.0 :S

        • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

          I had to change the removeMovie function to as follows:
          removeMovie: function(movie) { viewModel.FavouriteMovies.remove(movie); },

  • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

    Thanks for these posts. I’ve love to see an example of the final version with external script files, as I’m not sure of the best way to implement them

  • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

    I’m trying to implement this into my own project.
    I’ve ran into a problem that’s taken me a while to track down.
    The solution doesn’t seem to work with jquery.validate.unobtrusive bundled in.
    I’m fairly new to this so not entirely sure of the reasons as yet….

    • http://ivanz.com/ Ivan Zlatev

      Yeah it’s not pretty doing collection editing. In this case it’s because the way the unobtrusive validation works is when the page completed loading it does a one off parsing of the content and trickery to bridge the ASP.NET MVC validation attributes with the jquery.validation plugin. This is where your problem lies – when you insert new content it won’t be covered by the unobtrusive trickery, so you need to explicitly make it parse the new content by doing: $.validator.unobtrusive.parse($(“selector”)); after you’ve inserted it into the DOM

      • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

        I should have mentioned – for some reason including the microsoft jquery.validate.unobtrusive.js script actually seems to break the validate submit button. The validate function is simply never called and instead the default action of the submit button is called – bypassing the save function.

        • http://www.facebook.com/profile.php?id=577075530 Chris Nevill

          I believe I’ve resolved this now as follows:
          I replaced my submit button with a standard button:

          Save Cancel

          and then have the button click function check the validation before moving onto the save:
          $(“#btnSave”).click(function(e) { e.preventDefault(); if ($(“#profileEditorForm”).valid()) { if (viewModel.save()) { window.location.href = “/”; } } });

  • Garry

    This is awesome thanks very much to share such a great post..

  • Tomas

    Hi Ivan, great work. But, how can I nest another collection inside Movies using this “knockout” viewmodel approach? I’m trying for hours and still not successful. Can u help me, please?

  • Marina

    Thank you Ivan!

  • Girish Kolte

    excellent description and very proper explanation of code… loved it… Thanks…