Understanding MVVM – A Guide For JavaScript Developers

Read Later 

MVVM (Model View ViewModel) is an architectural pattern based on MVC and MVP, which attempts to more clearly separate the development of user-interfaces (UI) from that of the business logic and behaviour in an application. To this end, many implementations of this pattern make use of declarative data bindings to allow a separation of work on Views from other layers.

This facilitates UI and development work occurring almost simultaneously within the same codebase. UI developers write bindings to the ViewModel within their document markup (HTML), where the Model and ViewModel are maintained by developers working on the logic for the application.

History

MVVM was originally defined by Microsoft for use with Windows Presentation Foundation (WPF) and Silverlight, having been officially announced in 2005 by John Grossman in a blog post about Avalon (the codename for WPF). It also found some popularity in the Adobe Flex community as an alternative to simply using MVC.

In recent years, MVVM has been implemented in JavaScript in the form of structural frameworks such as KnockoutJSKendo MVVM and Knockback.js, with an overall positive response from the community.

Let's now review the three components that compose MVVM.

Model

As with other members of the MV* family, the Model in MVVM represents domain-specific data or information that our application will be working with. A typical example of domain-specific data might be a user account (e.g name, avatar, e-mail) or a music track (e.g title, year, album).

Models hold information, but typically don't handle behaviour. They don't format information or influence how data appears in the browser as this isn't their responsibility. Instead, formatting of data is handled by the View, whilst behaviour is considered business logic that should be encapsulated in another layer that interacts with the Model – the ViewModel.

The only exception to this rule tends to be validation and it's considered acceptable for Models to validate data being used to define or update existing models (e.g does an e-mail address being input meet the requirements of a particular Regular expression?).

In KnockoutJS, Models fall under the above definition, but often make Ajax calls to a server-side service to both read and write Model data.

If we were constructing a simple Todo application, a KnockoutJS Model representing a single Todo item could look as follows:

view plaincopy to clipboardprint?

  1. var Todo = function (content, done) {  
  2.     this.content = ko.observable(content);  
  3.     this.done = ko.observable(done);  
  4.     this.editing = ko.observable(false);  
  5. };  

Note: You may notice in the above snippet that we are calling a method observables()on the KnockoutJS namespace ko. In KnockoutJS, observables are special JavaScript objects that can notify subscribers about changes and automatically detect dependencies. This allows us to syncronize Models and ViewModels when the value of a Model attribute is modified.

View

As with MVC, the View is the only part of the application of users actually interact with. They are an interactive UI that represent the state of a ViewModel. In this sense, MVVM View is considered active rather than passive, but what does this mean?.

A passive View has no real knowledge of the models in our application and is manipulated by a controller. MVVM's active View contains the data-bindings, events and behaviours which require an understanding of the Model and ViewModel. Although these behaviours can be mapped to properties, the View is still responsible for handling events to the ViewModel.

It's important to remember the View isn't responsible here for handling state – it keeps this in sync with the ViewModel.

A KnockoutJS View is simply a HTML document with declarative bindings to link it to the ViewModel. KnockoutJS Views display information from the ViewModel, pass commands to it (e.g a user clicking on an element) and update as the state of the ViewModel changes. Templates generating markup using data from the ViewModel can however also be used for this purpose.

To give a brief initial example, we can look to the JavaScript MVVM framework KnockoutJS for how it allows the definition of a ViewModel and it's related bindings in markup:

ViewModel:

view plaincopy to clipboardprint?

  1. var aViewModel = {  
  2.     contactName: ko.observable('John');  
  3. };  

View:

view plaincopy to clipboardprint?

  1. <input id="source" data-bind="value: contactName, valueUpdate: 'keyup'" /></p>  
  2. <div data-bind="visible: contactName().length > 10">  
  3.     You have a really long name!  
  4. </div>  

Our input text-box (source) obtains it's initial value from contactName, automatically updating this value whenever contactName changes. As the data binding is two-way, typing into the text-box will update contactName accordingly so the values are always in sync.

Although implementation specific to KnockoutJS, the <div> containing the 'You have a really long name! text also contains simple validation (once again in the form of data bindings). If the input exceeds 10 characters, it will display, otherwise it will remain hidden.

