Knockout.js vs. Backbone.js – Part 1
So I’ve recently been diving into web client frameworks and decided to compare Knockout.js and Backbone.js. To compare the two, I chose a very simple scenario (based on a user request we recently had to implement in an ASP.NET web forms site) – the user wanted the ability to query for the status of a SQL Agent job. I already had a service that returned metadata about a SQL Agent Job, so I simply needed to create a way for users to select a job (or more), and view the status on the screen.
The Gist
At the outset, I wanted to keep it simple, so I set the following requirements:
- Our existing legacy “.asmx” web service was going to be the data source.
- The UI needed to be able to display any number of SQL Agent jobs at a time.
- The data for each SQL Agent job should continuously refresh at short intervals (2 seconds or so)
Backbone
What exactly is Backbone.js? Backbone.js is an MVC framework for the web client (i.e.- the browser). In their own words:
“Backbone supplies structure to JavaScript-heavy applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing application over a RESTful JSON interface.”
It’s important to bear in mind that in my exploration of Backbone.js, I’ve barely scratched the surface. In fact, my simple example doesn’t actually extend Backbone’s controller type. As a result, I didn’t take advantage of the interesting ability to use URL fragments to map to controller actions and events. Instead, what I was looking for was a brief and quick introduction to the framework. (Note: one of the example Backbone.js apps also omits the use of controllers, while still demonstrating a large set of interesting features – arguably much more interesting than my example!.)
The Service
Our legacy web service takes the string name of a SQL Agent job as an argument and returns an object with the following members:
- Job Name
- Current Step
- Last Start Date
- Last End Date
- Last Run Time
- Next Run Date
- Last Outcome Message
A typical response will look like this:
The Models
We have two models – a “SqlJobStatus” model that represents the data listed above, and a “SqlStatusJobCollection” model that represents a container of “SqlJobStatus” models.
$(function(){ window.SqlJobStatus = Backbone.Model.extend({ defaults: { JobName: "", CurrentStep: "", LastStartDate: "", LastEndDate: "", LastRunTime: "", LastOutcomeMessage: "", NextRunDate: "", CssClass: "idle" }, initialize: function() { _.bindAll(this, "fetch", "clear", "updateData"); }, fetch: function(options) { if(this.get('JobName') != "") { $.ajax({ type : "POST", url : "http://localhost/SqlAgentHelper.asmx/GetJobStatus", data : '{"jobName":"' + this.get('JobName') + '" }', contentType : "application/json", dataType : "json", success : this.updateData, error : function() { alert('There was an error retrieving the status of the job.'); } }); } }, clear: function() { this.view.remove(); }, updateData: function(result) { this.set({ JobName : result.d.JobName, CurrentStep : result.d.CurrentStepName == undefined ? "N/A" : result.d.CurrentStepName, LastEndDate : result.d.LastEndDate == undefined ? "N/A" : result.d.LastEndDate.to_date(), LastOutcomeMessage : result.d.LastOutcomeMessage, LastRunTime : result.d.LastRunTime == undefined ? "N/A" : result.d.LastRunTime, LastStartDate : result.d.LastStartDate == undefined ? "N/A" : result.d.LastStartDate.to_date(), NextRunDate : result.d.NextRunDate == null ? "N/A" : result.d.NextRunDate, CssClass : (result.d.CurrentStepName == undefined || result.d.CurrentStepName == "") ? "idle" : "active" },{silent:true}); this.change(); } }); window.SqlJobStatusCollection = Backbone.Collection.extend({ model: SqlJobStatus }); });
Let’s this break down:
- “defaults” – initializing members with default values. (this is a Backbone.js standard member)
- “initialize” – called when the model is created. In this case, we’re simply binding the context of “this” in the scope of the function names provided to be the model object. (this is a Backbone.js standard member)
- “fetch” – I’ve overridden the fetch function to use a standard ajax call. Backbone.js prefers to work with RESTful services, not the legacy .asmx service I’m dealing with. (this is a Backbone.js standard member)
- “clear” – removes the view this model is associated with from the page. (this is a Backbone.js standard member)
- “updateData” – ajax success call back function that takes the response and populates the model with updated data.
The Views
We have two views: “JobStatusView” and “AppView”. AppView is what it sounds like – the main app view. It houses the controls used to add a “JobStatusView” to the page.
$(function() { window.JobStatusView = Backbone.View.extend({ tagName: "li", className: "jobStatView", template: _.template($('#stat-template').html()), initialize: function(options) { _.bindAll(this, 'render', 'remove'); this.model.bind('change', this.render); this.model.view = this; }, render: function() { $(this.el).html(this.template(this.model.toJSON())); $(this.el).bind('dblclick', this.remove); return this; }, remove: function() { $(this.el).remove(); this.model.container.remove(this.model); } }); window.AppView = Backbone.View.extend({ el : $('#statusApp'), statList : $('#statusList'), initialize: function() { _.bindAll(this, 'addOne', 'updateData' ,'render', 'addAllJobs'); this.model = new SqlJobStatusCollection(); }, addOne: function(jobName) { var mdl = new window.SqlJobStatus({ JobName: jobName}); var view = new window.JobStatusView({ model: mdl }); this.model.add(mdl); mdl.container = this.model; mdl.fetch(); this.statList.append(view.render().el); }, updateData: function() { for(i = 0; i < this.model.length; i++) { this.model.models[i].fetch(null); } }, render: function() { var html = "<div id='choice-div'>" + "<span>Select the job you want to query for:</span>" + "<select id='selJob' name='jobName' >" + "<option>Back to the Future</option>" + "<option>Dances with Wolves</option>" + "<option>Gladiator</option>" + "<option>Primer</option>" + "<option>Shaun of the Dead</option>" + "<option>Stargate Continuum</option>" + "</select>" + "<input type='button' value='Add' onclick='window.appView.addOne($("#selJob").val());' /> " + "<input type='button' value='Add All Jobs' onclick='window.appView.addAllJobs();' />" + "</div>"; $(this.el).html(html); }, addAllJobs: function() { $('#selJob option').each(function() { window.appView.addOne($(this).val()); }); } }); });
Let’s break down these two views:
JobStatusView (View for individual SQL Agent Job Status)
- “tagName” – The DOM element tag name that will be created for this view. (Note that Backbone.js allows you to specify an existing DOM element, OR your can provide a tagName value and it will create a DOM element of that type for you. (this is a Backbone.js standard member)
- “className” – The CSS class name that will be applied to this DOM element. (this is a Backbone.js standard member)
- “template” – the name of the view template that will be used.
- “initialize” – called when the view is created. I’m setting the “this” context of function members, binding the model “change” event to the render method of the view, and setting the model’s “view’” handle to this view. (this is a Backbone.js standard member)
- “render” – I’m binding the template to the model, appending the resulting html to the DOM element, binding the double click event to the “remove” function. (this is a Backbone.js standard member)
- “remove” – removes the view’s DOM element from the page, and triggers the removal of the model from its container as well. (this is a Backbone.js standard member)
AppView (The Application View)
- “el” – represents the DOM element for this view (this is a Backbone.js standard member)
- “statList” – a member I added that has a handle to the DOM element where JobStatusViews will be added.
- “initialize” – called when the view is created. In addition to setting the “this” context of the several functions, I’m setting the model the view is bound to. (this is a Backbone.js standard member)
- “addOne” – handles adding a new JobStatusView to the statList element, which includes adding a SqlJobStatus model to this view’s collection.
- “updateData” – simple iterates over the models in this views model collection and calls fetch on each one.
- “render” – renders the view to the page. (this is a Backbone.js standard member)
- “addAllJobs” – iterates over the list of available jobs and adds models/views for each one.
And Finally – the HTML Page to tie it all together
<html> <head> <link rel="stylesheet" type="text/css" href="../Common/style.css" /> <script type="text/javascript" src="../Common/project.js"></script> <script type="text/template" id="stat-template"> <fieldset class="<%=CssClass %>"> <legend> <div class="legend-header">SQL Job Status</div> </legend> <table> <tr> <td class="label-header">Job Name:</td> <td class="label-value"><span id="jobName"><%= JobName %></span></td> </tr> <tr> <td class="label-header">Current Step:</td> <td class="label-value"><span id="currentStep"><%= CurrentStep %></span></td> </tr> <tr> <td class="label-header">Last Start Date:</td> <td class="label-value"><span id="lastStartDate"><%= LastStartDate %></span></td> </tr> <tr> <td class="label-header">Last End Date:</td> <td class="label-value"><span id="lastEndDate"><%= LastEndDate %></span></td> </tr> <tr> <td class="label-header">Last Run Time (minutes):</td> <td class="label-value"><span id="lastDuration"><%= LastRunTime %></span></td> </tr> <tr> <td class="label-header">Next Run Date:</td> <td class="label-value"><span id="nextRunDate"><%= NextRunDate %></span></td> </tr> <tr> <td class="label-header">Last Outcome Message:</td> <td class="label-value"><span id="lastOutcomeMessage"><%= LastOutcomeMessage %></span></td> </tr> </table> </fieldset> </script> <script type="text/javascript" src="underscore.js"></script> <script type="text/javascript" src="backbone.js"></script> <script type="text/javascript" src="../Common/json2.js"></script> <script type="text/javascript" src="../Common/jquery-1.4.2.js"></script> <script type="text/javascript" src="Views.js"></script> <script type="text/javascript" src="Models.js"></script> <script type="text/javascript"> $(function() { window.appView = new window.AppView(); window.appView.render(); window.pollData = function() { window.appView.updateData(); } setInterval("window.pollData()", 2000); }); </script> <div id="statusApp" class="div-containerB"></div> <ul id="statusList"></ul>
Oddly enough, the majority of the page consists of script elements. Notice that we’re not only using them to load script dependencies, but we’ve also defined the view template for the JobStatusView in one. In the last script element we’re creating the AppView instance, rendering it, setting up a polling function to retrieve data for any jobs being viewed at a regular interval.
Viewing the Results
When the page loads, it’s empty aside from this:
The above output is evidence that our AppView has rendered correctly. If we were to select one of the jobs and click “Add”, we’d see something similar to this:
If we click the “Add All Jobs” button, views like the one above will be added to the page for all six available jobs. You might have noticed that in our “updateData” function in the SqlJobStatus model conditionally set the “CssClass” based on the status of the job. As a result, active jobs in the below screen shot are green:
Post Mortem
I have mixed feelings about Backbone.js – albeit based on this very limited exposure. I think their approach to mapping controller actions to url fragments is brilliant. Although I didn’t know when I started this experiment that Backbone.js is primarily designed to talk to RESTful services (and my example service is anything but), I think it’s the right direction and a good choice on their part. The aspect of Backbone.js that I am least happy with is how Views are handled. Emitting HTML – whether it be from a template, or in-line concatenated HTML (my example has both) – from a “render” method feels….heavy? Too opinionated? There were several moments as I built this example when I wondered why I wasn’t seeing my results on the screen – it was because I had forgotten to call “render”. This is a tough hurdle to clear when you’re used to there being very little between you, your HTML and it being immediately rendered to the browser.
This complaint wouldn’t rule out my consideration of Backbone.js for projects in the future. I think the main thing to keep in mind is that it is an opinionated framework (much more so than Knockout.js, in my opinion) – designed to be a full stack for client-side MVC apps. As long as that doesn’t conflict with your goals, I’d would definitely recommend checking it out. The sample apps on the Backbone.js site are definitely worth browsing – these guys have done some amazing things.
Check back soon for the second post in this series where I cover the same “example” application using Knockout.js…
Tags: Backbone.js, HTML, JavaScript, Knockout.js, MVC, Web

Pingback: Knockout.js vs. ..