Three D’s of Web Development #2: Data-Binding

The first part of this series explored the differences between declarative and imperative code. The declarative approach states what to do. The imperative approach describes how to do it.

If you understand the value of declarative markup, data-binding leverages the concept to provide the foundation for powerful design patterns. Data-binding is a major breakthrough for user interface design and experience because it is implemented with declarative code to facilitate the translation of an app’s internal data model to the user’s views of and inputs to that model.

Modern frameworks are often described as MV* frameworks. The M refers to the internal domain model used by the app. The V is for the view of the app that is presented to the end user. The asterisk is meant to be replaced by one of several design patterns that solve the problem of managing interactions between the model and the view. A brief overview of these patterns will help provide context for better understanding data-binding and its modern implementations.

Benefits of User Experience Patterns

Patterns are simply proven solutions to recurring problems. User experience patterns exist to solve common problems related to displaying information and obtaining user input. Several popular patterns evolved to address the top challenges that face developers and designers building business applications.

Fluidity of the User Interface

Most business applications have a fairly consistent domain of entities, workflows, processes, and business rules to follow. What often changes are the experiences of interfacing with and managing those elements.

Developers are frequently faced with the challenge of having to rewrite significant portions of a user interface due to design decisions, such as turning a form into a wizard, visualizing inputs using sliders and toggles instead of free form input fields, or creating graphs and charts to visualize data. A good pattern successfully decouples the implementation of the interface from the underlying model so that changes are isolated to the presentation layer and don’t require a major rewrite of business or even presentation logic.

Parallel Developer and Design (Developer/Designer Workflow)

Although some developers have the luxury (and ability) of being a “one stop” shop for applications, a very common approach is to tackle user experience with a dedicated UX team that works in collaboration with developers to deliver the final product.

Although designers may be able to write code, they typically deliver assets such as Adobe Photoshop Document files (PSDs) or HTML and CSS and leave the rest of the application to the developers. A good pattern will enable the UX team to work in parallel and minimize
disruption to the development team as designs are implemented or changed.

Decoupling of Presentation Logic

Rewriting presentation logic any time the user interface changes adds overhead and thrash to the development lifecycle. A good pattern allows the developer to build testable presentation logic that is decoupled from the UI implementation.

An example of this is the common pattern of presenting a list that allows the user to select an item. Presentation logic should be able to generate the list and track the selected item regardless of whether the UI is implemented using a drop-down, a bulleted list, or a third-party control.

View Logic Testing

In modern web applications, the first line of defense is the browser. Most users expect immediate feedback when they are interacting with their app, so validations, pick list selection, even light calculations are often performed on the client (JavaScript) layer.

For large applications, being able to test validation and view logic is critical to streamlining upgrades, updates, and bug fixes, especially when there is a large development team. A good pattern should facilitate testing without relying on the existence
of a live browser window.

View Ports and Sensors

Today’s consumers demand access to their apps anytime, anywhere, from any device. That device may be a small phone with a touch interface or a large monitor with an external keyboard and mouse. Input is no longer restricted to a key press or a mouse pointer but may include compass bearings and GPS coordinates.

A good pattern allows the developer to abstract these concerns from the main business logic. For example, menu logic should focus on generating the hierarchical list of items a user has access to and navigation, regardless of whether it is presented as a mega-menu bar or a mobile hamburger button.

