How to cook a complete Windows 8 application with HTML5, CSS3 and JavaScript in a week – Day 4

This is the final part of our series. Actually, I will post a last article when the Release Preview will be available to give you the updated version but you can consider this version as feature full.

And as usual the complete solution is available here: https://www.catuhe.com/msdn/urza/day4.zip

The complete series can be found here:

During this article you will discover how you can use Skydrive (via the Live SDK: Download the Live SDK) to save the collection’s state of your user.

The entire collection list is downloaded on a site as a json file (cf. Day 0). The collection’s state will be saved in another json file so the cards list can evolve without impacting the collection’s state. This json file will be saved to the user’s SkyDrive.

Adding a collection’s state file

In the expansion page, you have to add a contextual app bar to allow user to select card in order to indicate that the card is currently in his collection:

The new controls in the appbar are the following:

<button data-win-control="WinJS.UI.AppBarCommand" 
data-win-options="{id:'checkButton',section:'selection'}"> </button> <button data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'uncheckButton',section:'selection'}"> </button> <hr data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{type:'separator',section:'selection'}" /> <button data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'checkAllButton',section:'selection'}"> </button> <button data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'uncheckAllButton',section:'selection'}"> </button>

Every button is set to use the ‘selection’ section in order to appear in the left part of the app bar (the contextual part). By right-clicking an item (or swiping it down) in the listView, WinJS displays the appbar with all contextual buttons activated. Otherwise, only the buttons placed in the global part are displayed.

Don’t forget obviously to activate the multi-selection on the listView:

listView.selectionMode = WinJS.UI.SelectionMode.multi;

By clicking on these buttons, you call one of the following functions:

var removeFromCollection = function (tab) {
    if (!UrzaGatherer.UserData) {
        showWarning();
        return;
    }

    for (var index = 0; index < (tab ? tab.length : filtered.length); index++) {
        var card = filtered.getAt(tab ? tab[index] : index);

        card.isChecked = false;

        if (card.checkElement)
            WinJS.Utilities.addClass(card.checkElement, "hidden");

        delete UrzaGatherer.UserData[card.id];
    };

    var listView = document.querySelector(".cardsList").winControl;
    listView.selection.clear();

    if (document.getElementById("checkFilter").selectedIndex > 0)
        that.updateLayout(document, appView.value);

    UrzaGatherer.Skydrive.UploadUserFile();

    document.querySelector(".appBar").winControl.hide();
};

var addToCollection = function (tab) {
    if (!UrzaGatherer.UserData) {
        showWarning();
        return;
    }

    for (var index = 0; index < (tab ? tab.length : filtered.length); index++) {
        var card = filtered.getAt(tab ? tab[index] : index);

        card.isChecked = true;

        if (card.checkElement)
            WinJS.Utilities.removeClass(card.checkElement, "hidden");

        UrzaGatherer.UserData[card.id] = true;
    };

    var listView = document.querySelector(".cardsList").winControl;
    listView.selection.clear();

    if (document.getElementById("checkFilter").selectedIndex > 0)
        that.updateLayout(document, appView.value);

    UrzaGatherer.Skydrive.UploadUserFile();

    document.querySelector(".appBar").winControl.hide();
};

Every function checks the existence of UrzaGatherer.UserData which represents the current state of the user’s collection. If a tab parameter is passed, each functions works on it else they work on the entire expansion.

Adding an item to the user’s collection is just like adding a property named with the card unique id:

UrzaGatherer.UserData[card.id] = true;

So the user’s collection is just a big object with a property for each owned card Sourire. This object is easily serilizable with JSON.stringify!

For ease of access, the card has also a isChecked property. 

Connecting to Skydrive

To keep track of the user’s collection, it is required to save it to a safe place. You can then use SkyDrive as a central repository (with the agreement of the user obviously).

To do so, you have to download the Live SDK and to reference it in your project:

Then you have to reference the javascript file:

    <script src="///LiveSDKHTML/js/wl.js"></script>

First of all you have to give the user a way to connect to Live services:

var init = function () {
    var onStatusChange = function (e) {
        WL.getLoginStatus(function (response) {
            if (response.status == "notConnected") {
                connect();
            }
        });
    };

    WL.Event.subscribe("auth.login", getUserFile);
    WL.Event.subscribe("auth.statusChange", onStatusChange);

    WL.init({
        scope: ["wl.signin", "wl.skydrive_update"]
    });
};

This code can be called from a button in your application (I recommend you to put it in a corner of your application with a duplicate in the settings pane).

As you saw, the code subscribes to two events: auth.login and auth.statusChange. When the user is connected, you have to call the getUserFile (you will see it later). When the status change to “notConnected” you have to call the connect code:

var connect = function () {
    if (UrzaGatherer.Skydrive.OnConnecting)
        UrzaGatherer.Skydrive.OnConnecting();

    WL.login({
        scope: ["wl.signin", "wl.skydrive_update"]
    }).then(function (response) {
        getUserFile();
    }, function (responseFailed) {

        if (responseFailed.error == "access_denied") {
        }
        else {
            if (UrzaGatherer.Skydrive.OnFailed)
                UrzaGatherer.Skydrive.OnFailed();
        }
    });
};

I defined some events to allow my UI to display some feedbacks during the connection. The WL.login functions displays a control that asks the user to authorize your application to sign in Live services (“wl.signin”) and to access the user’s skydrive (“_wl.skydrive_update_”).

