Local Data Access: I: IndexedDB - Beginning Windows 8 Data DevelopmentUsing C# and JavaScript (2013)

Beginning Windows 8 Data DevelopmentUsing C# and JavaScript (2013)

4. Local Data Access: I: IndexedDB

Vinodh Kumar1

(1)

NY, US

Abstract

WinRT does not have any built-in database capabilities like SQL Server CE. It doesn't provide any APIs to connect directly to a SQL Server; instead we need to use a cloud storage solution or rely on third-party options like SQLite. Cloud storage is not an ideal solution in many cases, as it requires complex data management. Also it might not be an affordable solution, as storing data in the cloud is not free in most cases. In the next three chapters we learn about local storage options like indexedDB, JET API, Application Storage, and SQLite. To start with, in this chapter we learn to use IndexedDB for storing structured data locally and build a Movie collection and Inventory app that use IndexedDB as data storage.

WinRT does not have any built-in database capabilities like SQL Server CE. It doesn’t provide any APIs to connect directly to a SQL Server; instead we need to use a cloud storage solution or rely on third-party options like SQLite. Cloud storage is not an ideal solution in many cases, as it requires complex data management. Also it might not be an affordable solution, as storing data in the cloud is not free in most cases. In the next three chapters we learn about local storage options like indexedDB, JET API, Application Storage, and SQLite. To start with, in this chapter we learn to use IndexedDB for storing structured data locally and build a Movie collection and Inventory app that use IndexedDB as data storage.

What Is IndexedDB?

IndexedDB or the Indexed Database API is a nonrelational data store, designed to store structured objects in collections known as the object store. The object store holds records as key–value pairs. Each record in the object store has a single key, which can be configured to autoincrement or can be provided by the application. This key is like the primary key in a relational database table, where no two records with in an object store can be identified by the same key.

Note

IndexedDB is supported by Firefox (since version 4), Internet Explorer 10, and Google Chrome (since version 11). Safari and Opera support an alternate mechanism for client-side database storage called Web SQL Database. As of ­November 2010, the W3C Web Applications Working Group ceased working on the Web SQL Database specification, citing lack of independent implementations.

Using IndexedDB in Windows 8 Application

Internet Explorer 10 and Windows Store apps using JavaScript support the Indexed Database API defined by the World Wide Web Consortium (W3C) Indexed Database API specification, so applications written using HTML5 and JavaScript will be able to use IndexedDB as a local storage options. The following are some of the common IndexedDB contracts.

· Database: A database consists of one or more object stores that hold the data stored in the database. It also contains indexes and is used to manage transactions. There can be multiple databases in an application.

· Object store: An object store is the primary storage used for storing data in a database. It’s a collection of JavaScript objects where attributes have key–value pairs.

· Key: A key is used to uniquely identify an object within a database. It has values of type float, date, string, and array. It’s very similar to the primary key columns in a relational database table. It also imposes an ascending sort order on the associated objects.

· Value: A value is a JavaScript object that is associated with a given key. Every record is associated with a value. It can be a complex object that has no schematization requirements.

· Key path: A key path is a string that defines a way to extract a key from a value. A key path is said to be valid when either it is has an empty string, or multiple JavaScript elements separated by periods.

· Index: An index is an alternative method to retrieve records in an object store rather than using a key. It’s a specialized form of storage that supports searching objects in the store by attribute values.

· Transaction: A transaction is used to read or write data into the database. Transactions are always operated in one of the three modes: read-only, readwrite, or versionchange.

· Request: A request is used to perform read or a write operation on a database. It’s analogous to a SQL statement.

· Key range: The key range is used to retrieve records from object stores and indexes.

· Cursor: A cursor is a brief mechanism used to iterate over multiple records in a database. It is bidirectional, and can skip duplicate records in a nonunique index.

Even though IndexedDB concepts looks similar to relational database management elements, one key difference is that there is no relational semantics, which means we cannot use joins. With this introduction we learn how to use IndexedDB as data storage by creating My Collections, a movie collection and inventory Windows Store app using HTML5 and JavaScript.

Creating the My Collections App

Many movie buffs have a huge collection of DVD and Blu-ray movies that they share with their friends and family. Sometimes keeping track of all these movies becomes a tedious process. To manage and keep track of the collection, here we build a simple app that helps to add and track movies within the collection. This app has three HTML pages.