MV/*

The notion of de-coupling view logic from application logic isn’t new. In fact, you can read a paper [PDF], published in 1979, that described the grandfather of UI design patterns – Model-View-Controller (MVC).

MVC

The key concept of MVC is that the controller provides a “link between the user and system.” The controller responds to inputs by manipulating the model. It also informs the view whenever the model changes, so that the view can re-render. This is a rough sketch of the pattern:

MVC Diagram

As you can see from this example the view, model, and controller are separate and therefore testable. You could just as easily inject a mock JavaScript object as the “view” to the controller as you could the active view that is listening to and updating the HTML DOM.

MVVM

The Model-View-ViewModel pattern was first described in the blog post “Introduction to Model/View/ViewModel pattern for building WPF apps” back in 2005. The pattern was design to take advantage of a key feature of the platform: data-binding.

The MVVM pattern replaces the controller with a “ViewModel.” This is a special model that exposes both properties and behaviors for synchronization to a view. Because it is completely decoupled from the view, the logic can be built and tested independently of the user interface.

MVVM Diagram

A very naive implementation of MVVM is below (and available here).

The model in this case is the same as the view model. Notice that the implementation of the view is very simple and straightforward. In a mature framework the data-binders are more comprehensive and span more elements and are often attached automatically by means of declarative markup.

As you can see in the example, data-binding uses an intermediary to synchronize information between a source and a target. In web applications, the source and target are JavaScript objects and the Document Object Model (DOM) while synchronization occurs between properties, events and functions. For example, in the div code, the data-binder understands that when the source changes, it should update the text on the div:

DivBinder.prototype.onChange = function() {
    this._elem.innerText = this.value;
};

Likewise, the data-binder for the input element knows to listen for changes from the source and update the value to the target.

function InputBinder(elem) {
    var that = this;
    this._listeners = [];
    this._elem = elem;
    elem.onkeyup = function() {
      that.value = elem.value;
    };
}

Case Study: Angular and the Demo Health App

The last two examples demonstrated how the underlying feature works but may make the pattern seem overcomplicated. Fortunately, many mature frameworks exist that provide a data-binding
implementation. A major reason why Angular is such a popular
framework is because of the data-binding it provides out of the box.

In the example Angular 2 Health App, you can see the implementation of a viewmodel in TypeScript.

Here is a snippet of the viewmodel that deals with age. Notice that although it always exposes whatever age property is set on viewmodel, it only updates the internal model if the age is in the valid range.

export class ProfileComponent {
    constructor(@Inject("UserProfile")private userProfile: IUserProfile) {
        this._ageValue = userProfile.ageYears;  
    }

    private _ageValue: number;

    public get ageValue(): number {
        return this._ageValue;
    }

    public set ageValue(val: number) {
        var incoming = Number(val);
        this._ageValue = val;
        if (incoming >= 13 && incoming <= 120) {
            this.userProfile.ageYears = incoming;
        }
    }
}

The important thing to recognize is that the viewmodel itself isn’t coupled to the view. The implementation of age could easily be a slider or an input box (or both) and it wouldn’t matter. The viewmodel is independent of the view and any behaviors can be tested (for example, you could try to set a value outside of the range and confirm the underlying model is not updated as a result of the validation failing).

The View declares the data-binding like this:

<div class="unit">
    <div class="label">Age (13 - 120):</div>
    <div class="labelTarget">
        <input id="ageFrm" 
            type="number" 
            required="" 
            min="13" 
            max="120" 
            [(ngModel)]="ageValue"
            #age="ngForm" 
            [class.error]="!age.valid"/>years
    </div>
</div>

The data-binding declaration is in the [(ngModel)]="ageValue" line. It sets up two-way data-binding by creating a setter that inspects the viewmodel and updates the input field, and a listener that waits for changes to the DOM element and communicates those to the viewmodel.

If the designer decided that this would be better implemented as a slider, no viewmodel code would have to change. Instead, the new element can be inserted in the view with a data-binding declaration like this:

<input type="range" 
    step="1" 
    min="13" 
    max="120" 
    [(ngModel)]="ageValue"/>

Notice the error class is no longer needed because the slider will prevent out of range values. It is easily added back in if other forms of input could result in an invalid age.

One other important caveat is that the data-binding made it easier to upgrade the app from Angular 1.x to the 2.0 version which is significantly different. This was possible because the markup language (HTML) did not change much between versions (in fact, although the data-binding syntax changed, it was similar enough to allow a simple search and replace to update most of the templates). The viewmodels also remained consistent in the properties they exposed, even if the way they are registered to
the Angular application changed.

Data-Binding Misconceptions

There are a number of misconceptions about data-binding that result in some developers shying away from the pattern.

  • Data-binding is complex.

    Actually, it can simplify the development and maintenance of applications when used correctly.

  • Data-binding forces all business logic into the browser.

    Data-binding doesn’t have an opinion about business logic. Most well-written apps take validation closer to the user for performance but double-check it on the server. Well-written APIs have validation built-in that can be leveraged by the client app.

  • Data-binding is tough to debug.

    This really depends on the framework. For example, Angular provides very detailed messages that are generated as a result of parsing templates to help troubleshoot data-binding markup.

  • Data-binding reduces the performance of the application.

    Manually authoring code that is optimized for the problem being solved will almost always result in the best performance. The real question is whether the extra effort is worth the tradeoff. In many cases, the difference in performance is minimal and not even detectable by the end users, so the advantages of a highly maintainable application outweigh any performance concerns. With modern browsers, updates to the JavaScript engines and improvements in popular frameworks, benchmarks show that data-binding is very viable for applications that have high performance demands.

  • Data-binding makes it difficult to understand the application.

    Again, this depends more on the framework and conventions than data-binding itself. For example, in Angular 2.0 it is very evident that parenthesis bind events to the underlying viewmodel and square braces bind properties, while double moustaches provide one-way bindings from the underlying viewmodel.

A Stateful Mindset for Data-Binding

After over ten years of working with MVVM and data-binding patterns, I’ve found the most common reason developers have a hard time adopting or understanding data-binding is because they try to approach viewmodels as procedural constructs rather than entities that synchronize state. An example that makes this easier to understand is when two portions of the application share the same data. This is illustrated in the Angular 2 Health App.

The app features two different views. One view is an input form used to gather information about the subject, and another view is a set of tiles that display computed values for basal metabolic rate (BMR), body mass index (BMI), and target heart rate (THR).

The procedural approach to building the app involves synchronizing the views through events. A listener waits for an input, such as age, to change, and raises an event that the model has changed. The output view listens for the change event and responds by re-computing the model and rendering the updates.

This approach involves setting up communication mechanisms that make it more difficult to test the app and can result in memory leaks when listeners aren’t detached when they are no longer needed. Data-binding doesn’t require this approach and, with a different mindset, eliminates the need for events altogether.

The app is implemented with the concept of an underlying model for the user’s profile. This model is shared by the viewmodel for obtaining information about the user, and the one for displaying the computed values. The earlier code snippet demonstrated how the viewmodel simply updates the internal model based on the state of the age changing.

Here is the viewmodel for the health tiles.

export class FormulaComponent {
    constructor(@Inject("UserProfile")private userProfile: IUserProfile) {}

    private profileToParam(): IFormulaParameter {
        return {
            isMale: this.userProfile.isMale,
            weight: this.userProfile.weightPounds,
            height: this.userProfile.heightInches,
            age: this.userProfile.ageYears 
        };
    }

    public get bmr(): number {
        return formulaBmr(this.profileToParam());
    }

    public get bmi(): number {
        return formulaBmi(this.profileToParam());
    }

    public get thr(): any {
        return formulaThr(this.userProfile.ageYears);
    }
}

Notice there is no event triggered and no listener. The properties simply expose the computed values. How does this work?

In the case of Angular, the data-binding management always refreshes the views when the model changes if two-way data-binding is specified. Therefore, whenever a change to the user profile view results in the
model getting “dirty” (meaning one or more values changed from the last time it was inspected), the other view is re-rendered automatically. The data-binders reach out to the viewmodels to check the updated properties, and the viewmodel in turn computes the right value based on the latest version of the model.

This viewmodel is very easy to test, because you can mock the model state and simply confirm the various properties generate expected results. The Angular 1.x version of the application features a full test suite and you inspect the controller specifications.

Here is an example test that arranges the internal model based on a given user profile, acts by inspecting the Basal Metabolic Rate (BMR) property on the controller, and asserts that it is the expected result.

describe("Given a 40-year old 5 ft 10 in male who weighs 200 pounds", function () {

    it("bmrValue should be 1929", function () {
        uomSvc.usMeasure = true;
        userProfileSvc.isMale = true;
        userProfileSvc.ageYears = 40;
        userProfileSvc.heightInches = 5*12 + 10;
        userProfileSvc.weightPounds = 200;
        expect(formulaController.bmrValue).toBeCloseTo(1929);
    });

});

The inputs, of course, are based on a hypothetical subject who may just write articles about data-binding.

Summary

Data-binding is one of the major components of modern web frameworks and provides numerous benefits, especially when it takes advantage of declarative markup. To recap, here are some key advantages data-binding provides:

  1. Makes it easier to maintain changing user interfaces;
  2. Facilitates the designer/developer workflow;
  3. Decouples of presentation logic from the view implementation;
  4. Supports testing;
  5. Enables multiple view ports to render from a common code base;
  6. Declaratively maps inputs and sensors (such as mouse taps or voice input) to behaviors on the viewmodel;
  7. And decouples implementation of lists, grids, selectors, and other patterns from the viewmodel.

In this article, I shared the patterns related to data-binding, discussed its advantages, and tackled some common misconceptions. In the next and last article in this series, I will explore the impact of dependency injection on modern web development and explain how important it is for building large apps with large teams.

Related Resources

Comments