Freigeben über


Using KnockoutJS in Windows 8 Metro Style Apps

What is it?

If you have been messing around with JavaScript lately and come from a .Net Background you might have already heard of KnockoutJS.

Knockout

If you haven’t, KnockoutJS is a lightweight, free, JavaScript Library from Steve Sanderson that brings the MVVM pattern to the web world.  It’s also filled with #awesome sauce.

 

Cats and Dogs Living Together – Mass Hysteria!

It wasn’t too long ago that Windows app development and Web app development were two entirely different views of the world.  Thanks to the magic of WinRT language projection with Metro Style App development we no longer have to give up all the great libraries we are used from the web when  moving onto the desktop.  Most JavaScript libraries will run by just including it in your Metro Style App project.  Well at least local copies of those JavaScript Libraries.  If you want to call out to external libraries however you will need to start looking into the security model of how Windows Web Apps run (WWA). 

The exception to this is jQuery if you are pulling from the public CDN’s it has already been whitelisted for you.  I wont dive into details here but you can check out the great Build Session from Chris Tavares to learn about the security model for third party JavaScript libraries.

 

Ok Dave where's the code?

Hang tight!  If you just want the Windows 8 Metro Style App soure code grab it now.

Source Code

 

Setting up KnockoutJS

The first thing you will need to do is grab the latest version of KnockoutJS off GitHub.  I’ve included and tested with both the stable 2.0.0 release as well the 2.1.0rc update.

Create a new project in Visual Studio 11 Express as usual and then add the Knockout.js file to your JS folder.

KnockoutJS.js

As with all JavaScript files if you want Visual Studio to give you Intellisense you will need to add the following to the current JavaScript source file you are working on:

 /// <reference path="knockout-2.0.0.js" />

Note that Visual Studio will do this automatically for you if you simply drag the .js file over to your code window.

Next, you will need to add a reference to the KnockoutJS file in the html source. While we are at it let’s go ahead and grab the latest jQuery Library as well.  Some of the KnockouJS Learning samples assume jQuery is included.  Your references should look like below:

 <script src="/js/knockout-2.0.0.js"></script>
 <script src="/js/jquery-1.7.2.min.js"></script>

Now we should have working jQuery and Knockout JavaScript functionality but don’t forget that both of these libraries assume you have a DOM.  On the web we would normally start executing code after the onLoad event.  Fortunately for us this is super easy to do in Metro Style Apps by adding an event handler for DOMContentLoaded.  This should be the last line in your main function like so:

 app.start();
  
 //If Document fully loaded than begin processing
 document.addEventListener("DOMContentLoaded", initialize, false);
  
 })();

This will ensure your DOM is loaded and now accessible inside of your app.

The last step now is to ensure that all of our KnockoutJS bindings occur once our initialize function has been called.  Here is an example of what I mean:

 //Main Execution
 function initialize() {
  
     // Activates knockout.js
     ko.applyBindings(new AppViewModel());
  
 }

 

Converting Learn.KnockoutJS samples to a Metro Style Apps

There are some really great tutorials on Learn.KnockoutJS.com to help get you started.  I decided to go through each of those and bring them over into a Metro Style App.

Learn KnockoutJS

All five tutorials came over with minor tweaks except for Single Page applications.   I’ve named the tutorials using the following convention:

  • Introduction – default.html/default.js
  • Working with Lists and Collections – seats.html/seats.js
  • Single page applications – spa.html/spa.js
    • Didn’t work. I’ve included the source so if anyone wants to hack on it some more and see what’s up please let me know.  
  • Creating custom bindings – custom.html/custom.js
  • Loading and saving data – data.html/data.js

To change which sample you want to run just edit the startup page inside the project properties.

Capture

 

Introduction Example

No tweaks needed this came over perfectly.  Here is what my html looks like:

 <body>
  
 <p>First name: <strong data-bind="text: firstName"></strong></p>
 <p>Last name: <strong data-bind="text: lastName"></strong></p>
  
 </body>

 

Here is the JavaScript:

 (function () {
     "use strict";
  
     var app = WinJS.Application;
  
     app.onactivated = function (eventObject) {
         if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
             if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                 // TODO: This application has been newly launched. Initialize 
                 // your application here.
             } else {
                 // TODO: This application has been reactivated from suspension. 
                 // Restore application state here.
             }
             WinJS.UI.processAll();
         }
     };
  
     function AppViewModel() {
         this.firstName = "David";
         this.lastName = "Isbitski";
     }
  
     
  
     //Main Execution
     function initialize() {
  
         // Activates knockout.js
         ko.applyBindings(new AppViewModel());
  
     }
  
  
     app.oncheckpoint = function (eventObject) {
         // TODO: This application is about to be suspended. Save any state
         // that needs to persist across suspensions here. You might use the 
         // WinJS.Application.sessionState object, which is automatically
         // saved and restored across suspension. If you need to complete an
         // asynchronous operation before your application is suspended, call
         // eventObject.setPromise(). 
     };
  
     app.start();
  
     //If Document fully loaded than begin processing
     document.addEventListener("DOMContentLoaded", initialize, false);
  
 })();

 