· Start page: This page displays the list of movies in the collection.

· Search page: The Search page is invoked from the Windows 8 Search charm. This page displays the matching results from the collections and it also searches for matching results on one of the most popular movie review and information sites, www.Rottentomatoes.com , and displays the results.

· Add/Edit page: This page can be accessed from the Start page or from the search result page. It displays the movie details and provides an option to add it to one’s collection. We can also edit the movie information and its available status (see Figure 4-1).

A978-1-4302-4993-1_4_Fig1_HTML.jpg

Figure 4-1.

My Collection Windows 8 app displays the movies in the collection

Getting Started

To start with, let’s create a new Windows Store Blank App (JavaScript) project and name it MyCollections (see Figure 4-2). We add two new pages to the project: Home.html and Details.html.

A978-1-4302-4993-1_4_Fig2_HTML.jpg

Figure 4-2.

Visual Studio templates for JavaScript creates a Blank application with HTML, CSS, and JavaScript files

We also add a Search Contract and name it searchResults.html as shown in Figure 4-3. As mentioned before, this is the page that will be involved when we search for movies from the Windows 8 Search charm.

A978-1-4302-4993-1_4_Fig3_HTML.jpg

Figure 4-3.

Adding support for the Search contract by using the Visual Studio template

Finally we add a JavaScript file, Movie.js. This is the main file and it contains the functionality that drives the entire application.

With all the files in place and after you move some files to restructure the project for better management, the final MyCollections Windows 8 app project will look like the one shown in Figure 4-4.

A978-1-4302-4993-1_4_Fig4_HTML.jpg

Figure 4-4.

Visual Studio Solution Explorer displaying the MyCollections project structure

Defining the Schema

The MyCollections IndexedDB database CollectionDB will have only one object store called Movies. The Movies object store will have six key paths, as follows.

· Id: Autoincrement acts like a primary key.

· Title: S tores the title of the movie.

· Year: Stores the release year of the movie.

· Thumbnail: S tores the Rotten tomatoes.com thumbnail link.

· poster: Stores the Rotten tomatoes.com poster link.

· Status: C urrent status of the movie like Available, Borrowed, Lent Out.

Creating the Database

We start the coding by creating the CollectionDB database as soon as the application starts at the activated event in default.js by calling a function createDB as shown in Listing 4-1.

Listing 4-1. createDB Function Is Called Inside the Activated Event

app.addEventListener("activated", function (args) {

if (args.detail.kind === activation.ActivationKind.launch) {

if (args.detail.previousExecutionState !== 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.

}

if (app.sessionState.history) {

nav.history = app.sessionState.history;

}

args.setPromise(WinJS.UI.processAll().then(function () {

if (nav.location) {

nav.history.current.initialPlaceholder = true;

return nav.navigate(nav.location, nav.state);

} else {

return nav.navigate(Application.navigator.home);

}

}));

//creating the indexeddb database

createDB();

}

});

The CreateDB function creates the request to open the databases. If it doesn’t exist, create it and it will immediately upgrade to version 1. At any given time with in the app only a single version of a database can exist. After it’s created, a database and its object stores can only be changed through a specialized type of transaction mode known as a versionchange. To change a database after its creation, we must open the database with a higher version number, and that is the reason to change it to version 1. This action causes the upgradeneeded event to fire and the code to create the Movies object store in onupgradeneeded.

The Movies object store is created using the IndexedDB createObjectStore function. This function gets a name for the object store and sets the key path and key generator. This object store also has indexes that hold additional information. These indexes are created using the createIndexfunction. Success callback is invoked on opening the database. Here we set the database context to an attribute, as shown in Listing 4-2.

Listing 4-2. Database and Table Are Created in the createDB Function

function createDB() {

// Create the request to open the database, named CollectionDB. If it doesn't exist, create it and immediately

// upgrade to version 1.

var dbRequest = window.indexedDB.open("CollectionDB", 1);

dbRequest.onupgradeneeded = function (e) {

MyCollection.db = e.target.result;

var txn = e.target.transaction;

var movieTable = MyCollection.db.createObjectStore(

"Movies"

,{

keyPath: "id"

, autoIncrement: true

});

movieTable.createIndex("title"

, "title"

, { unique: false });

movieTable.createIndex("year"

, "year"

, { unique: false });

movieTable.createIndex("image"

, "image"

, { unique: false });

movieTable.createIndex("poster"

, "poster"

, { unique: false });

movieTable.createIndex("status"

, "status"

, { unique: false });

txn.onerror = function () {

WinJS.log && WinJS.log("Database creation failed"

, "Log"

, "Status");

};

txn.oncomplete = function () {

WinJS.log && WinJS.log("Database table created"

, "Log"

, "Status");

};

};

dbRequest.onsuccess = function (e) {

MyCollection.db = e.target.result;

};

}