Moving on to a more advanced example, we can return to our Todo application. A trimmed down KnockoutJS View for this, including all the necessary data-bindings may look as follows.

view plaincopy to clipboardprint?

  1. <div id="todoapp">  
  2.     <header>  
  3.         <h1>Todos</h1>  
  4.         <input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add"  
  5.                placeholder="What needs to be done?"/>  
  6.     </header>  
  7.     <section id="main" data-bind="block: todos().length">  
  8.         <input id="toggle-all" type="checkbox" data-bind="checked: allCompleted">  
  9.         <label for="toggle-all">Mark all as complete</label>  
  10.         <ul id="todo-list" data-bind="foreach: todos">  
  11.            <!-- item -->  
  12.             <li data-bind="css: { done: done, editing: editing }">  
  13.                 <div class="view" data-bind="event: { dblclick: $root.editItem }">  
  14.                     <input class="toggle" type="checkbox" data-bind="checked: done">  
  15.                     <label data-bind="text: content"></label>  
  16.                     <a class="destroy" href="#" data-bind="click: $root.remove"></a>  
  17.                 </div>  
  18.                 <input class="edit" type="text"  
  19.                        data-bind="value: content, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }"/>  
  20.             </li>  
  21.         </ul>  
  22.     </section>  
  23. </div>  

Note that the basic layout of the mark-up is relatively straight-forward, containing an input textbox (new-todo) for adding new items, togglers for marking items as complete and a list (todo-list) with a template for a Todo item in the form of an li.

