Search code examples
javascriptxmlwinapiwindows-8windows-runtime

Windows 8 Javascript app XML Object


I'm currently trying to make a HTML/JavaScript Windows 8 modern application in which I want to access a local XML file that is in the installation directory. After reading many ideas and code snippets around the web, I came up with a convoluted asynchronous method of accessing the file, which works. However, is this the best/correct way to do something as simple as accessing a local XML file? Additionally, I'd like to be able to have a function load the xml file, and save the XMLDocument object as a "global" variable, so that on button presses and other triggers, the XMLDocument object can be accessed and parsed. This is where all the problems start, since one method is async, and then the variables are undefined, etc....

(function () {
"use strict";

WinJS.UI.Pages.define("/pages/reader/reader.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.
        var button = document.getElementById("changeText");
        button.addEventListener("click", this.buttonClickHandler, false);
        var dropdown = document.getElementById("volumeDropdown");
        dropdown.addEventListener("change", this.volumeChangeHandler, false);
        var loadSettings = new Windows.Data.Xml.Dom.XmlLoadSettings;
        loadSettings.prohibitDtd = false;
        loadSettings.resolveExternals = false;

        //previous attempt, also didn't work:
        //this.xmlDoc = null;
        //this.loadXMLdoc(this, this.testXML);

        //also not working:
        this.getXmlAsync().then(function (doc) {
            var xmlDoc = doc;
        });

        //this never works also, xmlDoc always undefined, or an error:
        //console.log(xmlDoc);            
    },
    buttonClickHandler: function (eventInfo) {
        // doesn't work, xmlDoc undefined or error:
        console.log(xmlDoc);
    },
    volumeChangeHandler: function (eventInfo) {
        var e = document.getElementById("volumeDropdown");
        // of course doesn't work, since I can't save the XMLDocument object into a variable (works otherwise):
        var nodelist2 = xmlDoc.selectNodes('//volume[@name="volumeName"]/chapter/@n'.replace('volumeName', list[0]));
        var volumeLength = nodelist2.length;
        for (var index = 0; index < volumeLength; index++) {
            var option = document.createElement("option");
            option.text = index + 1;
            option.value = index + 1;
            var volumeDropdown = document.getElementById("chapterDropdown");
            volumeDropdown.appendChild(option);
        }
    },
    getXmlAsync: function () {
        return Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("books").then(function (externalDtdFolder) {
            externalDtdFolder.getFileAsync("book.xml").done(function (file) {
                return Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file);
            })
        })
    },
    loadXMLdoc: function (obj, callback) {
        var loadSettings = new Windows.Data.Xml.Dom.XmlLoadSettings;
        loadSettings.prohibitDtd = false;
        loadSettings.resolveExternals = false;
        Windows.ApplicationModel.Package.current.installedLocation.getFolderAsync("books").then(function (externalDtdFolder) {
            externalDtdFolder.getFileAsync("book.xml").done(function (file) {
                Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file, loadSettings).then(function (doc) {

                    var nodelist = doc.selectNodes("//volume/@name");
                    var list = [];
                    for (var index = 0; index < nodelist.length; index++) {
                        list.push(nodelist[index].innerText);
                    };
                    for (var index = 0; index < list.length; index++) {
                        var option = document.createElement("option");
                        option.text = list[index] + "new!";
                        option.value = list[index];
                        var volumeDropdown = document.getElementById("volumeDropdown");
                        volumeDropdown.appendChild(option);
                    };
                    var nodelist2 = doc.selectNodes('//volume[@name="volumeName"]/chapter/@n'.replace('volumeName', list[0]));
                    var volumeLength = nodelist2.length;
                    for (var index = 0; index < volumeLength; index++) {
                        var option = document.createElement("option");
                        option.text = index + 1;
                        option.value = index + 1;
                        var volumeDropdown = document.getElementById("chapterDropdown");
                        volumeDropdown.appendChild(option);
                    };


                    obj.xmlDoc = doc;
                    callback(obj);

                })
            })
        });
    },
    initializeXML: function (doc, obj) {
        console.log("WE ARE IN INITIALIZEXML NOW")
        obj.xmlDoc = doc;
    },
    testXML: function (obj) {
        console.log(obj.xmlDoc);
    },


});

})();

In summary with all these complicated methods failing, how should I go about doing something as simple as loading an XML file, and then having it available as an object that can be used by other functions, etc.?

Thanks for your help!

PS: I'm very new to JavaScript and Windows 8 Modern Apps/ WinAPIs. Previous experience all in Python and Java (where doing this is trivial!).


Solution

  • There are a couple of things going on here that should help you out.

    First, there are three different loading events for a PageControl, corresponding to methods in your page class. The ready method (which is the only one the VS project template includes) gets called only at the end of the process, and is thus somewhat late in the process for doing an async file load. It's more appropriate to do this work within the init method, which is called before any elements have been created on the page. (The processed method is called after WinJS.UI.processAll is complete but before the page has been added to the DOM. ready is called after everything is in the DOM.)

    Second, your getXMLAsync method looks fine, but your completed handler is declaring another xmlDoc variable and then throwing it away:

    this.getXmlAsync().then(function (doc) {
        var xmlDoc = doc; //local variable gets discarded
    });
    

    The "var xmlDoc" declares a local variable in the handler, but it's discarded as soon as the handler returns. What you need to do is assign this.xmlDoc = doc, but the trick is then making sure that "this" is the object you want it to be rather than the global context, which is the default for an anonymous function. The pattern that people generally use is as follows:

    var that = this;
    this.getXmlAsync().then(function (doc) {
        that.xmlDoc = doc;
    });
    

    Of course, it's only after that anonymous handler gets called that the xmlDoc member will be valid. That is, if you put a console.log at the end of the code above, after the });, the handler won't have been called yet from the async thread, so xmlDoc won't get be valid. If you put it inside the handler immediately after that.xmlDoc = doc, then it should be valid.

    This is all just about getting used to how async works. :)

    Now to simplify matters for you a little, there is the static method StorageFile.getFileFromApplicationUriAsync which you can use to get directly to in-package file with a single call, rather than navigating folders. With this you can load create the XmlDocument as follows:

    getXmlAsync: function () {
        return StorageFile.getFileFromApplicationUriAsync("ms-appx:///books/book.xml").then((function (file) {
            return Windows.Data.Xml.Dom.XmlDocument.loadFromFileAsync(file);
        }).then(function (xmlDoc) {
            return xmlDoc;
        });
    }
    

    Note that the three /// are necessary; ms-appx:/// is a URI scheme that goes to the app package contents.

    Also notice how the promises are chained instead of nested. That's typically a better structure, and one that allows a function like this to return a promise that will be fulfilled with the last return value in the chain. This can then be used with the earlier bit of code that assigns that.xmlDoc, and you avoid passing in obj and a callback (promises are intended to avoid such callbacks).

    Overall, if you have any other pages in your app to which you'll navigate, you'll really want to load this XML file and create the XmlDocument once for the app, not with the specific page. Otherwise you'd be reloading the file every time you navigate to the page. For this reason, you could choose to do the loading on app startup, not page load, and use WinJS.Namespace.define to create a namespace variable in which you store the xmlDoc. Because that code would load on startup while the splash screen is visible, everything should be ready when the first page comes up. Something to think about.

    In any case, given that you're new to this space, I suggest you download my free ebook, Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition, where Chapter 3 has all the details about app startup, page controls, and promises (after the broader introductions of Chapters 1 and 2 of course).