UI Code Expert

Essential Knockout.js, Part 2: Best Practices and More

The first installment discussed what Knockout.js is, why and how it evolved, and how it fits into Web development. This month, the discussion dives into best practices for Knockout, extending it and creating custom bindings.

For Part 1, click here.

To get the most utility out of Knockout's components, the need for the Model-View-ViewModel (MVVM) design pattern must be understood. This architectural pattern arose out of Microsoft's implementation of Martin Fowler's presentation model design pattern to support Windows Presentation Foundation (WPF) and Silverlight.

Consider the first Windows Forms app you ever wrote. There was probably all kinds of code in click and form load event handlers. While the app solved a problem and worked, it was extremely difficult to unit test and refactor out common code. Additionally, problems likely arose with blocking the UI thread, so the code was littered with calls to DoEvents. And, if you wanted to change the font from serif to non-serif, you'd likely have a major chore on your hands surfing through many property pages or creatively using find/replace through all the .designer files in the project. Either way, the tight coupling between your code and UI properties would make it extremely difficult to complete such a seemingly easy task. Collaboration with other developers on the effort only exacerbated the issues -- how would you keep track of who fixed what, and avoid colliding in the same project files?

The MVVM pattern came from a desire to alleviate these problems. By taking properties and layout of visual controls out of compiled code and into an XML-based markup language, the view became an isolated concept that could be worked on effectively by itself. Now the details of the view can be given to a designer or artist that has experience in how things look and act, and they can change them without having to understand the entire stack of functionality. By adding data-binding syntax to the view markup, a programmer can even instrument prototype or wireframe views easily and quickly. There's tremendous value in being able to see an application and make judgments on it before it's fully developed.

In the world of HTML and Knockout, the language of the View is of course HTML. Because HTML has been around as long as the Web, almost everyone understands how to write it. This ubiquity means that it's trivial to turn a back-of-the-napkin idea into some simple HTML-only pages that provide a preview of what an app will look like and how it might function. Applying the MVVM pattern to this design means you can take these simple HTML-only pages and incrementally wire them up to actions and data sources.

Consider again the fledgling Windows Forms app mentioned earlier. If there was external data, there probably wasn't a cohesive view of it. Most likely, arrays of data rows were indexed into with integers or magic strings, and these were piped individually into properties of text boxes. It may have been difficult to determine where a database row ended and a UI control began. If you wished to validate user input, this meant writing some high cyclomatic complexity code that was doubly fragile because it depended heavily on both the shape of the data and its representation in the UI.

In MVVM, the model portion of the pattern can refer to either a domain model or an abstraction in a data-access layer. The domain model definition works well if your domain models already follow an object-oriented approach. An example of this would be WCF Data Services providing OData JSON objects that came from an Entity Framework data model of a SQL database. Even though a Person object might transition through several languages, frameworks and states, it will always have a FirstName property with a type of String. Alternatively, the data-access layer abstraction pattern represents a more data-centric architecture. Business logic may be pushed down into the database itself. An example of this architecture would be the Facebook Open Graph API. In this case, model objects not only represent units of underlying data, but also contain methods that form a data-access layer for interacting with that data.

The view model exposes properties that are Observable instances of model data, and command functions that can affect change on that data or configure the UI. The view model is the wiring between the UI elements (both display and actions) and the actual backing data and its storage mechanism. As the view model is only lightly coupled with the model objects and the view, its properties and methods remain unit-testable by injecting fake models and views.

Knockout is the glue that joins each of the MVVM pieces. It essentially provides a framework for the binding between each layer in MVVM. The MVVM/Knockout combination lets you build functionally complex Web sites quickly and more easily, all the while keeping the code testable and Don't Repeat Yourself (DRY) conformant. Furthermore, the Knockout framework -- and the MVVM pattern in general -- helps you avoid some of the pitfalls of asynchronous programming and event-driven UIs. Due to the asynchronous nature of JavaScript, you can easily make long-running calls to back-end services without causing the UI to lock up. When these calls return, the magic of data binding allows that data to trickle into the UI without disorienting the user with page refreshes or losing scrollbar position. In short, Knockout makes it easy for you to "fall into the pit of success."