The data bindings in the above markup can be broken down as follows:

   

  • The input textbox new-todo has a data-binding for the current property, which is where the value of the current item being added is stored. Our ViewModel (shown shortly) observes the current property and also has a binding against the addevent. When the enter key is pressed, the add event is triggered and our ViewModel can then trim the value of current and add it to the Todo list as needed
  • The input checkbox toggle-all can mark all of the current items as completed if clicked. If checked, it triggers the allCompleted event, which can be seen in our ViewModel
  • The item li has the class done. When a task is marked as done, the CSS classediting is marked accordingly. If double-clicking on the item, the $root.editItemcallback will be executed
  • The checkbox with the class toggle shows the state of the done property
  • A label contains the text value of the Todo item (content)
  • There is also a remove button that will call the $root.remove callback when clicked.
  • An input textbox used for editing mode also holds the value of the Todo itemcontent. The enterKey event will set the editing property to true or false

       

    ViewModel

    The ViewModel can be considered a specialized Controller that acts as a data converter. It changes Model information into View information, passing commands from the View to the Model.

    For example, let us imagine that we have a model containing a date attribute in unix format (e.g 1333832407). Rather than our models being aware of a user's view of the date (e.g 04/07/2012 @ 5:00pm), where it would be necessary to convert the address to it's display format, our model simply holds the raw format of the data. Our View contains the formatted date and our ViewModel acts as a middle-man between the two.

    In this sense, the ViewModel might be looked upon as more of a Model than a View but it does handle most of the View's display logic.The ViewModel may also expose methods for helping to maintain the View's state, update the model based on the action's on a View and trigger events on the View.

    In summary, the ViewModel sits behind our UI layer. It exposes data needed by a View (from a Model) and can be viewed as the source our Views go to for both data and actions.

    KnockoutJS interprets the ViewModel as the represtation of data and operations that can be performed on a UI. This isn't the UI itself nor the data model that persists, but rather a layer that can also hold the yet to be saved data a user is working with. Knockout's ViewModels are implemented JavaScript objects with no knowledge of HTML markup. This abstract approach to their implementation allows them to stay simple, meaning more complex behaviour can be more easily managed on-top as needed.

    A partial KnockoutJS ViewModel for our Todo application could thus look as follows:

    view plaincopy to clipboardprint?

  1. // our main ViewModel  
  2.     var ViewModel = function (todos) {  
  3.         var self = this;  
  4.     // map array of passed in todos to an observableArray of Todo objects  
  5.     self.todos = ko.observableArray(ko.utils.arrayMap(todos, function (todo) {  
  6.         return new Todo(todo.content, todo.done);  
  7.     }));  
  8.     // store the new todo value being entered  
  9.     self.current = ko.observable();  
  10.     // add a new todo, when enter key is pressed  
  11.     self.add = function (data, event) {  
  12.         var newTodo, current = self.current().trim();  
  13.         if (current) {  
  14.             newTodo = new Todo(current);  
  15.             self.todos.push(newTodo);  
  16.             self.current("");  
  17.         }  
  18.     };  
  19.     // remove a single todo  
  20.     self.remove = function (todo) {  
  21.         self.todos.remove(todo);  
  22.     };  
  23.     // remove all completed todos  
  24.     self.removeCompleted = function () {  
  25.         self.todos.remove(function (todo) {  
  26.             return todo.done();  
  27.         });  
  28.     };  
  29.     // writeable computed observable to handle marking all complete/incomplete  
  30.     self.allCompleted = ko.computed({  
  31.         //always return true/false based on the done flag of all todos  
  32.         read:function () {  
  33.             return !self.remainingCount();  
  34.         },  
  35.         //set all todos to the written value (true/false)  
  36.         write:function (newValue) {  
  37.             ko.utils.arrayForEach(self.todos(), function (todo) {  
  38.                 //set even if value is the same, as subscribers are not notified in that case  
  39.                 todo.done(newValue);  
  40.             });  
  41.         }  
  42.     });  
  43.     // edit an item  
  44.     self.editItem = function(item) {  
  45.         item.editing(true);  
  46.     };  
  47.  ..  

Above we are basically providing the methods needed to add, edit or remove items as well as the logic to mark all remaining items as having been completed Note: The only real difference worth noting from previous examples in our ViewModel are observable arrays. In KnockoutJS, if we wish to detect and respond to changes on a single object, we would use observables. If however we wish to detect and respond to changes of a collection of things, we can use an observableArray instead. A simpler example of how to use observables arrays may look as follows:

view plaincopy to clipboardprint?

  1. // Define an initially an empty array  
  2. var myObservableArray = ko.observableArray();  
  3. // Add a value to the array and notify our observers  
  4. myObservableArray.push('A new todo item');  

Note: The complete Knockout.js Todo application we reviewed above can be grabbed from TodoMVC if interested.

Recap: The View and the ViewModel

Views and ViewModels communicate using data-bindings and events. As we saw in our initial ViewModel example, the ViewModel doesn't just expose Model attributes but also access to other methods and features such as validation.

Our Views handle their own user-interface events, mapping them to the ViewModel as necessary. Models and attributes on the ViewModel are syncronized and updated via two-way data-binding.

Triggers (data-triggers) also allow us to further react to changes in the state of our Model attributes.

Recap: The ViewModel and the Model

Whilst it may appear the ViewModel is completely responsible for the Model in MVVM, there are some subtleties with this relationship worth noting. The ViewModel can expose a Model or Model attributes for the purposes of data-binding and can also contain interfaces for fetching and manipulating properties exposed in the view.

Pros and Cons

You now hopefully have a better appreciation for what MVVM is and how it works. Let's now review the advantages and disadvantages of employing the pattern:

Advantages

  1. <input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?"/>  

When the provider locates an element with this attribute, it parses it and turns it into a binding object using the current data context. This is the way KnockoutJS has always worked, allowing you to declaratively add bindings to elements which KnockoutJS binds to the data at that layer.

Once you start building Views that are no longer trivial, you may end up with a large number of elements and attributes whose bindings in markup can become difficult to manage. With custom binding providers however, this is no longer a problem.

A binding provider is primarily interested in two things:

  • When given a DOM node, does it contain any data-bindings?
  • If the node passed this first question, what does the binding object look like in the current data context?.

    Binding providers implement two functions:

  • nodeHasBindings: this takes in a DOM node which doesn't necessarily have to be an element
  • getBindings: returns an object representing the bindings as applied to the current data context

    A skeleton binding provider might thus look as follows:

    view plaincopy to clipboardprint?

  1. var ourBindingProvider = {  
  2.     nodeHasBindings: function(node) {  
  3.         // returns true/false  
  4.     },  
  5.     getBindings: function(node, bindingContext) {  
  6.         // returns a binding object  
  7.     }  
  8. };  

Before we get to fleshing out this provider, let's briefly discuss logic in data-bind attributes.

If when using Knockout's MVVM you find yourself dissatisfied with the idea of application logic being overly tied into your View, you can change this. We could implement something a little like CSS classes to assign bindings by name to elements. Ryan Niemeyer (of knockmeout.net) has previously suggested using data-class for this to avoid confusing presentation classes with data classes, so let's get our nodeHasBindings function supporting this:

view plaincopy to clipboardprint?

  1. // does an element have any bindings?  
  2. function nodeHasBindings(node) {  
  3.     return node.getAttribute ? node.getAttribute("data-class") : false;  
  4. };  

Next, we need a sensible getBindings() function. As we're sticking with the idea of CSS classes, why not also consider supporting space-separated classes to allow us to share binding specs between different elements?.

Let's first review what our bindings will look like. We create an object to hold them where our property names need to match the keys we wish to use in our data-classes.

Note: There isn't a great deal of work required to convert a KnockoutJS application from using traditional data-bindings over to unobstrusive bindings with custom binding providers. We simply pull our all of our data-bind attributes, replace them with data-class attributes and place our bindings in a binding object as per below:

view plaincopy to clipboardprint?

  1. var viewModel = new ViewModel(todos || []);  
  2. var bindings = {  
  3.         newTodo:  {  
  4.             value: viewModel.current,  
  5.             valueUpdate: 'afterkeydown',  
  6.             enterKey: viewModel.add  
  7.         },  
  8.         taskTooltip :  { visible: viewModel.showTooltip },  
  9.         checkAllContainer :  {visible: viewModel.todos().length },  
  10.         checkAll: {checked: viewModel.allCompleted },  
  11.         todos: {foreach: viewModel.todos },  
  12.         todoListItem: function() { return { css: { editing: this.editing } }; },  
  13.         todoListItemWrapper: function() { return { css: { done: this.done } }; },  
  14.         todoCheckBox: function() {return { checked: this.done }; },  
  15.         todoContent: function() { return { text: this.content, event: { dblclick: this.edit } };},  
  16.         todoDestroy: function() {return { click: viewModel.remove };},  
  17.         todoEdit: function() { return {  
  18.             value: this.content,  
  19.             valueUpdate: 'afterkeydown',  
  20.             enterKey: this.stopEditing,  
  21.             event: { blur: this.stopEditing } }; },  
  22.         todoCount: {visible: viewModel.remainingCount},  
  23.         remainingCount: { text: viewModel.remainingCount },  
  24.         remainingCountWord: function() { return { text: viewModel.getLabel(viewModel.remainingCount) };},  
  25.         todoClear: {visible: viewModel.completedCount},  
  26.         todoClearAll: {click: viewModel.removeCompleted},  
  27.         completedCount: { text: viewModel.completedCount },  
  28.         completedCountWord: function() { return { text: viewModel.getLabel(viewModel.completedCount) }; },  
  29.         todoInstructions: {visible: viewModel.todos().length}  
  30.     };  
  31.     ....  

There are however two lines missing from the above snippet – we still need ourgetBindings function, which will loop through each of the keys in our data-class attributes and build up the resulting object from each of them. If we detect that the binding object is a function, we call it with our current data using the context this. Our complete custom binding provider would look as follows:

view plaincopy to clipboardprint?

  1.     // We can now create a bindingProvider that uses  
  2.     // something different than data-bind attributes  
  3.     ko.customBindingProvider = function(bindingObject) {  
  4.         this.bindingObject = bindingObject;  
  5.         //determine if an element has any bindings  
  6.         this.nodeHasBindings = function(node) {  
  7.             return node.getAttribute ? node.getAttribute("data-class") : false;  
  8.         };  
  9.       };  
  10.     // return the bindings given a node and the bindingContext  
  11.     this.getBindings = function(node, bindingContext) {  
  12.         var result = {};  
  13.         var classes = node.getAttribute("data-class");  
  14.         if (classes) {  
  15.             classes = classes.split(' ');  
  16.             //evaluate each class, build a single object to return  
  17.             for (var i = 0, j = classes.length; i < j; i++) {  
  18.                var bindingAccessor = this.bindingObject[classes[i]];  
  19.                if (bindingAccessor) {  
  20.                    var binding = typeof bindingAccessor == "function" ? bindingAccessor.call(bindingContext.$data) : bindingAccessor;  
  21.                    ko.utils.extend(result, binding);  
  22.                }  
  23.             }  
  24.         }  
  25.         return result;  
  26.     };  
  27. };  