Creating the Movie Object in Windows 8 JavaScript

Movie.js contains a self-executing anonymous function where objects are created inside the MyCollections namespace. This object contains a property, Movie.

The Movie object is defined as a WinJS.Class using the WinJS.Class.define() method. As can be seen in Listing 4-3, this method takes various parameters that are assigned to the corresponding properties of the Movie class. The Movie class has five properties that match the IndexedDB objectstore Movie and one other propety, IsInCollection, that is used to determine whether the object is already in the collection.

Listing 4-3. Defining Movie Object in Movie.js

WinJS.Namespace.define("MyCollection", {

Movie: WinJS.Class.define(

function () {

this.title = "";

this.year = 0;

this.image = "";

this.isInCollection = false;

this.status = "";

this.poster = "";

this.id = 0;

},

{

getTitle: function () { return this.title; },

setTitle: function (newValue) { this.title = newValue; },

getImage: function () { return this.image; },

setImage: function (newValue) { this.image = newValue; },

getIsInCollection: function () { return this.isInCollection; },

setIsInCollection: function (newValue) { this.isInCollection = newValue; },

getYear: function () { return this.year; },

setYear: function (newValue) {

this.year = newValue;

},

getPoster: function () { return this.poster; },

setPoster: function (newValue) { this.poster = newValue; },

getStatus: function () { return this.status; },

setStatus: function (newValue) { this.status = newValue; },

getID: function () { return this.id; },

setID: function (newValue) {

this.id = newValue;

},

},

The Movie object also has CRUD functions that we use to add, delete, and update data to IndexedDB. Let’s look at each one of them in detail.

Saving the Movie Object

To add a movie to the movie collection, we call the saveMovie function. This function first checks to see if the movie details already exist in the IndexedDB Movie objectStore. If this information is already present, it will update the existing row; otherwise, it will add a new row to the IndexedDB Movie objectStore. To do this, first create a new transaction involving the Movie objectStore as shown in the Listing 4-4 and set the mode to readWrite and will get a handle of the Movie object store. Now that we have access to the objectstore, we can just pass a JSON object to either add or put command depending on the value of the ID parameter.

Listing 4-4. Saving Movie Object to IndexedDB Using the saveMovie Function

saveMovie: function (id, title, year, image, poster, status ) {

var txn = MyCollection.db.transaction(["Movies"], "readwrite");

var movieTable = txn.objectStore("Movies");

var saveRequest;

if (id > 0)

saveRequest = movieTable.put(

{

id: id,

title: title,

year: year,

image: image,

poster: poster,

status: status

});

else {

saveRequest = movieTable.add(

{

title: title,

year: year,

image: image,

poster: poster,

status: status

});

}

saveRequest.onsuccess = function () {

WinJS.log && WinJS.log("Movie Updated: " + this + ".", "Log", "Status");

};

saveRequest.onerror = function () {

WinJS && WinJS.log("Failed to update Movie: " + this + ".", "Log", "error");

};

}

Note

In IndexedDB, objectStore.add() is used to add an object to the store and objectStore.put() is used to update an object.

Deleting the Movie Object

The deleteMovie function deletes a row from the Movie objectStore. Just like saveMovie, we start a transaction, reference the object store that contains the Movie object, and issue a delete command with the unique ID of our object (see Listing 4-5).

Listing 4-5. Deleting Movie Object From IndexedDB Using the deleteMovie Function

deleteMovie: function ( id ) {

var txn = MyCollection.db.transaction(["Movies"], "readwrite");

var movieTable = txn.objectStore("Movies");

var deleteRequest = movieTable.delete(id)

deleteRequest.onsuccess = function () {

WinJS.log && WinJS.log("Movie Deleted: " + this + ".", "Log", "Status");

};

deleteRequest.onerror = function () {

WinJS && WinJS.log("Failed to delete Movie: " + this + ".", "Log", "error");

};

}

Retrieving Movie Details

Movie details are retrieved either from the database that is part of the user’s collection or from the Rotten Tomatoes database using the REST API. These two actions are perfomaed within the JavaScript functions loadFromDB and loadSearchResult. These functions in turn call the buildMoviefunction to build a Movie object. The buildMovie function checks the model passed in as a parameter, creates a new Movie object, tries to set its values from the model passed, and returns a bondable object with the help of the WinJS.Binding.as() method (see Listing 4-6).

Listing 4-6. buildMovie Function Creates a Movie Object From the Model

buildMovie: function (model) {

var newMovie = new MyCollection.Movie();

if (model.hasOwnProperty("title")) {

newMovie.setTitle(model.title);

}

if (model.hasOwnProperty("year")) {

newMovie.setYear(model.year);

}

if (model.hasOwnProperty("movieId")) {

newMovie.setID(model.id);

newMovie.setIsInCollection(true);

}

if (model.hasOwnProperty("status")) {

newMovie.setStatus(model.status);

}

if (model.hasOwnProperty("thumbnail")) {

newMovie.setImage(model.thumbnail);

}

if (model.hasOwnProperty("poster")) {

newMovie.setPoster(model.poster);

}

//only if the request from rottentomatoes

if (model.hasOwnProperty("posters")) {

newMovie.setImage(model.posters.thumbnail);

newMovie.setPoster(model.posters.detailed);

}

return new WinJS.Binding.as(newMovie);

}

The loadFromDB function, shown in Listing 4-7, takes string as a parameter and returns an array of Movie objects that matches with the title of the movie stores in the IndexedDB Movie objectStore. Like the saveMovie and deleteMovie methods, we first create a new transaction involving the Movie objectStore and set the mode to read-only, as only the data is retrieved here. Then we open a cursor to iterate over records in the Movie object store. The results are passed through to the success callback on the cursor, where we render the result. The callback is fired only once per result, and will call continue to keep iterating across the data on the result object. A JSON object is constructed out of the result and is passed to the buildMovie function to return a WinJS.Binding object that is finally added to the array.

Listing 4-7. Searches the indexedDB and fill the results to an array

loadFromDB: function (searchText) {

var collection = new Array();

var txn = MyCollection.db.transaction(["Movies"], "readonly");

var movieCursorRequest = txn.objectStore("Movies").openCursor();

movieCursorRequest.onsuccess = function (e) {

var cursor = e.target.result;

if (cursor) {

var data = cursor.value;

if (data.title.indexOf(searchText) > -1) {

var movieData = {

movieId: data.id

, title: data.title

, year: data.year

, thumbnail: data.image

, poster: data.poster

, status: data.status

};

var newMovie = MyCollection.Movie.buildMovie(movieData);

collection.push(newMovie);

}

cursor.continue();

}

};

return collection;

}

Like loadFromDB, the loadSearchResult function shown in Listing 4-8 takes search text as a parameter and returns an array of Movie objects. This function performs two actions. First it queries the Rotten Tomatoes movie database using a public API and loads the result to an array. Next it calls loadFromDB and adds the matching movie object to the existing array. Now the array has objects that match the search result from the IndexedDB Movie object store and also from the Rotten Tomatoes search results.

Listing 4-8. Search the Rotten Tomatoes Database and Add the Results to an Array

loadSearchResult: function (searchText) {

var searchUrl = " http://api.rottentomatoes.com/api/public/v1.0/movies.json?apikey=XXXXXXXXXXXXXf8&page_limit=10&q= " + searchText;

return WinJS.xhr({ url: searchUrl }).then(

function (result) {

var result = window.JSON.parse(result.responseText).movies;

var collection = new Array();

if (result) {

result.forEach(function (newObject) {

var newMovie = MyCollection.Movie.buildMovieFromRottentomatoes(newObject);

collection.push(newMovie);

});

var txn = MyCollection.db.transaction(["Movies"], "readonly");

var movieCursorRequest = txn.objectStore("Movies").openCursor();

movieCursorRequest.onsuccess = function (e) {

var cursor = e.target.result;

if (cursor) {

var data = cursor.value;

if (data.title.indexOf(searchText) > -1) {

var movieData = {

movieId: data.id

, title: data.title

, year: data.year

, thumbnail: data.image

, poster: data.poster

, status: data.status

};

var newMovie = MyCollection.Movie.buildMovie(movieData);

collection.push(newMovie);

}

cursor.continue();

}

};

}

return collection;

});

}

Designing the App Start Page

Home.html is the start page of this app (see Figure 4-1). It displays the Movies in our IndexedDB collection in a grid layout using the WinJS.UI.ListView element by binding to a Movies Collection in Home.js. We also define an item template that contains the markup to display the details of each movie (see Listing 4-9).

Listing 4-9. Home.html Page Includes a ListView With Item Template to Display Movie Details

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<title>homePage</title>

<!-- WinJS references -->

<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />

<script src="//Microsoft.WinJS.1.0/js/base.js"></script>

<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

<link href="/css/default.css" rel="stylesheet" />

<link href="/pages/home/home.css" rel="stylesheet" />

<script src="/pages/home/home.js"></script>

</head>

<body>

<!-- The content that will be loaded and displayed. -->

<div id="dbItemtemplate"

class="itemtemplate"

data-win-control="WinJS.Binding.Template">

<div class="item">

<img

class="item-image"

src="#"

data-win-bind="src: image; alt: title" />

<div class="item-content">

<h3

class="item-title win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: title"></h3>

<h4

class="item-subtitle win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: year"></h4>

<h4

class="item-subtitle win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: status"></h4>

</div>

</div>

</div>

<div class="fragment homepage">

<header

aria-label="Header content"

role="banner">

<button

class="win-backbutton"

aria-label="Back"

disabled type="button"></button>

<h1 class="titlearea win-type-ellipsis">

<span class="pagetitle">My Collections!</span>

</h1>

</header>

<section aria-label="Main content" role="main">

<div

id="listView"

class="resultslist win-selectionstylefilled"

aria-label="Movies in my collection"

data-win-control="WinJS.UI.ListView"

data-win-options="{

itemTemplate: select('#dbItemtemplate'),

}">

}"></div>

</section>

</div>

</body>

</html>

Home.js

Home.js is where we write additional code that provides interactivity for our home.html page. This script file only implements the ready function. This function is called at the time of the page load. Inside this function we iterate through the IndexedDB Movies object store and store it into an array and will bind that array to the ListView, as shown in Listing 4-10. This page also has an itemInvoked function that is attached to the ListView and is called when an item is selected from the ListView. Once called, this function navigates the user to the MovieDetail.html (see Listing 4-11) using the WinJS.Navigation.navigate function. This function takes the detail page location and selected movie object as parameters.

Listing 4-10. Movies in the Collection Are Bound to the ListView Element

(function () {

"use strict";

WinJS.UI.Pages.define("/pages/home/home.html", {

// This function is called whenever a user navigates to this page. It

// populates the page elements with the app's data.

ready: function (element, options) {

var listView = element.querySelector(".resultslist").winControl;

var tapBehavior = listView.tapBehavior;

listView.tapBehavior = tapBehavior;

listView.oniteminvoked = this._itemInvoked;

var collection = new Array();

var txn = MyCollection.db.transaction(["Movies"], "readonly");

var movieCursorRequest = txn.objectStore("Movies").openCursor();

movieCursorRequest.onsuccess = function (e) {

var cursor = e.target.result;

if (cursor) {

var data = cursor.value;

var movieData = {

movieId: data.id

, title: data.title

, year: data.year

, thumbnail: data.image

, poster: data.poster

, status: data.status

};

var newMovie = MyCollection.Movie.buildMovie(movieData);

collection.push(newMovie);

cursor.continue();

}

else {

listView.itemDataSource = new WinJS.Binding.List(collection).dataSource;

}

}

},

_itemInvoked: function (args) {

args.detail.itemPromise.done(function itemInvoked(item) {

WinJS.Navigation.navigate("/pages/details/movieDetail.html"

, { movieDetail: item.data });

});

},

});

})();

Designing the Movie Detail Page

The MovieDetail.html page is redirected either from the home or search result page and displays the details of the selected movies from its previous page. MovieDetail.html also provides an option to add or edit the movie object. The markup of this page contains an HTML element that is bound to the properties of the Movie object using the WinJS data-win-bind property. This page also has two app bar buttons that allow us to save or delete the Movie object, as shown in Figure 4-5.

Listing 4-11. MovieDetail.html With HTML Elements to Display Selected Movie Details

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<title>movieDetail</title>

<!-- WinJS references -->

<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />

<script src="//Microsoft.WinJS.1.0/js/base.js"></script>

<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

<link href="/pages/details/movieDetail.css" rel="stylesheet" />

<script src="/pages/details/movieDetail.js"></script>

</head>

<body>

<div class="movieDetail fragment">

<header

aria-label="Header content"

role="banner">

<button

class="win-backbutton"

aria-label="Back"

disabled

type="button" />

<h1

class="titlearea win-type-ellipsis">

<span class="pagetitle">My Collection</span>

</h1>

</header>

<section

aria-label="Main content"

role="main">

<div

id="divDetail"

class="detailView">

<h3 id="title">Edit Movie</h3>

<br />

<!--Movie Image-->

<img

src="#"

data-win-bind="src: poster; alt: title" />

<!--Movie Title-->

<label>Title</label>

<input

id="txtTitle"

type="text"

data-win-bind="value: title Binding.Mode.twoway" />

<br>

<!--Movie Release Year-->

<label>Year</label>

<input

id="txtYear"

type="text"

data-win-bind="value: year Binding.Mode.twoway" />

<br>

<!--Movie Status-->

<label>Status</label>

<select

id="status"

data-win-bind="selected: status; value: status Binding.Mode.twoway">

<option value="Avaliable">Avaliable</option>

<option value="Lend Out">Lend Out</option>

<option value="Rented">Borrowed</option>

</select>

<br />

</div>

</section>

</div>

<!--App bar-->

<div

data-win-control="WinJS.UI.AppBar"

class="appBar"

id="appBar">

<!--Save Movie Button-->

<button

data-win-control="WinJS.UI.AppBarCommand"

data-win-options="{id:'saveButton', label:'Save', icon:'save',section:'global'}"/>

<!--Delete Movie Button-->

<button

data-win-control="WinJS.UI.AppBarCommand"

data-win-options="{id:'deleteButton', label:'Delete', icon:'delete',section:'global'}"/>

</div>

</body>

</html>

A978-1-4302-4993-1_4_Fig5_HTML.jpg

Figure 4-5.

The My Collections app displaying movie details along with the app bar for saving and deleting

MovieDetail.js

Here we bind the Movie object that is passed on to this page to the div element inside the page ready function. This function, shown in Listing 4-12, also sets the event handler for the save and delete button which in turn calls the saveMovie (see Listing 4-4) and deleteMovie (see Listing 4-5) functions in Movie.js .

Listing 4-12. Binds the Movie Object to the div Element for Adding or Deleting

WinJS.UI.Pages.define("/pages/details/movieDetail.html", {

// This function is called whenever a user navigates to this page. It

// populates the page elements with the app's data.

ready: function (element, options) {

// TODO: Initialize the page here.

movieDetail = options.movieDetail;

var src = WinJS.Binding.as(movieDetail);

var form = document.getElementById("divDetail");

WinJS.Binding.processAll(form, src);

document.getElementById("saveButton")

.addEventListener("click", doClickSave, false);

document.getElementById("deleteButton")

.addEventListener("click", doClickDelete, false);

if (movieDetail.fromSearch == true) {

document.getElementById("deleteButton").disabled = true;

document.getElementById("title").innerText = "Add to collection";

}

}

});

function doClickSave() {

MyCollection.Movie.saveMovie(movieDetail.id, movieDetail.title, movieDetail.year, movieDetail.image, movieDetail.poster, movieDetail.status);

WinJS.Navigation.back();

}

function doClickDelete() {

MyCollection.Movie.deleteMovie(movieDetail.id);

WinJS.Navigation.back();

}

Even though we bound the elements to the Movie object properties, the changes we make to the element are not reflected in the Movie object as WinJS doesn’t support two-way binding, but adding this functionality is quite easy thanks to the WinJS.Binding.initializer function. This function gets involved when binding is created. WinJS.Binding.initializer give access to both source and target objects and their properties, which allows them to subscribe to the target element’s events and push data to the source object, as shown in Listing 4-13.

Listing 4-13. Defining Two-Way Binding Using WinJS.Binding.initializer

WinJS.Namespace.define("Binding.Mode", {

twoway: WinJS.Binding.initializer(function (source, sourceProps, dest, destProps) {

WinJS.Binding.defaultBind(source, sourceProps, dest, destProps);

dest.onchange = function () {

var d = dest[destProps[0]];

var s = source[sourceProps[0]];

if (s !== d) source[sourceProps[0]] = d;

}

})

});

Once defined, we just need to apply the initializer to binding like the one shown in Listing 4-14.

Listing 4-14. Enabling Two-Way Binding in the HTML Element

<input type="text" data-win-bind="value: title Binding.Mode.twoway" />

Searching for Movies

The searchResults.html page is invoked when we do the app-level search using the Search charm. This page displays the matching results in a ListView element (see Listing 4-15). The result is shown in Figure 4-6. Displaying movie information in ListView is very similar to Home.html with one exception: Here we use two item templates, one for displaying movie details from the Rotten Tomatoes database and another from the IndexedDB that is dynamically switched using the JavaScript code in searchResult.js as shown in Listing 4-16.

Listing 4-15. Search Page Displays Results in a ListView When Invoked From Search Charm

<!DOCTYPE html>

<html>

<head>

<meta charset="utf-8" />

<meta name="ms-design-extensionType" content="Search" />

<title>Search Contract</title>

<!-- WinJS references -->

<link href="//Microsoft.WinJS.1.0/css/ui-dark.css" rel="stylesheet" />

<script src="//Microsoft.WinJS.1.0/js/base.js"></script>

<script src="//Microsoft.WinJS.1.0/js/ui.js"></script>

<link href="/css/default.css" rel="stylesheet" />

<link href="/pages/search/searchResults.css" rel="stylesheet" />

<script src="/js/data.js"></script>

</head>

<body>

<!-- This template is used to display each item in the ListView declared

below. -->

<!--ItemTemplate to display search from Rotten Tomatoes database -->

<div

id="onlineItemtemplate"

class="itemtemplate"

data-win-control="WinJS.Binding.Template">

<div class="item">

<img

class="item-image"

src="#" data-win-bind="src: image; alt: title" />

<div class="item-content">

<h3

class="item-title win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: title searchResults.title" />

<h4

class="item-subtitle win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: year searchResults.text" />

</div>

</div>

</div>

<!--ItemTemplate to display search from IndexedDB -->

<div id="dbItemtemplate"

class="itemtemplate"

data-win-control="WinJS.Binding.Template">

<div class="item">

<img

class="item-image"

src="#" data-win-bind="src: poster; alt: title" />

<div class="item-content">

<h3

class="item-title win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: title searchResults.title" />

<h4

class="item-subtitle win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: year searchResults.text" />

<h4

class="item-subtitle win-type-x-small win-type-ellipsis"

data-win-bind="innerHTML: status searchResults.text" />

</div>

</div>

</div>

<!-- The content that will be loaded and displayed. -->

<div class="searchResults fragment">

<!--Page Header-->

<header

aria-label="Header content"

role="banner">

<button

class="win-backbutton"

aria-label="Back"

disabled

type="button" />

<div class="titlearea">

<h1 class="pagetitle win-type-ellipsis" />

<h2 class="pagesubtitle win-type-ellipsis" />

</div>

</header>

<section

aria-label="Main content"

role="main">

<div

class="resultsmessage win-type-x-large">

No results match your search.

</div>

<!--Filter section-->

<div class="filterarea">

<ul class="filterbar"></ul>

<select class="filterselect" />

</div>

<!--ListView-->

<div

id="searchListView"

class="resultslist win-selectionstylefilled"

aria-label="Search results"

data-win-control="WinJS.UI.ListView" />

</section>

</div>

</body>

</html>

A978-1-4302-4993-1_4_Fig6_HTML.jpg

Figure 4-6.

My Collections app displaying search results

searchResult.js

The searchResult.js page does basically three things: gets the result data and binds it to the ListView, dynamically switches the template, and finally generates filters to the page so the user can filter the result by online and in collection.

Dynamic Template Change

Dynamic template change (see Listing 4-16) can be achieved by assigning the function itemTemplateFunction to the ListLiew itemTemplate property. This function returns a DOM element depending on the value of the Movie object isInCollection property.

Listing 4-16. Dynamically Switching the Template

ready: function (element, options) {

var listView = element.querySelector(".resultslist").winControl;

// listView.itemTemplate = element.querySelector(".itemtemplate");

listView.oniteminvoked = this._itemInvoked;

listView.itemTemplate = itemTemplateFunction;

this._handleQuery(element, options);

listView.element.focus();

}

function itemTemplateFunction(itemPromise) {

return itemPromise.then(function (item) {

var itemTemplate = document.getElementById("onlineItemtemplate");

if (item.data.isInCollection) {

itemTemplate = document.getElementById("dbItemtemplate");

};

var container = document.createElement("div");

itemTemplate.winControl.render(item.data, container);

return container;

});

}

Getting the Data

When we add the search contract page, Visual Studio includes the necessary code to fulfill the minimum requirements of the Search contract automatically. There are two functions in searchResults.js that are of interest to us, _handleQuery and _searchData (see Listing 4-17). The_handleQuery in turn calls the function _searchData. In the _searchData function we populate a WinJS.Binding.List with the search data by calling the methods loadFromDB and loadSearchResult. Apart from these two functions, we also call _generateFilters and the _populateFilterBarfunctions.

Listing 4-17. Handling Search Query and Loading the Result in an Array

_handleQuery: function (element, args) {

var originalResults;

this._lastSearch = args.queryText;

WinJS.Namespace.define("searchResults"

, {

markText: WinJS.Binding.converter(this._markText.bind(this))

});

this._initializeLayout(element.querySelector(".resultslist").winControl

, Windows.UI.ViewManagement.ApplicationView.value);

this._generateFilters();

this._searchData(args.queryText, element, this);

},

// This function populates a WinJS.Binding.List with search results for the

// provided query.

_searchData: function (queryText, element, object) {

var originalResults;

originalResults = MyCollection.Movie.loadFromDB(queryText);

MyCollection.Movie.loadSearchResult(queryText).done(

function (result) {

for (var i = 0; i < result.length; i++)

{

originalResults.push(result[i]);

}

originalResults = new WinJS.Binding.List(originalResults);

if (originalResults.length === 0) {

document.querySelector('.filterarea').style.display = "none";

} else {

document.querySelector('.resultsmessage').style.display = "none";

}

object._populateFilterBar(element, originalResults);

object._applyFilter(object._filters[0], originalResults);

return originalResults;

});

}

});