If the user refuses to grant the authorization, you can raise an event to synchronize your UI.

If the user accepts to grant the authorization, the following code is called:

var getUserFile = function () {
    UrzaGatherer.UserLogged = true;

    // Creating folder
    WL.api({
        path: "me/skydrive",
        method: "POST",
        body: {
            "name": "UrzaGatherer",
            "description": "UrzaGatherer repository folder"
        }
    }).then(
            function (response) {
                Windows.Storage.ApplicationData.current.roamingSettings.values["folderID"] = response.id;
                getFileOnline();
            },
            function (responseFailed) {
                if (responseFailed.error.code != "resource_already_exists") {
                    getFileOffline();
                    if (UrzaGatherer.Skydrive.OnFailed)
                        UrzaGatherer.Skydrive.OnFailed();
                }
                else {
                    getFileOnline();
                }
            }
        );

};

This function creates a folder for UrzaGatherer in SkyDrive and try to get the user’s collection. If there is an error, it tries to get the local copy of the user’s collection.

To get the online version, the following code is called:

var getFileOnline = function () {
    var fileID = Windows.Storage.ApplicationData.current.roamingSettings.values["fileID"];

    if (!fileID) {
        getFileOffline();
        return;
    }

    // Get file info
    WL.api({
        path: fileID,
        method: "GET"
    }).then(
            function (response) {
                var distantVersion = parseInt(response.description ? response.description : "-1");
                var localVersion = currentUserFileVersion();

                // Download file
                if (localVersion < distantVersion) {
                    Windows.Storage.ApplicationData.current.localFolder.
createFileAsync(filename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { WL.download({ path: fileID, file_output: file }).then( function (response) { getFileOffline(); }, function (responseFailed) { getFileOffline(); } ); }); return; } getFileOffline(); }, function (responseFailed) { getFileOffline(); } ); };

You can notice that once the file is loaded, a local copy is automaticaly created.

You may also have noticed that a file version number is maintained in order to know if the server version is more recent than the local one.

In case of an error, the following code can retrieve the local copy (if it exists):

var getFileOffline = function () {
    Windows.Storage.ApplicationData.current.localFolder.getFileAsync(filename).then(function (file) {
        // On success
        Windows.Storage.FileIO.readTextAsync(file).then(function (data) {
            if (data)
                UrzaGatherer.UserData = JSON.parse(data);
            else
                UrzaGatherer.UserData = {};

            if (UrzaGatherer.Skydrive.OnConnected && UrzaGatherer.UserLogged)
                UrzaGatherer.Skydrive.OnConnected();

            if (UrzaGatherer.Skydrive.OnDataAvailable)
                UrzaGatherer.Skydrive.OnDataAvailable();
        });
    }, function () {
        // On error
        Windows.Storage.ApplicationData.current.localFolder.
createFileAsync(filename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { UrzaGatherer.UserData = {}; Windows.Storage.FileIO.writeTextAsync(file, JSON.stringify(UrzaGatherer.UserData)); Windows.Storage.ApplicationData.current.
localSettings.values[
"userFileVersion"] = currentUserFileVersion() + 1; if (UrzaGatherer.Skydrive.OnConnected && UrzaGatherer.UserLogged) UrzaGatherer.Skydrive.OnConnected(); if (UrzaGatherer.Skydrive.OnDataAvailable) UrzaGatherer.Skydrive.OnDataAvailable(); }); }); };

If the system is unable to get a local copy, an empty version is created.

Updating the user’s collection to SkyDrive

Finally, the application can call the following code to upload the user’s collection to SkyDrive:

var uploadUserFile = function (deferral) {
    Windows.Storage.ApplicationData.current.localFolder.
createFileAsync(filename, Windows.Storage.CreationCollisionOption.replaceExisting).then(
function (file) { Windows.Storage.FileIO.writeTextAsync(file, JSON.stringify(UrzaGatherer.UserData)).
then(
function () { var folderID = Windows.Storage.ApplicationData.current.roamingSettings.values["folderID"]; Windows.Storage.ApplicationData.current.localSettings.
values[
"userFileVersion"] = currentUserFileVersion() + 1; WL.upload({ path: folderID, file_name: file.name, file_input: file, overwrite: true }).then( function (response) { Windows.Storage.ApplicationData.current.roamingSettings.values["fileID"] = response.id; updateDistantFileVersion(currentUserFileVersion()); if (deferral) deferral.complete(); }, function (responseFailed) { if (deferral) deferral.complete(); } ); }); }); };

Once the file is uploaded, the file version number is updated locally and on SkyDrive:

var updateDistantFileVersion = function (version) {
    var fileID = Windows.Storage.ApplicationData.current.roamingSettings.values["fileID"];

    if (!fileID)
        return;

    WL.api({
        path: fileID,
        method: "PUT",
        body: {
            description: version
        }
    }).done();
};

So with a small set of API, you can create, read and write data on SkyDrive. The connection UI is provided by the SDK so it is really simple to integrate it in your onw application.

You can even use predefined controls to present an Connect/Disconnect button.

To be continued

You have now a complete and compliant Windows 8 application that can help you create our own award winning app! When the Release Preview will be out (early in June), I will post a last article to show you how to port your code.

On my side, I will publish UrzaGatherer in the Windows store when the Release Preview will be available. I added some visual features like a card of the day, a dynamic background and tons of appealing animations:

 

So stay tuned for the final version Sourire