Thus, the final few lines of our bindings object can be defined as follows:

view plaincopy to clipboardprint?

  1.     // set ko's current bindingProvider equal to our new binding provider  
  2.     ko.bindingProvider.instance = new ko.customBindingProvider(bindings);  
  3.     // bind a new instance of our ViewModel to the page  
  4.     ko.applyBindings(viewModel);  
  5. })();  

What we're doing here is effectively defining constructor for our binding handler which accepts an object (bindings) which we use to lookup our bindings. We could then re-write the markup for our application View using data-classes as follows:

view plaincopy to clipboardprint?

  1. <div id="create-todo">  
  2.                 <input id="new-todo" data-class="newTodo" placeholder="What needs to be done?" />  
  3.                 <span class="ui-tooltip-top" data-class="taskTooltip" style="display: none;">Press Enter to save this task</span>  
  4.             </div>  
  5.             <div id="todos">  
  6.                 <div data-class="checkAllContainer" >  
  7.                     <input id="check-all" class="check" type="checkbox" data-class="checkAll" />  
  8.                     <label for="check-all">Mark all as complete</label>  
  9.                 </div>  
  10.                 <ul id="todo-list" data-class="todos" >  
  11.                     <li data-class="todoListItem" >  
  12.                         <div class="todo" data-class="todoListItemWrapper" >  
  13.                             <div class="display">  
  14.                                 <input class="check" type="checkbox" data-class="todoCheckBox" />  
  15.                                 <div class="todo-content" data-class="todoContent" style="cursor: pointer;"></div>  
  16.                                 <span class="todo-destroy" data-class="todoDestroy"></span>  
  17.                             </div>  
  18.                             <div class="edit">  
  19.                                 <input class="todo-input" data-class="todoEdit"/>  
  20.                             </div>  
  21.                         </div>  
  22.                     </li>  
  23.                 </ul>  
  24.             </div>  