Utilities
In addition to the core concepts of Observables and data binding, Knockout also provides a slew of utility methods for dealing with common scenarios in HTML/JavaScript programming. Because Knockout doesn't have any other framework dependencies (on libraries such as jQuery or Underscore), it can act as a complete end-to-end solution for programmers to go from prototype to mature application.

JavaScript is a wonderfully lightweight and dynamic language, but sometimes this agility means you must write boilerplate code for dealing with common situations that the language itself does not provide syntax for directly. Consider working with IEnumerables in the Microsoft .NET Framework Base Class Library (BCL) -- with approximately 60 unique methods to call. That's a richly filled toolbox for dealing with collections of things. In JavaScript, the native collection type, Array, has only 23 methods, and their behavior might differ between browsers. Knockout provides several convenience methods to make dealing with Array (and ObservableArray) values a lot easier, as shown in Table 1.

Utility Method What It Does
ko.utils.arrayForEach Iterates over an array and calls a function on each item
ko.utils.arrayFirst Searches an array for the first item where an evaluator function returns true
ko.utils.arrayFilter Returns a subset of the original array with only items that match a certain criteria
ko.utils.arrayGetDistinctValues Returns an array of just the unique values
ko.utils.arrayIndexOf Returns the index of the first array item found that matches criteria
ko.utils.arrayMap Maps certain properties of objects in an array into a new array
ko.utils.arrayPushAll Add multiple items to the end of an array
ko.utils.arrayRemoveItem Removes a single item from an array

Table 1. Knockout convenience methods for dealing with Array and ObservableArray values.

In addition to dealing with arrays, Knockout also has utility functions for converting objects to and from JSON. The ko.utils.toJS method can take your model object with all its Observable properties and return a regular JavaScript object with "normal" properties. In turn, ko.utils.toJSON will first call toJS on an object and then return a serialized JavaScript string (JSON) representation of the object, ready to be sent on the wire to a Web service. The toJSON will use the browser's default JSON serializer, so older browsers will need a reference to Douglas Crockford's json2.js library to function.