Working with Lists and Collections Example

Worked right out of the box.  Here is the HTML:

 <body>
  
 <h2>Your seat reservations</h2>
  
 <table>
     <thead><tr>
         <th>Passenger name</th><th>Meal</th><th>Surcharge</th><th></th>
     </tr></thead>
     <tbody data-bind="foreach: seats">
         <tr>
             <td data-bind="text: name"></td>
             <td data-bind="text: meal().mealName"></td>
             <td data-bind="text: meal().price"></td>
         </tr>    
     </tbody>
 </table>
 </body>

 

Here is the JavaScript:

  
 (function () {
     "use strict";
  
     var app = WinJS.Application;
  
     app.onactivated = function (eventObject) {
         if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
             if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                 // TODO: This application has been newly launched. Initialize 
                 // your application here.
             } else {
                 // TODO: This application has been reactivated from suspension. 
                 // Restore application state here.
             }
             WinJS.UI.processAll();
         }
     };
  
     // Class to represent a row in the seat reservations grid
     function SeatReservation(name, initialMeal) {
         var self = this;
         self.name = name;
         self.meal = ko.observable(initialMeal);
     }
  
     // Overall viewmodel for this screen, along with initial state
     function ReservationsViewModel() {
         var self = this;
  
         // Non-editable catalog data - would come from the server
         self.availableMeals = [
         { mealName: "Standard (sandwich)", price: 0 },
         { mealName: "Premium (lobster)", price: 34.95 },
         { mealName: "Ultimate (whole zebra)", price: 290 }
         ];
  
         // Editable data
         self.seats = ko.observableArray([
         new SeatReservation("Steve", self.availableMeals[0]),
         new SeatReservation("Bert", self.availableMeals[0])
         ]);
     }
  
    
  
     //Main Execution
     function initialize() {
  
         // Activates knockout.js
          ko.applyBindings(new ReservationsViewModel());
  
     }
  
  
     app.oncheckpoint = function (eventObject) {
         // TODO: This application is about to be suspended. Save any state
         // that needs to persist across suspensions here. You might use the 
         // WinJS.Application.sessionState object, which is automatically
         // saved and restored across suspension. If you need to complete an
         // asynchronous operation before your application is suspended, call
         // eventObject.setPromise(). 
     };
  
     app.start();
  
     //If Document fully loaded than begin processing
     document.addEventListener("DOMContentLoaded", initialize, false);
  
 })();

 

Creating Custom Bindings Example

I needed to do two things to get this example to work.  The first one was to include jQuery it assumes it would be present.  The second was to replace the alert() function call (this doesn’t exist in WinRT) with a native WinRT MessageDialog.  Here is what the HTML looks like:

 <body>
  
 <h3 data-bind="text: question"></h3>
 <p>Please distribute <b data-bind="text: pointsBudget"></b> points between the following options.</p>
  
 <table>
     <thead><tr><th>Option</th><th>Importance</th></tr></thead>
     <tbody data-bind="foreach: answers">
         <tr>
             <td data-bind="text: answerText"></td>
             <td><select data-bind="options: [1,2,3,4,5], value: points"></select></td>
         </tr>    
     </tbody>
 </table>
  
 <h3 data-bind="fadeVisible: pointsUsed() > pointsBudget">You've used too many points! Please remove some.</h3>
 <p>You've got <b data-bind="text: pointsBudget - pointsUsed()"></b> points left to use.</p>
 <button data-bind="enable: pointsUsed() <= pointsBudget, click: save">Finished</button>
 </body>

 