Neil Kerkin has put together a complete TodoMVC demo app using the above, which can be accessed and played around with here.

Whilst it may look like quite a lot of work in the explanation above, now that you have a generic getBindings method written, it's a lot more trivial to simply re-use it and use data-classes rather than strict data-bindings for writing your KnockoutJS applications instead. The net result is hopefully cleaner markup with your data bindings being shifted from the View to a bindings object instead.

MVC Vs. MVP Vs. MVVM

Both MVP and MVVM are derivatives of MVC. The key difference between it and it's derivatives is the dependency each layer has on other layers as well as how tightly bound they are to each other.

In MVC, the View sits on top of our architecture with the controller laying below this. Models sit below the controller and so our Views know about our controllers and controllers know about Models. Here, our Views have direct access to Models. Exposing the complete Model to the View however may have security and performance costs, depending on the complexity of our application. MVVM attempts to avoid these issues.

In MVP, the role of the controller is replaced with a Presenter. Presenters sit at the same level as views, listening to events from both the View and model and mediating the actions between them. Unlike MVVM, there isn't a mechanism for binding Views to ViewModels, so we instead rely on each View implementing an interface allowing the Presenter to interact with the View.

MVVM consequently allows us to create View-specific subsets of a Model which can contain state and logic information, avoiding the need to expose the entire Model to a View. Unlike MVP's Presenter, a ViewModel is not required to reference a View. The View can bind to properties on the ViewModel which in turn expose data contained in Models to the View. As we've mentioned, the abstraction of the View means there is less logic required in the code behind it.

One of the downsides to this however is that a level of interpetation is needed between the ViewModel and the View and this can have performance costs. The complexity of this interpretation can also vary – it can be as simple as copying data or as complex as manipulating them to a form we would like the View to see. MVC doesn't have this problem as the whole Model is readily available and such manipulation can be avoided.

Backbone.js Vs. KnockoutJS

Understanding the subtle differences between MVC, MVP and MVVM are important but developers ultimately will ask whether they should consider using KnockoutJS over Backbone based in what we've learned. The following notes may be of help here:

posted on 2012-04-17 09:51  wwwsinagogogo  阅读(334)  评论(0编辑  收藏  举报