Knockout can also optionally use the ko.mapping plug-in. This plug-in takes a JavaScript or JSON representation of a model object (like one you'd get calling a RESTful service) and returns a JavaScript object with all of the properties wrapped in the Observable behavior. This greatly reduces the amount of work needed to get data off the wire and into your views, as shown in Listing 1.

Listing 1. The ko.mapping plug-in.
<form>
  <button data-bind="click: loadBigshot">Load the media big shot's data</button>
  <button data-bind="click: loadAssistant">Load the put-upon assistant's data</button>
  <div data-bind="with: person">
    <label for="name">Name:</label><input id="name" type="text" data-bind="value: name" /><br />
    <label for="cash">Cash:</label><input id="cash" type="text" data-bind="value: cash" /><br />
    <label for="phone">Phone:</label><input id="phone" type="tel" data-bind="value: phone" />
  </div>
</form>
<script>
  var bigshotJSON = '{ "name": "Diamond Joe Quimby", "cash": 10000, "phone": "5558675309" }',
    assistantJSON = '{ "name": "Gil Gunderson", "cash": "two bits", "phone": "8005551212" }',
    viewModel = {
      person: ko.observable(),
      loadBigshot: function () {
        this.person(ko.mapping.fromJSON(bigshotJSON));
      },
      loadAssistant: function () {
        this.person(ko.mapping.fromJSON(assistantJSON));
      }
    };
 
  ko.applyBindings(viewModel);
</script>

This example leverages the "with" binding to establish the binding context for the form fields that are bound to the "person" Observable. The ko.mapping plug-in is highly configurable. If you look at the Listing 1 code in the F12 developer tools of your favorite browser, you'll see that the "person" property also has a "__ko_mapping__" property that contains metadata about how the plug-in was configured and the results of the mapping operation.

The ko.utils module also contains functions for unwrapping an Observable property and returning it to its plain-old JavaScript state. This is especially useful when extending Knockout with extenders or custom bindings, as described later. You may have noticed that the data binding syntax doesn't seem to need to call Observables like functions to get at their values. This is because most data bindings call ko.utils.unwrapObservable on the parsed binding value passed to them.

The extensions in ko.utils aren't limited to just dealing with the inner workings of Knockout. There are memoization functions to cache values that are expensive to compute and don't change very often. There's also a host of DOM manipulation functions, functions for firing and subscribing to events in code, simple string manipulation functions, and even functions for getting the values from a form field and posting it as JSON to a Web service.

Best Practices
Knockout is a turnkey framework for creating agile Web UIs, so I'll review some best practices to ensure success with Knockout in the future.

Don't force logic in binding markup code. Because Knockout evaluates any binding as code, it's easy to add a little bit of inline JavaScript to make a binding work how you want. Be careful when doing this, however, that you don't inject untestable behavior into the application. While valid, binding markup like this is not recommended:

<span data-bind="text: 'Hello: ' + firstName + ' ' + lastName></span>

It would be much better to make a Computed Observable whose logic could be proved in a unit test. Coding this way also has the added effect of making maintenance more difficult. A maintainer coming behind you to make this binding support multiple languages might not find this code right away, and when he does, he'll have to rip it out anyway.

Use a local variable to keep track of "this." Because of JavaScript lexical scoping, the "this" keyword can be very useful, but it can also stab you in the back. If you look back to the example for Computed Observables, the value of "this" was saved into a local variable "self." The reason for this is that the "this" reference, when the function inside the Computed Observable gets called, will not necessarily refer to the current instance of the view model. In general, when creating view models as functions (which is a good idea to take advantage of constructor behavior), always store the "this" reference in a local variable that the instance will close over:

function ViewModel() {
  var self = this;
  // Other properties and logic
}

Don't forget to call ko.applyBindings. Because the actual data bindings can be hard to debug (how do you put a breakpoint in the HTML?), always remember to call ko.applyBindings({your viewmodel}) when the DOM is fully loaded. Remember that you don't have to use the jQuery document ready event for this; it can be done in a script block at the bottom of the page.

ObservableArray contents don't automatically have Observable properties. Unless you're using the ko.mapping plug-in, making a property of your view model an ObservableArray doesn't mean objects you put in that array will have Observable properties. Remember, an ObservableArray just observes changes in the array itself, like additions, removals or order changes. It isn't responsible for also watching the properties of objects in the array.

Hopefully following these best practices can become second nature and will make your experience with Knockout all the more fruitful.

Extending Knockout
There are a lot of useful and generalized tools in the Knockout toolbox. But often you'll need to extend default behaviors, or add your own to reduce code repetition. To this end, Knockout offers three main extension points:

Extending with fn Knockout is broken out into four main objects, or modules:

  • ko.subscribable
  • ko.observable
  • ko.observableArray
  • ko.computed

Each one of them can have new functionality tacked on via the "fn" object. For example, to add a function to ko.observableArray, you'd add it like this:

ko.observableArray.fn.customFunction = function(args) {
  // Return an observableArray
};

This is a great way to consolidate extra functionality in an application. However, don't get carried away adding a bunch of functions that only ever get called once to the global Knockout modules. As always, the You Ain't Gonna Need It (YAGNI) principle applies here.

Adding Functionality to Observables with Extenders Through the concept of extenders, you can attach functionality and new properties to existing Observables. Once defined, extenders can easily be mixed into your view model Observable properties. Possible examples of useful extender functionality include a dirty bit to allow change tracking within a model, logging change events for debugging, and storing human-readable data alongside typed data (like dates in ISO-8601 format). Listing 2 shows an example of an extender that converts an Observable value to uppercase and adds a suffix, but doesn't affect the underlying value.

Listing 2. Converting an Observable value to uppercase and adding a suffix.
Greeting:
<input data-bind="value: message" />
<h1 data-bind="text: message.shouty"></h1>
<script>
  ko.extenders.shoutyCase = function (target, suffix) {
    target.shouty = ko.computed(function () {
      if (target()) {
        return target().toUpperCase() + suffix;
      }
      return '';
    });
    return target;
  };
 
  var viewModel = {
    message: ko.observable().extend({ shoutyCase: '!!!' })
  };
 
  ko.applyBindings(viewModel);
</script>

Notice how the value in the text box doesn't change, and a new "shouty" Observable is added to the "message" Observable.

One extender already comes in the box with Knockout. This is the "throttle" extender. It's useful for aggregating events that fire quickly into a more manageable stream. Consider a type-ahead search box. If a user is a quick typist and the code queries a back-end service with every key press, there's a risk that the server becomes saturated with requests. By adding the "throttle" extender to the Observable bound to the search box, the server-calling code can be made to wait for a certain number of milliseconds even if subsequent events fire.

Creating Custom Bindings Finally, one of the most useful extension points in the Knockout framework is creating custom bindings. This is where you can easily wire Knockout into existing UI controls such as Kendo UI grids or jQuery datepickers. Additionally, new bindings can be created that incorporate procedural UI changes like fading in and out, or other visual transitions. Listing 3 shows an example of using a custom binding to wire up an Observable value to jQuery UI animation effects.

Listing 3. A custom binding to wire up an Observable value to jQuery UI animation effects.
<h2>List of greenlit shows:</h2>
<ul data-bind="foreach: shows">
  <li>
    <input type="checkbox" data-bind="checked: greenlit" />
      <span data-bind="    text: name, explode: greenlit"></span></li>
</ul>
<script>
  ko.bindingHandlers.explode = {
    update: function (element, valueAccessor) {
      ko.utils.unwrapObservable(valueAccessor()) ? $(element).fadeIn() : 
        $(element).effect('explode');
    }
  };
 
  var ShowModel = function (name) {
    var self = this;
    self.greenlit = ko.observable(true);
    self.name = ko.observable(name);
  }, ViewModel = function (shows) {
    var self = this;
    self.shows = ko.observableArray(shows);
  };
 
  var vm = new ViewModel([new ShowModel("Reality show 1"),
    new ShowModel("Angry cooking show"),
    new ShowModel("Reality show 2")]);
  ko.applyBindings(vm);
</script>

Because you're using the unwrapObservable utility function, this custom binder would also work with non-Observable properties. And because custom bindings are added to a global "ko.bindingHandlers" module, it's easy to move custom binding functionality in your app out into a common file to reduce code repetition.

Arguably, custom bindings give the most bang for the buck when extending Knockout. Several libraries exist with custom bindings for specific jobs, as shown in Table 2.

Library Function
Knockout-UI Bind to jQuery UI widgets
KoGrid A data grid inspired by SlickGrid
Knockout-Validation Extenders and bindings for model and property validation

Table 2. Libraries with custom bindings for specific jobs.

As JavaScript heads down the path to becoming the "assembly language of the Web," the need exists for concise and efficient tools to increase productivity. Reactive and beautiful UIs in browser-based applications are ceasing to be the exception and becoming the norm. Knockout provides an excellent framework for you to create HTML/JavaScript applications that work well on a variety of devices, progress quickly from prototype to released software, are easy to maintain and grow, and take advantage of close-to-the-user processing to create an agile and reactive experience. No greater endorsement exists for Knockout than the fact that it ships in the default project template in ASP.NET MVC 4.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube