Hour 11. Storing Data Locally


What You’ll Learn This Hour:

• Using DOM local storage

• Using DOM session storage

• Making use of the WinJS sessionState object

• Using the WinRT application data

• Saving a canvas contents to an image file

• Using the Indexed Database API


Storing information apps can use is a common need. This can include simple settings for apps or a complete database with thousands of records. During this hour, we look at the different ways to store data locally. We also look at ways to retrieve the data and do work on files we included in our app package.

Knowing the different available options and which storage type to use is important. We discuss application roaming and what data we can store to support that scenario. We also provide a demo on writing the contents of a canvas into an image on disk.

Working with DOM Local Storage

DOM storage enables us to store small textual web site content. It enables us to store key/value pairs much as we can cookies, but with more disk space. The size limit of cookies is 4KB and only 20 different key/value pairs. For local storage, it is roughly 10MB. Local storage data never expires. Using DOM local storage is pretty trivial.

Data can be removed by calling clear or looping through the collection and calling removeItem for each item.

Glancing at Session Storage

In addition to local storage, we have access to session storage. The HTML5 standard is to hold data that is available for only the current session. In the web world, if a user has multiple tabs open accessing the same site, the data is shared across all tabs. Windows 8 has no concept of multiple tabs in regard to our apps. If we have an app with multiple page fragments, we can store the data in memory, and each page fragment then has access to the data. However, if we did a full redirect for some reason (not a best practice), we could store the shared data in session storage so that subsequent pages could access the value. This provides little value when creating Windows Store apps properly using Page Controls, so we don’t provide any demos of this. Functionally, it works the same way as with window.localStorage, except that the data is removed after the app closes. With the other options available, session storage should not be used in a Windows Store app.

Looking at the sessionState Object

WinJS provides a sessionState object under the WinJS.Application namespace. In general, the sessionState object can be considered functionally similar to the session storage provided in the DOM. However, the important feature that the sessionState object provides is the capability to persist the data automatically when an app is suspended and then retrieve those stored values when an app is activated again. The sessionState object should be used for application data that is accessed frequently and needs to be persisted when an app is suspended. We discuss the app life cycle events and other ways to tie into suspended apps during Hour 15, “Handling Our App Life Cycle Events.” For now, the sessionState object, which is part of WinJS, enables us to get this functionality with no additional effort.

Appropriate content to be placed in session state includes navigational information and form data entered by the user. App settings and user preference values should not be stored in session storage. The data is meant to be stored only for a single session. For example, if we had a messaging app and the user was entering the message but then switched to another app, we would want to make sure the data was intact when they come back to our app. Likewise, if our app terminated while the user was doing something else, we would want to display that data when the user switched back to our app. We see how to utilize the sessionState object during Hour 15.

The sessionState object should be used to store values that change often. Even if something needs to be persisted through multiple app launches, if it changes frequently during the use of the app, using the sessionState object and then periodically writing to permanent storage might make sense. It reduces the number of expensive file operations the app might need to do.

Working with WinRT Local Settings

The Windows Runtime provides a mechanism for storing app settings. The Windows Runtime localSettings object can be accessed by:

var localSettings = Windows.Storage.ApplicationData.current.localSettings;

To see how to work with local settings, let’s create a new Blank App project called WinRTLocalSettingsExample. We can replace the body contents in default.html with the following markup:

<body>
    <div>Content from local settings:</div>
    <p>Content goes here</p>

    <input id="dataToSave" type="text" autofocus="autofocus" />
    <button id="btnSave">Save Data</button>

    <div>
        <button id="btnDelete">Delete Container</button>
    </div>
</body>

In the default.js file, we can add the following ready event to the end of the file:

WinJS.Utilities.ready(function () {

    var appData = Windows.Storage.ApplicationData.current;
    var localSettings = appData.localSettings;
    localSettings.values["d"] = "this was stored directly on localSettings";

    var div = document.createElement("div");
    div.innerText = "Data read from local settings: " + localSettings.values["d"];
    document.querySelector("body").appendChild(div);

});

If we run the app, we see at the bottom of the page that the data was read from the local settings. Besides setting text on the localSettings directly, we can set data in a container. localSettings can have multiple containers, and we can group our data by container, if desired. Let’s add code to hook up the buttons we have on the form. This functions much like the first example we created, except that it also has a Delete button to delete the container. Add the following to the end of the ready function:

var sampleContainer = "userPreferences";

displayData(sampleContainer);

function displayData(container) {

    //read setting
    var hasContainer = localSettings.containers.hasKey(container);
    if (hasContainer) {
        if (localSettings.containers.lookup(container)
                                    .values.hasKey("data")) {

            var setting = localSettings.containers.lookup(container)
                .values["data"];

            var p = document.querySelector("p");
            p.innerHTML = toStaticHTML(setting);
        }
    }
};

var btnSave = document.getElementById("btnSave");

btnSave.onclick = function (evt) {
    var dataToSave = document.getElementById("dataToSave");


    createData(sampleContainer);

    displayData(sampleContainer);
};

function createData(container) {

    if (localSettings.containers.hasKey(container)) {
        localSettings.containers.lookup(container)
            .values["data"] = dataToSave.value;
    }
    else {

        var createdContainer = localSettings.createContainer(container,
            Windows.Storage.ApplicationDataCreateDisposition.always);

        localSettings.containers[container].values["data"] = dataToSave.value;
    }
}


var btnDelete = document.getElementById("btnDelete");

btnDelete.onclick = function (evt) {
    //delete container
    localSettings.deleteContainer(sampleContainer);
}

To begin, we call out to displayData, passing in a container called userPreferences. This can be any string, and we can group different items into different containers by using a different string. The displayData function checks to see if the container actually exists. If not, nothing else happens. If the container does exist, the data is looked up in the container. If the container has that piece of data, we store the data into our variable called setting. We then get a reference to the p element on the page and set the value stored in the setting variable to the innerHTML of the p element.

The next section of code wires up the btnSave button’s click event. Inside the event, we get a reference to the dataToSave input text field. We then call out to createData to store the data. We call the displayData function we just covered to display the data that was stored. The createData function accepts the container id as a parameter. It then does the same check to see if the container exists in the localSettings. If so, the data is simply updated. If it wasn’t found, the container is created and then the data on the container is set. When the container is created, we use the ApplicationDataCreateDisposition.always setting. The other option is existing. The always setting activates the container even if it isn’t present. The existing flag does not activate the container if it doesn’t exist. The last bit of code simply hooks up the btnDelete button to delete the container. When we run the app, we can put a value into the textbox and get the same value back. See Figure 11.1 for the results.

Image

Figure 11.1. Using optional containers, we can store the app’s local settings and roaming settings.

The local settings data store is best used for storing the user preferences and configuration settings, such as the preferred background color for the app. It is best used for data that does not change frequently. When a value changes, the data store is written to disk. The name of each setting must be 255 characters or shorter in length, and the data for each setting can be up to 8K in size.

Roaming Settings

In addition to localSettings, Windows.Storage.ApplicationData.current enables us to connect to the roaming settings. Roaming settings is accessed the exact same way, so we can add the following code to the ready function:

var roamingSettings = appData.roamingSettings;
if (!roamingSettings.values["d"]) {
    roamingSettings.values["d"] =
        "this was stored directly on roamingSettings" + new Date();
}
var rdiv = document.createElement("div");
rdiv.innerText = "Data read from roaming settings: " + roamingSettings.values["d"];
document.querySelector("body").appendChild(rdiv);

If we ran the app on two different machines, the date set in the roamingSettings would be the same across both machines. This is because each user is given free Microsoft SkyDrive space, which is associated to his or her Microsoft Account. Each Windows Store app also has access to a small portion of the space. The code is identical if it is local settings or remote settings, but the ultimate location of the data differs. It is a good practice to set the setting data in the roamingSettings so that the user experience will be the same on both systems. If the user chooses background X on machine A, when that user goes to machine B, the background should automatically update.

We have seen that we don’t want to set this type of data frequently, but we really need to handle the scenario of a value that changes frequently, such as the position in a movie the user is watching or the scroll position in the article the user is reading. We need to account for this scenario in which the user is actively using the app and modifying a key item that we need to know about. When that user goes to another machine, the app should provide the exact location they were expecting.

Fortunately, Microsoft has provided a specially named value in roamingSettings, called HighPriority. The following code sets this special data that can be updated frequently and needs to be roamed:

roamingSettings.values["HighPriority"] = currPosition;

Sometimes we need to make sure that pieces of data are set together. If we don’t have all the pieces of the information, the other pieces we do have are useless. When data is tightly coupled and should always roam together, we can use a CompositeSettingValue. This object enables us to effectively create a property bag that we can then assign to a setting. The HighPriority value accepts a CompositeSettingValue. To use it, we write the following code:

var composite = new Windows.Storage.ApplicationDataCompositeValue();
composite.insert("someNumber", 1);
composite.insert("someText", "string");
roamingSettings.values["HighPriority"] = composite;

We could also write it this way:

var composite = new Windows.Storage.ApplicationDataCompositeValue();
composite["someNumber"] = 1;
composite["someText"] = "string";
roamingSettings.values["HighPriority"] = composite;

When we want to read the data on the other side, we can read it using the following code:

var composite = roamingSettings.values["HighPriority"];
if (composite) {
    var someNum = composite["someNumber"];
    var someText = composite["someText"];
}

If we utilize a composite as a HighPriority setting, we need to make sure that the data is less than 8KB. If it is not, no exception is raised, but it is treated just as a regular setting instead of the special high-priority setting.


Note

CompositeSettingValue can also be used for local settings. The data can be up to 64KB.


The Windows.Storage.ApplicationData class has an event called DataChanged that fires when the app is running and Windows updates the application data. This can occur when data from another PC was delayed but then arrived after the app launched on another machine. This event enables us to obtain the data and refresh our app appropriately. This event is not meant to be a real-time communication mechanism between PCs; it is optimized for consumers who use a single PC at a time and switch between them (such as at work and at home).

Besides settings that we can store locally and remotely, we can store application data locally, remotely, and even temporarily. The next section explores how that works.

Working with WinRT Local Storage

This WinRT storage API includes multiple ways to talk to the local file system. So far, we have seen the DOM’s local storage, the DOM session storage, WinJS session state, and WinRT local settings. Now we look at WinRT’s local storage. We can access local storage by calling the WinRT APIs directly:

var applicationData = Windows.Storage.ApplicationData.current;
var localFolder = applicationData.localFolder;
var roamingFolder = applicationData.roamingFolder;
var temporaryFolder = applicationData.temporaryFolder;

Just as we were able to access the localSettings object, we can get access to localFolder, roamingFolder, and temporaryFolder. We can then write directly to files in those folders. Of course, we can also read from those files. We can write text files or binary files. Local storage is where we should store large files. Roaming storage can hold only a certain amount of data (currently, Microsoft has this set to 100KB), so if we want to roam large files, we need to utilize our own web server. But if we just have small data we need to make sure is available across the user’s devices, we can utilize the roaming storage. If we needed to use our own storage, we would still want to utilize the 100KB to store the location of the data the app needs to retrieve.

If we try to store more than the ApplicationData.RoamingStorageQuota amount (currently 100KB), nothing gets stored. No exceptions are thrown, but nothing is sent to the cloud until the amount of data we are trying to store is less than the per-app quota.

Another way we can access these folders is through WinJS:

var localFolder = WinJS.Application.local.folder;
var roamingFolder = WinJS.Application.roaming.folder;
var temporaryFolder = WinJS.Application.temp.folder;

However, the benefit is that WinJS has also created helper functions to help access read and write data to these folders. To write a text file to the folder without the helper functions, we do the following:

function writeData(folder) {
    return folder.createFileAsync("dataFile.txt",
                Windows.Storage.CreationCollisionOption.replaceExisting)
        .then(function (sampleFile) {
            var timestamp = formatter.format(new Date());

            return Windows.Storage.FileIO.writeTextAsync(sampleFile, timestamp);
        })
}

The code calls createFileAsync on the folder we passed in (local, roaming, or temporary). The name of the file it is creating is dataFile.txt. If the file is present, the code just overwrites it. Other options are failIfExists, openIfExists, and generateUniqueName. When the file is created, sampleFile is passed to the promise, actually creates the data (the date), and writes it to the file. The promise of doing the work is returned to the calling function.

To accomplish the same goal using the WinJS helper, we write this:

WinJS.Application.local.writeText("anotherFile.txt",formatter.format(new Date()));

A lot less code is used when we need to write to a text file. The formatter used in both examples follows:

var formatter = new Windows.Globalization.DateTimeFormatting
                                            .DateTimeFormatter("longtime");

If we need to write binary data, we need to use the long form because there are no helper methods for writing to nontext files. We could still use WinJS, but we would talk directly to the folder, just as with WinRT.

To write binary data to the file, we must use either writeBufferAsync or writeBytesAsync. If we want to write multiple text lines to a file instead of setting all the text in a single call, we use the final function, writeLinesAsync.

The counterparts to the write functions are the read functions. For WinRT, we write this:

function readData(folder, fileName) {
    folder.getFileAsync(fileName)
        .then(function (sampleFile) {
            return Windows.Storage.FileIO.readTextAsync(sampleFile);
        })
});

For WinJS, we write this:

WinJS.Application.local.readText("anotherFile.txt")

Under Hour11WinJSApplicationLocalExample is a project that writes two files to local storage. One file is written using the WinJS helper function, and one uses the WinRT API directly. Then each file is read twice, once by the WinRT API and once by the WinJS API. Figure 11.2 shows the output of the sample app.

Image

Figure 11.2. The same files can be read from and written to by WinRT and WinJS.

Reading Files from the App’s Installed Location

We can include files in our application package, located with our app on disk. We have the capability to pull files from our app’s installation folder.

During Hour 10, “Binding Data to Our Apps,” we created a data.js file and placed it in the /js folder. We didn’t actually include the JavaScript file in a script element on the page. Instead, we used WinJS.xhr to pull it in at runtime. We did this to simulate pulling the data from anywhere; however, it also demonstrated how to load a resource at runtime when it is included in an app package by using a relative path. As discussed during Hour 2, “Trying Out File | New | Project,” we could have also used a fully qualified path:

WinJS.xhr({ url: "ms-appx://apppackage/js/data.js" }).then(function (peeps) { ...

Here, apppackage is the package name set in the application manifest file. This defaults to a globally unique identifier (GUID), but can be replaced by another unique name. The application package name can be omitted, and this could also be shortened as follows, signifying this application package:

WinJS.xhr({ url: "ms-appx:///js/data.js" }).then(function (peeps) { ...

Similarly, if we wanted the logo image to be placed inside an img element, we could write the following:

<img src="ms-appx:///images/logo.png" />

To see this in action, let’s create a new Blank App project called InstalledLocationExample. We can add the following markup to the default.html page (by replacing the existing body element and contents):

<body>
    <div>Image loaded from: images/logo.png</div>
    <img src="images/logo.png" style="border: solid 2px red;" />
    <div>Image loaded from: ms-appx:///images/logo.png</div>
    <img src="ms-appx:///images/logo.png" style="border: solid 2px green;" />
</body>

The markup demonstrates how the markup has access to files stored in our app package. Let’s create the ready function inside the default.js file and include the previously seen displayData helper function:

WinJS.Utilities.ready(function () {

    function displayData(data) {
        var d = document.createElement("div");
        d.innerText = data;

        document.querySelector("body").appendChild(d);
    }
}

The following code shows how to display the app’s installed location path:

var pkg = Windows.ApplicationModel.Package.current;
var installedLocation = pkg.installedLocation;

displayData("Installed Location: " + installedLocation.path);

We can add this code directly after the displayData function, to return the actual physical path on disk where the app is installed. We include files in our app package so we can load them at runtime. To demonstrate this, let’s create a new text file on the root of our solution. We can call it textfile.txt and put a couple lines of text in it. When the app is installed, textfile.txt will be located on the root folder. We could display the text from that file by adding the following code:

//Load text file from our local app package
installedLocation.getFileAsync("textfile.txt")
.then(function (textFileBlob) {
    var reader = new FileReader();
    reader.readAsText(textFileBlob);
    reader.onload = function (evt) {
        var fileString = evt.target.result;
        displayData(fileString);
    }
});

This uses our physical path location and gets a handle on the text file. We can use standard HTML5 to work with the file by instantiating a new FileReader object and then passing the blob into the readAsText function. We then set up an event so that when the file has finished loading, we are notified. Our onload event handler simply stores the text information (from evt.target.result) into a string, and we display the string.

A more complex example involves reading in the logo file that is included in our project under imageslogo.png. Let’s add the following code to the ready function:

installedLocation.getFileAsync("images\logo.png")
    .then(function (logoFile) {
        //work with logoFile
        readInImage(logoFile);
    });

To finish the logo example, we add the readInImage function. It reads in the image off of disk and then creates a canvas element and puts the contents of the image on the canvas:

function readInImage(storageFile) {
    if (storageFile) {
        var loadStream = null;
        var imageProperties = null;
        storageFile.openAsync(Windows.Storage.FileAccessMode.read).then(
            function (stream) {

                loadStream = stream;

                // since we return the promise,
                //it will be executed before the following .done
                return storageFile.properties.getImagePropertiesAsync();
            }
        ).done(
            // do work
        );
    }
}

The function makes sure that we actually have an object passed in (storageFile). We then call openAsync on the file passing in the FileAccessMode of read. After that has been opened, the promise obtains the actual file stream. We set the stream to the variable loadStream we declared outside the promise chain and then return the image properties. By returning the value, to the promise complete before the final chain call of done is executed.

We can replace the // do work comment with the following code to read in the image and put the contents on the canvas:

function (imageProperties) {

    var canvas = document.createElement("canvas");
    //size of image is 150 plus we want 2px image around all sides
    var w = 154;
    var h = 154;

    canvas.width = w;
    canvas.height = h;

    var url = URL.createObjectURL(storageFile, { oneTimeOnly: true });

    displayData("url: " + url);

    var imageObj = new Image(150, 150);
    imageObj.onload = function () {

        //clear canvas to an orange background and white border
        var ctx = canvas.getContext("2d");

        ctx.fillStyle = "white";
        ctx.fillRect(0, 0, w, h);

        ctx.fillStyle = "orange";
        ctx.fillRect(2, 2, w-4, h-4);

        //draw the image on the canvas
        ctx.drawImage(imageObj, 2, 2, w-4, h-4);
    };

    imageObj.src = url;

    // input stream is IClosable interface and requires explicit close
    loadStream.close();

    document.querySelector("body").appendChild(canvas);
},
function (e) {
    displayData("Load failed.");

    // if the error occurred after the stream was opened,
    //close the stream
    if (loadStream) {
        loadStream.close();
    }
}

We create the canvas element dynamically. The logo’s dimensions are 150×150, but we want a 2-pixel border around it to match the other images we have displayed, so we set the canvas width and height to 154. The following call creates a one-time-use URL that we can assign to an element’s src (or similar) attribute:

var url = URL.createObjectURL(storageFile, { oneTimeOnly: true });

The URL returned is a blob URL. We discuss blobs in the next section. A blob is a W3C standard way of working with files. In fact, the File object inherits from the Blob object. The oneTimeOnly flag being set on the property bag is important because we are using only the image and want it to be cleaned up immediately. We would need to call the URL.revokeObjectURL function to clean up the resource.

After displaying the url, we instantiate a new Image object. We pass in the width and height of the image (150, 150) and then, just as we did with the text file, we hook up to the onload event. Inside the image load event handler, we get the context of the canvas and fill the entire 154×154 rectangle with white. We then offset it by 2 pixels in the x and y positions and fill in the rest with orange. We fill the width and height minus 4 because we want the white 2-pixel border on the right and bottom as well. Finally, we draw the actual logo on the exact same region as we drew the orange rectangle. Because the default logo is mostly transparent, we will see the orange background shining through; this allowed us to basically change the look of our logo.

Outside the load event handler, we set the url to the source that will kick off the event we just handled. Finally, we close out the stream and add the canvas to the body of the document. In the error handler, we display that an error happened and close the stream.

Let’s change the name of our package. This is done in the package manifest file under the Packing tab. The Package name defaults to a GUID, but we can change it to CustomAppPackageName. Now we add the final bit of code to the ready function:

addImage();

function addImage() {

    var i = document.createElement("img");

    var image = "ms-appx://CustomAppPackageName/images/logo.png";

    displayData("image from: " + image);

    i.src = image;
    i.style.border = "solid 2px blue";

    document.querySelector("body").appendChild(i);
}

This code simply demonstrates how we can set the image from the disk to the source of the image tag. This is much more simplistic than the canvas approach we just took, but it doesn’t provide the same flexibility. This approach also shows how we can load the resource by explicitly stating the package name. When we run the application, we can see the results in Figure 11.3.

Image

Figure 11.3. Files can be loaded from the local app package in multiple ways.

Saving Images to Disk As Binary Large Objects

A binary large object (blob) object represents raw binary data that is immutable (cannot be changed). The Blob object can be sliced into multiple Blob objects by using the slice method, which grants access to a range of bytes within the blob. The File object inherits from the Blob object. We saw the Blob object in action when we loaded and read in the text file in the previous section.

In the last section, we saw how to take a file stream (from disk, in that case) and display the image on a canvas. Now we go the other way: We display the contents on the canvas as an image on disk.

To start, we need to copy the InkExample project from Hour 8, “Working with Multitouch and Other Input.” Let’s do a little housekeeping on the project before we add new code. We first cut all the code after app.start until the end of the module. Then we create the ready function outside the module (self-executing anonymous function). Finally, we paste the code we just cut into the ready function. So the section of code should now look like this:

        ...
    app.start();

})();

WinJS.Utilities.ready(function () {

    var canvas;
    var ctx;
        ...
});

Next, we need to find the following line

document.addEventListener("DOMContentLoaded", init, false);

and replace it with this:

init();

We have put all the code in the ready function that happens after the DOM content is loaded, so the event would never fire because we are registering for it after the fact. Removing the event listener and directly calling the init function achieves the same behavior. We should be able to run the app and still have the same functionality we left with in Hour 8. If not, we didn’t do the cut-and-paste operation correctly.

Instead of setting up another gesture to save the current contents to an image, we create a button for this demo project. Let’s add the following button to our default.html file immediately after the results input textbox:

<button id="btnSave">Save</button>

Now we can style the button and modify the style of the textbox by replacing the #results style with the following CSS rules:

#results { width: 1000px; }
#btnSave { width: 250px; margin-left: 10px; }

We knocked down the width of the results textbox and made the button fit beside the textbox (with a little spacing) on 1,280-width screens.

Now we can write the JavaScript to hook up the Save button and write the contents of the canvas into a file on disk. All the code we will write needs to be put inside the ready function. To start, let’s hook up an event handler to the button itself:

var btnSave = document.getElementById("btnSave");
btnSave.addEventListener("click", writeOutImage, false);

Next, we can declare a variable where we store the name of the file we will be saving and start the writeOutImage function:

var storageFile = "temp.png";
function writeOutImage(evt) {

We can initialize some variables inside of the click handler:

var canvas = document.getElementById("canvas");
var blob = canvas.msToBlob();
var saveStream = null;

var sto = Windows.Storage;
var cco = sto.CreationCollisionOption;
var appData = sto.ApplicationData.current;
var folder = appData.localFolder;
    //appData.temporaryFolder;
    //appData.roamingFolder;

We obtained a reference to the canvas we will be saving to an image file.

We then create a blob variable by calling the msToBlob function on the canvas element. This returns the immutable raw binary data that makes up the pixels on the canvas. We use this to create the image file. Next, we created some variables that we will be using in the function. Some are simply to shorten the namespaces so that the lines of code don’t wrap in the book.

Now we can finish the writeOutImage function, which is simply a chain of promises:

folder.createFolderAsync("temp", cco.openIfExists)
    .then(function (createdFolder) {
        return createdFolder.createFileAsync(storageFile, cco.generateUniqueName);
    })
    .then(function (file) {

        return file.openAsync(sto.FileAccessMode.readWrite);

    }).then(function (stream) {
        saveStream = stream;

        return sto.Streams.RandomAccessStream.copyAsync(
            blob.msDetachStream(), saveStream);
    }).then(function () {
        // since we return the promise, it will be executed
        //before the following .done
        return saveStream.flushAsync();
    },
    function (e) {
        // errors occurred during saveAsync are reported as catastrophic.
        // for us it just means we could not save the file,
        //so we throw a different error.
        throw new Error("saveAsync");
    }).done(function (result) {
            // print the size of the stream on the screen
            displayStatus("File saved!", "Success");

            // output stream is IClosable interface and requires explicit close
            saveStream.close();
        },
        function (e) {
            displayStatus("save " + e.toString(), "Error");

            // if the error occurred after the stream was opened, close the stream
            if (saveStream) {
                saveStream.close();
            }
        })
    }

The first task we complete is to create a folder called temp in the local storage folder. If the folder exists, we open it. When that completes, we create the file temp.png. If the file exists, we keep creating unique files. Maybe we want to replace the existing file. If so, we use cco.replaceExisting. After generating the unique filename in the temp folder, the next promise in the chain opens the newly created file for readWrite access. We then return the stream to the next promise. We store the stream in the variable we declared earlier because we need to access this in the following chains. After storing the stream in our saveStream variable, we start copying the stream of bytes from our blob to saveStream. When that process is done, we flush the bytes to the stream. Finally, in the done method, we close the stream and display the status in the console (in Visual Studio) that the file was saved successfully. If an error happened, a message dialog pop-up appears to the user.

Finally, we can add the displayStatus function to display success or failure. Success writes to the console log; failure brings up the pop-up window:

function displayStatus(message, state) {

    if (state === "Error") {
        var msg = new Windows.UI.Popups.MessageDialog(message, state);
        msg.showAsync();
    }

    else {

        console.log(message);
    }
}

Finally, we need to rename the package name in our manifest file. We can set the package name to InkExample. Now when we run the program, we can write (or draw) something and then click the Save button to save the file. The file gets sent to the UsersNameAppDataLocalPackagesInkExample_xxxxLocalState emp folder.

For example, the temp folder on my machine is located at C:UsersChadAppDataLocalPackagesInkExample_qwnwt6twhqbv0LocalState emp.

Understanding the Lifetime of Application Data

It is important to understand that, although we have access to the Local, Roaming, and Temporary folders to store data, some data should not be stored there. If an app is uninstalled, the Packages folder for that app is removed. This removes the LocalState, RoamingState, and TempState folders we would be writing to. The user then does not have access to any of these files. Those folders are to be used to store application data. To store data that the user cares about, it is more appropriate to store them in the user’s library. We discuss how to do that during Hour 14, “Using the App Bar and Pickers.”

LocalState should be used to hold data our app needs. A large amount of space is available. RoamingState works for very small amounts of data—100KB that needs to be available across the user’s devices. TempState is similar to LocalState, but the system automatically clears the contents. LocalState and RoamingState last until the app is uninstalled or they are deleted by the app or the user. TempState is cleared out when the system deems best or when the user clears the data using the Disk Cleanup tool.


Tip

Although LocalState enables us to utilize a large amount of space compared to RoamingState, that space isn’t unlimited. It depends on how much space is on disk. On some smaller form factors, space could be limited. It is always wise to test for success when storing data.


When we used the Windows Runtime to store local settings using Windows.Storage.ApplicationData.current.localSettings, the actual location of the data is stored under the UsersNameAppDataLocalPackagespackagename_xxxxSettings folder in a file called settings.dat. Settings also stay around for the lifetime of the app. If it gets uninstalled, all the data is lost. Roaming data that makes it to SkyDrive remains for “a reasonable amount of time,” or a couple weeks.

Working with the Indexed Database API (IndexedDB)

The last piece of functionality we discuss regarding saving data locally is the Indexed Database API (IndexedDB). This nonrelational database simply stores information in files as key/value pairs. It facilitates storing large amounts of data. However, this database shouldn’t be used for storing a few values. Instead, the settings or regular files in local storage should be used. IndexedDB should be used when key–value data management for a large record set is needed.

IndexedDB is a W3C standard way of allowing browsers to store data. It is a database in the browser. This means it is available when creating Windows Store apps using JavaScript. Data is not stored in tables; instead, data is stored in object stores. The objects stored are simply JavaScript objects.

Every record consists of a key path and a corresponding value. The value can be a basic type, such as a string, or it could be entire JavaScript objects or arrays. Indexes are supported, to accommodate faster retrieval of data.

IndexedDB is available through the DOM and is part of the windows object. To start working with the IndexedDB object, we need to open it with the following code:

function initDatabase() {
    var request = indexedDB.open(databaseName, version);
    request.onsuccess = function (evt) {
        db = request.result;
    };

    request.onerror = function (evt) {
        console.log("IndexedDB error: " + evt.target.error.name);
    };

    request.onupgradeneeded = function (evt) {
    }
};

We store a request object that is returned from the IndexedDB open function. We pass in a string containing the database name, along with the version of the database. If the version passed in is larger than the current version of the database (or if the database doesn’t exist yet), then it kicks off the onupgradeneeded event, which is stubbed out at the bottom. If it doesn’t need to upgrade or create the database and no errors occur, the onsuccess event is fired.

The onupgradeneeded event handler follows:

request.onupgradeneeded = function (evt) {
    db = evt.currentTarget.result;
    var objectStore = db.createObjectStore(objectStoreName, {
        keyPath: "id",
        autoIncrement: true
    });

    objectStore.createIndex("customerNumber", "customerNumber",
        {  unique: true });
    objectStore.createIndex("fullName", "fullName",
        {  unique: false });
    objectStore.createIndex("firstName", "firstName",
        {  unique: false });
    objectStore.createIndex("lastName", "lastName",
        {  unique: false });
    objectStore.createIndex("email", "email",
         {  unique: true });
    objectStore.createIndex("invoiceDate", "invoiceDate",
         {  unique: false });
}

So onsuccess and onupgradeneeded both return a handle to the database. The onupgradeneeded event handler needs to create the object store it wants to work with. A database can have more than one object store. The createObjectStore function takes in a string value containing the name of the store, along with options object. The options property bag takes in the keyPath and autoIncrement parameters. A key path defines how to extract a key from a value. It can be an empty string, a JavaScript identifier, or multiple JavaScript identifiers separated by periods. If an empty string is used, a key generator (like autoIncrement) must be provided.

Finally, indexes are created. Indexes can have the unique property set to true or false. Indexes help speed up the retrieval process. The first parameter is the name of the index itself. The second is the name of the property the index is associated to. The third parameter is the options object, which can take properties of unique or multiEntry.

To recap the process so far, the database attempts to be opened; if the version number is less than the version number we want it to be, the onupgradeneeded event is fired. (New databases have a version number of 0, and the minimum number that can be passed into the open function is 1, so new databases always kick off the onupgradeneeded event.) The onupgradeneeded event creates any indexes. If an upgrade isn’t needed, the success event is raised and we can store a handle to the database.

If we want to add a record to the database, we use the following code:

var transaction = db.transaction(objectStoreName, "readwrite");
objectStore = transaction.objectStore(objectStoreName);
var request = objectStore.add({
    customerNumber: customerNumber.value,
    fullName: fullName,
    firstName: firstName.value,
    lastName: lastName.value,
    email: email.value,
    invoiceDate: invoiceDate.winControl.current
});
request.onsuccess = function (evt) {
    // clear controls

    firstName.value = "";
    lastName.value = "";
    fullName.value = "";
    customerNumber.value = "";
    email.value = "";
};
request.onerror = handleError;

All IndexedDB actions must happen within a transaction. The IDBTransaction object can be created in readonly, readwrite, or versionchange modes. As the name implies, readonly mode cannot make changes to the data, and multiple reads can occur simultaneously. readwrite is allowed to read, modify, and delete data from existing data stores. However, object stores and indexes cannot be added or removed. versionchange functions similarly to readwrite, but it can also create and remove object stores and indexes. However, this mode is used internally during the upgradeneeded event and can’t be manually created.

When we have a transaction, we can get the object store where our data is stored. An object store can roughly be thought of as a table in a relational database. We actually perform the operation on the object store. All actions return an IDBRequest object. We take the request and can set the onsuccess and onerror handlers. The actual add function that returned this request takes in the object we are storing in the object store. When the add succeeds, we simply clear out the values. The previous example assumes that controls are visible on the page where the user is setting the data.

To do a read on the data, the following code is used:

var id = document.getElementById("txtID").value;
var transaction = db.transaction(objectStoreName, "readonly");
objectStore = transaction.objectStore(objectStoreName);
var request = objectStore.get(parseInt(id));

request.onsuccess = function (evt) {
    // show it
    var record = evt.target.result;

    var firstName = document.getElementById("txtFirstName");
    var lastName = document.getElementById("txtLastName");
    var fullName = firstName.value + " " + lastName.value;
    var customerNumber = document.getElementById("txtCustomerNumber");
    var invoiceDate = document.getElementById("InvoiceDate");
    var email = document.getElementById("txtEmail");

    firstName.value = record.firstName;
    lastName.value = record.lastName;
    customerNumber.value = record.customerNumber;
    invoiceDate.winControl =
        new WinJS.UI.DatePicker(invoiceDate, {
            current: record.invoiceDate
        });
    email.value = record.email;
};

request.onerror = handleError;

As with the add function, we start with a transaction. This time, however, we are using the readonly file, so we can obtain the data faster. Using the transaction, we obtain the object store we want. With the request object that is passed back, we call the get function, passing in the id we are obtaining from a textbox. The auto-generated key is an integer, so we must pass in an integer value or it won’t find the record. We then simply set the controls with the values that were stored in the database.

Deleting a record has the same pattern. This is the line of interest:

var request = objectStore.delete(parseInt(id.value));

Similarly, to update a record, we simply do this:

var obj = {
    customerNumber: customerNumber.value,
    fullName: fullName,
    firstName: firstName.value,
    lastName: lastName.value,
    email: email.value,
    invoiceDate: invoiceDate.winControl.current
};
obj.id = recordId;
request = objectStore.put(obj);

Here, obj contains the data we are updating and recordId is assigned to the key of the object.

We can pull a large number of records using cursors. We can even provide parameters that enable us to accomplish “paging.” Although it wouldn’t be paging in the traditional sense, we could use the API to continually add items to a ListView as it was scrolling if we built our own DataSource object. That scenario is beyond the scope of the book, but we can see how to use a cursor to return all records in the database:

var output = document.getElementById("printOutput");
output.textContent = "";

var transaction = db.transaction(objectStoreName, "readonly");
objectStore = transaction.objectStore(objectStoreName);

var request = objectStore.openCursor();
request.onsuccess = function (evt) {
    var cursor = evt.target.result;
    if (cursor) {
        output.innerHTML += toStaticHTML("id: " + cursor.key + " is "
            + cursor.value.customerNumber + " " + "("
            + cursor.value.firstName + "; " + cursor.value.lastName
            + "; " + cursor.value.fullName + "; "
            + cursor.value.customerNumber + "; "
            + cursor.value.invoiceDate + "; " + cursor.value.email
            + ")" + "<br />");
        cursor.continue();
    }
    else {
        console.log("No more entries!");
    }
};

This is hardly surprising. We use the same pattern, but this time we call openCursor on the object store. In the success handler, we get the actual cursor. Assuming that it worked, we grab all data for that record and then get the next record by calling cursor.continue.

Finally, we can delete a single record or the entire database. To delete a single record, we do the following:

var request = objectStore.delete (parseInt(id.value));

To delete the entire database, the database needs to be closed and then we can call deleteDatabase on the indexedDB object, passing in the name of the database:

db.close();
indexedDB.deleteDatabase(databaseName);

Under Hour11/IndexedDBExample, we have a working example of all this code. It isn’t hardened, by any means; it crashes when trying to work with records that don’t exist. Regardless, it shows how the IndexedDB object can be used to create a database for data to be stored locally on the client. Figure 11.4 shows the program.

Image

Figure 11.4. indexedDB can be used to store large amounts of data that we can quickly retrieve.

Further Exploration

The StorageFolder object has many different methods we didn’t discuss. Besides creating and reading files, we can get a list of files by calling GetItemsAsync. A list of all methods and properties on the StorageFolder object can be found at http://msdn.microsoft.com/en-us/library/windows/apps/windows.storage.storagefolder.

A good overview of application data can be found in the Microsoft documentation: http://msdn.microsoft.com/en-us/library/windows/apps/hh464917.

Guidelines for roaming application data are located at http://msdn.microsoft.com/en-us/library/windows/apps/hh465094.

The W3C working draft document on IndexedDB is available at www.w3.org/TR/IndexedDB/.

The Windows SDK samples of interest this hour are

• App model sample

• IndexedDB sample

Summary

We have many options when it comes to storing app data on the machine for later retrieval. We looked at the local storage provided by the DOM, along with the DOM’s session storage. We looked at the WinJS sessionState object and saw that it was the best way to store session data. We look at the Windows Runtime Application Data APIs to see how to work with local storage, roaming storage, and temporary storage. We saw how to use the blob object to save the contents of a canvas to an image file. We discussed the lifetime of the different application data storage folders. We then finished the hour by looking at how to have a full-fledged database inside a Windows Store app by using the Indexed Database API (IndexedDB).

Q&A

Q. How much data can I store in roaming storage?

A. The maximum data that can be saved to the SkyDrive to be roamed between the user’s devices is currently 100KB.

Q. When does the temp data get cleared out? Will it be removed while the app is still running?

A. The system automatically clears the contents of the temp folder; this does not happen at a set time. TempState is cleared out when the system deems best. However, the system does not remove files from the temporary folder while the app is still running.

Q. Does IndexedDB have any synching capabilities so that I can bring the contents from the local database up to the database in the cloud?

A. No. The developer must handle any synching from the client database to a server database.

Workshop

Quiz

1. Which should be used, DOM session storage or WinJS sessionState?

2. WinJS helper functions enable us to easily write text files to the application data storage files. True or false?

3. Local storage is an appropriate place to store images the user will want to see even when the app is uninstalled. True or false?

4. User preference data should be stored under LocalSettings. True or false?

Answers

1. The WinJS sessionState object should be used because it has all the capabilities of DOM session storage, with the added benefit of storing data when apps are suspended, with no extra effort on our part. We look at reading that data and using what was saved during Hour 15.

2. True. WinJS helper functions enable us to easily write text files to the application data storage files. No such helper functions exist for binary data.

3. False. When an app is uninstalled, all local, roaming, and temp storage on the disk gets removed immediately. (Data stored on SkyDrive remains for a couple weeks.) Files that the user will want to have access to even if the app was uninstalled should be saved to the user’s library. This is discussed during Hour 14.

4. True. The local settings data store is best used for storing user preferences and configuration settings, such as the preferred background color for the app. It is best used for data that does not change frequently. When a value changes, the data store is written to disk.

Activities

1. Add in error handling to IndexedDBExample. Make it function even if bad data is entered.

2. Take the data entered in IndexedDBExample and hook it up to a ListView control.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.134.108.197