Multiple Knockout ViewModels and Reusable Partial Views

As part of our redesign of our Anvil application to use MVC and Knockout, we needed to figure out how to get reusability with Knockout and partial views.

When constructing a complex form you ideally want to break the problem down into smaller, easier to manage components that are decoupled from the bigger problem. However, you still need to be able to get all these items to interact and work together when you build the complex object.

Binding by Element ID

Knockout has the option of specifying a second parameter in the ko.applyBindings() function that restricts the binding to a given document element (and its contained controls), such as a DIV. Typically examples of this show an element with an ID, and the line, e.g.:

ko.applyBindings(addressVM, document.getElementById("address"));

 

The problem with this approach is that introducing IDs in reusable controls makes them single-instance controls per page. We could add a dynamically generated ID, but then we’d have to link up the correct ID with the correct viewmodel. It gets very messy, very quickly.

There is another issue with this approach: Knockout bindings cannot be nested. If you bind an element to a knockout viewmodel, this element cannot contain another element inside with a different binding. Here is an example, where the address div is inside an order div. We can’t bind this way.

<div id="order">
    <!-- order control bindings -->
    <div id="address">
        <!-- address bindings -->
    </div>
</div>
<
script> var orderVM = new OrderViewModel(); var addressVM = new AddressViewModel(); // this won't work ko.applyBindings(orderVM, document.getElementById("order")); ko.applyBindings(addressVM, document.getElementById("address")); </script>

Alternative Approach and Sample App

Let’s say we want to create an order form. The form will have a Knockout viewmodel for the whole order, but parts of this will be built from reusable components.

We will have a delivery address in the order, but it will be a reusable AddressViewModel. For the actual orders, we will have an observableArray and each order will be an OrderItemViewModel.

Address View Model

First we examine our address. We want reusability, and addresses might be used in several places. So we create a Razor Partial View that lays out the controls and includes the Knockout data-binding attributes thus:

    <table>
        <tr>
            <td>Line 1:</td><td><input data-bind="value: line1" type="text" maxlength="32" /></td>
        </tr>
        <tr>
            <td>Line 2:</td><td><input data-bind="value: line2" type="text" maxlength="32" /></td>
        </tr>
        <tr>
            <td>Line 3:</td><td><input data-bind="value: line3" type="text" maxlength="32" /></td>
        </tr>
        <tr>
            <td>Town:</td><td><input data-bind="value: town" type="text" maxlength="32" /></td>
        </tr>
        <tr>
            <td>Postcode:</td><td><input data-bind="value: postcode" type="text" maxlength="8" /></td>
        </tr>
    </table>

This is a simple control example with no validation or other clever stuff. It just has some controls with data-bind statements.

Note that this has no containing DIV (or other element) with an ID. Also note that there is no mention of scripts, either inline or external. We’ve isolated just the UI element of our control.

By doing this we have made this control reusable: we can put several Address views on a page. We have delegated the script and binding responsibilities to the parent container in which we sit.

With Usage

So the next issue is how to get around the nested viewmodel problem. We have an order view model, which has an address inside it. The solution is the data-binding with: statement, detailed in Ryan Niemeyer’s excellent blog article .

The with statement changes the databinding context from the viewmodel to a part of the view model. So I define our OrderViewModel as follows:

        var OrderViewModel = (function () {
            function OrderViewModel() {
                this.OrderID = ko.observable(0);
                this.CustomerName = ko.observable("");
                this.DeliveryAddress = ko.observable(new Test.AddressViewModel());
                this.InvoiceAddress = ko.observable(new Test.AddressViewModel());
            }
            return OrderViewModel;
        })();

Note that in this example, I’ve created two address entries: DeliveryAddress and InvoiceAddress. Now we can put these in our main view and use the AddressView partial view we created earlier:

<div>
    <!-- this is the order context -->
    <input type="text" data-bind="value: CustomerName" />

    <!-- change context -->
    <div data-bind="with: DeliveryAddress">
        <b>Delivery Address</b>
        @Html.Partial("_AddressView")
    </div>
    <div data-bind="with: InvoiceAddress">
        <b>Delivery Address</b>
        @Html.Partial("_AddressView")
    </div>

</div>

Interaction

The AddressViewModel is independent of the OrderViewModel, but we could add some checks to the order, e.g. checking to see if the address is valid.

We can also cross-bind the two models. Let’s say our order has a DeliveryCharge which is a computed observable, which is based on the country we are delivering to. We want to recalculate this when the country changes. To do this we could subscribe to changes on the Country observable in the Delivery, and then update the DeliveryCharge based on this value.