Here is the JavaScript:

  
 (function () {
     "use strict";
  
     var app = WinJS.Application;
  
     app.onactivated = function (eventObject) {
         if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
             if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                 // TODO: This application has been newly launched. Initialize 
                 // your application here.
             } else {
                 // TODO: This application has been reactivated from suspension. 
                 // Restore application state here.
             }
             WinJS.UI.processAll();
         }
     };
  
     
  
     // ----------------------------------------------------------------------------
     // Page viewmodel
  
     function Answer(text) { this.answerText = text; this.points = ko.observable(1); }
  
     function SurveyViewModel(question, pointsBudget, answers) {
         this.question = question;
         this.pointsBudget = pointsBudget;
         this.answers = $.map(answers, function (text) { return new Answer(text) });
  
         //Dave Isbitski - 4/24/12 - Changed alert to native WinRT dialog
         this.save = function () {
             var dlg = new Windows.UI.Popups.MessageDialog("To do");
              dlg.showAsync().done();
  
         };
  
         this.pointsUsed = ko.computed(function () {
             var total = 0;
             for (var i = 0; i < this.answers.length; i++)
                 total += this.answers[i].points();
             return total;
         }, this);
     }
  
     //Main Execution
     function initialize() {
  
         ko.bindingHandlers.fadeVisible = {
             init: function (element, valueAccessor) {
                 // Start visible/invisible according to initial value
                 var shouldDisplay = valueAccessor();
                 $(element).toggle(shouldDisplay);
             },
             update: function (element, valueAccessor) {
                 // On update, fade in/out
                 var shouldDisplay = valueAccessor();
                 shouldDisplay ? $(element).fadeIn() : $(element).fadeOut();
             }
         };
  
         // Activates knockout.js
         ko.applyBindings(new SurveyViewModel("Which factors affect your technology choices?", 10, [
    "Functionality, compatibility, pricing - all that boring stuff",
    "How often it is mentioned on Hacker News",
    "Number of gradients/dropshadows on project homepage",
    "Totally believable testimonials on project homepage"
         ]));
  
     }
  
  
     app.oncheckpoint = function (eventObject) {
         // TODO: This application is about to be suspended. Save any state
         // that needs to persist across suspensions here. You might use the 
         // WinJS.Application.sessionState object, which is automatically
         // saved and restored across suspension. If you need to complete an
         // asynchronous operation before your application is suspended, call
         // eventObject.setPromise(). 
     };
  
     app.start();
  
     //If Document fully loaded than begin processing
     document.addEventListener("DOMContentLoaded", initialize, false);
  
 })();

 

Loading and Saving Data Example

Only thing I had to add here was jQuery due to the dependency on it. Here is the HTML:

 <body>
  
 <h3>Tasks</h3>
  
 <form data-bind="submit: addTask">
     Add task: <input data-bind="value: newTaskText" placeholder="What needs to be done?" />
     <button type="submit">Add</button>
 </form>
  
 <ul data-bind="foreach: tasks, visible: tasks().length > 0">
     <li>
         <input type="checkbox" data-bind="checked: isDone" />
         <input data-bind="value: title, disable: isDone" />
         <a href="#" data-bind="click: $parent.removeTask">Delete</a>
     </li> 
 </ul>
  
 You have <b data-bind="text: incompleteTasks().length">&nbsp;</b> incomplete task(s)
 <span data-bind="visible: incompleteTasks().length == 0"> - it's beer time!</span>
  
 </body>

 

Here is the JavaScript:

  
 (function () {
     "use strict";
  
     var app = WinJS.Application;
  
     app.onactivated = function (eventObject) {
         if (eventObject.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
             if (eventObject.detail.previousExecutionState !== Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                 // TODO: This application has been newly launched. Initialize 
                 // your application here.
             } else {
                 // TODO: This application has been reactivated from suspension. 
                 // Restore application state here.
             }
             WinJS.UI.processAll();
         }
     };
  
     
     function Task(data) {
         this.title = ko.observable(data.title);
         this.isDone = ko.observable(data.isDone);
     }
  
     function TaskListViewModel() {
         // Data
         var self = this;
         self.tasks = ko.observableArray([]);
         self.newTaskText = ko.observable();
         self.incompleteTasks = ko.computed(function () {
             return ko.utils.arrayFilter(self.tasks(), function (task) { return !task.isDone() });
         });
  
         // Operations
         self.addTask = function () {
             self.tasks.push(new Task({ title: this.newTaskText() }));
             self.newTaskText("");
         };
         self.removeTask = function (task) { self.tasks.remove(task) };
  
         // Load initial state from server, convert it to Task instances, then populate self.tasks
         $.getJSON("/tasks", function (allData) {
             var mappedTasks = $.map(allData, function (item) { return new Task(item) });
             self.tasks(mappedTasks);
         });
     }
  
     //Main Execution
     function initialize() {
  
         // Activates knockout.js
         ko.applyBindings(new TaskListViewModel());
  
     }
  
  
     app.oncheckpoint = function (eventObject) {
         // TODO: This application is about to be suspended. Save any state
         // that needs to persist across suspensions here. You might use the 
         // WinJS.Application.sessionState object, which is automatically
         // saved and restored across suspension. If you need to complete an
         // asynchronous operation before your application is suspended, call
         // eventObject.setPromise(). 
     };
  
     app.start();
  
     //If Document fully loaded than begin processing
     document.addEventListener("DOMContentLoaded", initialize, false);
  
 })();

 

Conclusion

I hope this post has given you an idea of how easy it is to include existing JavaScript Libraries from the Web inside your own Metro Style Apps.  As Always - if you are currently working on a Windows 8 app I would love to hear about it!

You may also want to check out my previous Windows 8 Metro Style Development Tips:

Comments

  • Anonymous
    April 24, 2012
    Great sample! Thanks for sharing.

  • Anonymous
    December 04, 2012
    Thanks Dave. This is exactly what I was looking for. Plus, it is always funny when you search the web and it turns out the best result is from someone you know!

  • Anonymous
    March 18, 2013
    thank you Microsoft for understanding that the html/javascript platform is where it's at