Generating Filters

The filters are created in the _generateFilters function. Depending on the value of the isInCollection property, we add two filters for the search result, as shown in Listing 4-18: one to showcase the Rotten Tomatoes matches and the other to show the matches from the Indexed database.

Listing 4-18. Creating Filters for the Search Result

generateFilters: function () {

this._filters = [];

this._filters.push(

{

results: null

, text: "All"

, predicate: function (item) {

return true;

}

});

this._filters.push(

{

results: null

, text: "Online"

, predicate: function (item) {

return item.isInCollection === true;

}

});

this._filters.push(

{

results: null

, text: "In My Collection"

, predicate: function (item) {

return item.isInCollection !== true;

}

});

}

Now with all the codes in place, when we run the My Collections app, at first it shows an empty Start screen, but we can search for movies using the Search charm and add them to our movie collection. Once they are added, the Start screen will look like the one shown in Figure 4-1.

Ideas for Improvement

The My Collections app can be worked on and improved to make it a fully functional inventory app. Here are some of the features that can be added:

· You could add an option to search for books or games using third-party APIs like http://www.thegamesdb.net/ and Google Books.

· Right now we can only add a movie to the collection by searching the Rotten Tomatoes database. You could provide and option to add ad hoc entries.

· A barcode scan option would allow users to enter movies by quickly scanning the barcodes on the movie cases.

· Back up your movie collection in SkyDrive or Dropbox.

· Add the capability for advanced search features and filters.

· You could categorize the items into books, movies, games, and so on.

Conclusion

In this chapter we learned to use IndexedDB as a local storage option by creating a Windows 8 JavaScript app. As we saw, the IndexedDB API is a simple but powerful option for storing data locally, even though it is a little bit different from the relational database.

There are also some IndexedDB wrappers like IDBWrapper ( https://github.com/jensarps/IDBWrapper ) that can be used to ease the use of IndexedDB. In the next chapter, we continue to explore local storage options by learning to use Jet API and Application Storage.

Vinodh KumarBeginning Windows 8 Data DevelopmentUsing C# and JavaScript10.1007/978-1-4302-4993-1_5

© Vinodh Kumar 2013