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:

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:

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:

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">
            
                <li data-bind="sortableItem: movie">
                    <div data-bind="template: { name: 'movieTemplate', data: movie }"></div>
                </li>
            
        </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:

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 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!

[top]

comments powered byDisqus