Creating and navigating between pages in Hilo (Windows Store apps using JavaScript and HTML)

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

From: Developing an end-to-end Windows Store app using JavaScript: Hilo

Previous page | Next page

In Windows Store apps such as Hilo, there is one page for each screen of the application that a user can navigate to. The app loads a home page (the main hub page) on startup and loads additional pages in response to navigation requests. We created pages by using page controls, which provide built-in support for navigation, and we modified a project template file, navigator.js, to implement the navigation model.

Download

After you download the code, see Getting started with Hilo for instructions.

You will learn

  • How pages were designed in the Hilo app.
  • How the Hilo app loads pages and their data sources at run time.
  • How the Hilo app uses a customized version of the template navigation control.

Applies to

  • Windows Runtime for Windows 8
  • Windows Library for JavaScript
  • JavaScript

Adding New Pages to the Project

There are five pages in the Hilo app. These are:

  • The main hub page, which shows a small subset of photos and provides links to other views.
  • The month view page, where you can view photos by group.
  • The details page, where you can select a photo to crop or rotate.
  • The crop page, which allows you to edit a photo.
  • The rotate page, which allows you to rotate a photo.

When we built the app, Hilo developers added each page to the project by using the Add New Item dialog box in Visual Studio. Each page is a separate PageControl with an associated HTML, CSS, and JavaScript file.

The hub page is the start page you see when you run the app. Here is what the main hub page looks like.

[Top]

Designing pages in Visual Studio and Blend

Hilo C++ developers used Blend for Microsoft Visual Studio 2012 for Windows 8 to fine-tune the shared UX design for Hilo C++ and Hilo JavaScript. Blend helped with the design of pages and controls. For more info on the shared UX design, see Designing the UX. For Hilo JavaScript, we used Blend primarily as a versatile CSS editor. In addition to providing a live DOM view, Blend supports a feature called Interactive Mode that allows you to run the app and pause it in a particular state. You can view the paused app on the Design Surface, edit CSS, and see the effects immediately. We found this feature useful to fine-tune the page presentation. The following illustration shows a paused app running in the Design Surface. The CSS properties pane on the right provides information about how the styles are applied to the page and allows you to change values.

For more info on using Blend, see Blend for Visual Studio 2012.

We recommend that you use Visual Studio to work with the code-focused aspects of your app. Visual Studio works best for writing JavaScript, running, and debugging your app. The Refresh Windows app button in Visual Studio allowed us to iterate quickly through code modifications by reloading updated HTML, CSS, and JavaScript code without restarting the debugger.

For more info on designing an app with Visual Studio, see Create your first Windows Store app using JavaScript. That topic provides an introduction that shows you how to design a simple page in a Windows Store app.

[Top]

Pages in the Hilo app are page controls that support a single-page navigation model, which is the preferred navigation model for Windows Store apps using JavaScript. Each page control represents content that the user can navigate to. Pages contain other controls. The single-page navigation model provides a smoother, app-like transition between pages, and also makes it easier to manage state, because scripts are never unloaded. When we created Hilo, we found that the fastest route to a functional navigation model was to use the single-page model. In this way, we could benefit by re-using existing, tested navigation code. When you use single-page navigation in very large applications, you may need to more actively manage memory to improve performance.

In this navigation model, HTML pages (the page controls) are loaded into a single content host, a DIV element declared in default.html. In default.html, the content host DIV element is declared as a control of type PageControlNavigator using the HTML data-win-control attribute that is provided by WinJS.

Hilo\Default.html

<div id="contenthost" data-win-control="Hilo.PageControlNavigator" data-win-options="{home: '/Hilo/hub/hub.html'}">
</div>

The PageControlNavigator is implemented in PageControlNavigator.js. In Hilo, PageControlNavigator.js is a modified version of the navigator.js file that is included with Visual Studio project templates, such as Grid and Split. For more information about the single-page navigation model used in some Windows Store apps, see QuickStart: using single-page navigation.

We made a few modifications to navigator.js for Hilo, such as renaming the file and changing the namespace to fit the Hilo naming conventions. We also replaced some DOM level-0 style event registrations with the use of the DOM level 2 addEventListener.

We also implemented a reload function (not shown) in the modified navigator.js file. We added the reload function to support scenarios in which a user makes a change in the file system’s Pictures folder while the app is running. We capture the file system change by using the file query’s contentschanged event. The listener for the event is implemented in the contentChangedListener.js file.

[Top]

To load a page into the content host DIV element, the app calls WinJS.Navigation.navigate, passing in the page URL. The navigate function is used to navigate to a page control. In Hilo pages, you can find a typical example of using the navigate function in itemClicked, which is invoked when a picture is clicked or tapped. In this code, nav contains a reference to WinJS.Navigation.

Hilo\Hilo\hub\hubPresenter.js

itemClicked: function (args) {

    // Get the `Hilo.Picture` item that was bound to the invoked image,
    // and the item index from the list view control.
    var picture = args.detail.item.data;

    // Build the query that can find this picture within it's month group.
    var options = this.buildQueryForPicture(picture);

    // Navigate to the detail view, specifying the month query to
    // show, and the index of the individual item that was invoked.
    this.nav.navigate("/Hilo/detail/detail.html", options);
},

WinJS.Navigation.navigate does not directly navigate to a page, but invokes the WinJS.Navigation.onnavigated event. The handler for WinJS.Navigation.onnavigated is implemented in PageControlNavigator.js. The main job of the handler is to call a WinJS page control function (render) that loads the actual page. The code in the handler, not shown, is unmodified from Navigator.js.

Each page control implements a ready function that automatically runs when the page loads. The implementation of the ready function is different for each page. For more information on feature implementation in the pages, see Using controls.

[Top]

Creating and loading pages

To create a new Hilo page, we used Visual Studio to add a Page Control item template (Add > New Item). By using the template, in one step we added an HTML, CSS, and JavaScript file to the project. These files included default content and were already wired up to support the single-page navigation model. We then modified the template to work as a Hilo page.

Hilo pages use a custom base object for all page controls. We implemented the base object so that we could combine common page logic and centralize some tasks more easily than we could in the default item template. In the base object, we included code to handle the following tasks:

  • Register pages, such as "Hilo/hub/hub.html".
  • Process localized string resources.
  • Process clickable subtitles that are present in most of the pages.
  • Handle deserialization when an app resumes after being suspended.

To make a new page use our base page object, we replace the call to WinJS.UI.Pages.define in the item template code with a call to Hilo.controls.pages.define. We moved the call to WinJS.UI.Pages.define, which is required to create a page control, to the base page object. The arguments passed to define specify the name of the page and the methods implemented on the page. The following code example is from hub.js. In this code, the methods implemented on the page include ready, updateLayout, and unload.

Hilo\Hilo\hub\hub.js

Hilo.controls.pages.define("hub", {

    ready: function (element, options) {

        // Handle the app bar button clicks for showing and hiding the app bar.
        var appBarEl = document.querySelector("#appbar");
        var hiloAppBar = new Hilo.Controls.HiloAppBar.HiloAppBarPresenter(appBarEl, WinJS.Navigation);

        // Handle selecting and invoking (clicking) images.
        var listViewEl = document.querySelector("#picturesLibrary");
        this.listViewPresenter = new Hilo.Hub.ListViewPresenter(listViewEl, Windows.UI.ViewManagement.ApplicationView);

        // Coordinate the parts of the hub page.
        this.hubViewPresenter = new Hilo.Hub.HubViewPresenter(
            WinJS.Navigation,
            hiloAppBar,
            this.listViewPresenter,
            new Hilo.ImageQueryBuilder()
        );

        this.hubViewPresenter
            .start(knownFolders.picturesLibrary)
            .then(function () {
                WinJS.Application.addEventListener("Hilo:ContentsChanged", Hilo.navigator.reload);
            });
    },

    updateLayout: function (element, viewState, lastViewState) {
        this.listViewPresenter.setViewState(viewState, lastViewState);
    },

    unload: function () {
        WinJS.Application.addEventListener("Hilo:ContentsChanged", Hilo.navigator.reload);
        Hilo.UrlCache.clearAll();
        this.hubViewPresenter.dispose();
        this.hubViewPresenter = null;
    }
});

The define function for the base object, implemented in pages.js, is exposed for use in the app with WinJS.Namespace.define.

Hilo\Hilo\controls\pages.js

WinJS.Namespace.define("Hilo.controls.pages", {
    define: define
});

In the define function, we set the URL for the page and then pass the page-specific ready function into a call to wrapWithCommonReady.

Hilo\Hilo\controls\pages.js

function define(pageId, members) {

    var url = "/Hilo/" + pageId + "/" + pageId + ".html";

    members.ready = wrapWithCommonReady(members.ready);
    members.bindPageTitle = bindPageTitle;

    return WinJS.UI.Pages.define(url, members);
}

wrapWithCommonReady takes the page’s ready function as input, and returns a new wrapper function. In the wrapper function, we added additional code that is common to all the Hilo pages. This includes, for example, the call to WinJS.Resources.processAll, which processes the localized string resources (.resjson files). It also includes a call to handle deserialization if the app is resuming after being suspended. The wrapper function includes a call to the page’s ready function in this line: return ready(element, options). By centralizing these tasks, we were able to reduce page dependencies, avoid repeating code on different pages, and make page behavior more consistent.

Hilo\Hilo\controls\pages.js

function wrapWithCommonReady(pageSpecificReadyFunction) {

    pageSpecificReadyFunction = pageSpecificReadyFunction || function () { };

    return function (element, options) {

        processLinks();

        // Handle localized string resources for the page.
        WinJS.Resources.processAll();

        // Ensure that the `options` argument is consistent with expectations,
        // for example, that it is properly deserialized when resuming.
        Hilo.controls.checkOptions(options);

        // We need to bind the `pageSpecificReadyFunction` function explicitly, 
        // otherwise it will lose the context that the developer expects (that is, 
        // it will not resolve `this` correctly at execution time.
        var ready = pageSpecificReadyFunction.bind(this);

        // Invoke the custom `ready`.
        return ready(element, options);
    };
}

When wrapWithCommonReady returns, we use its return value (the wrapper function) to set the new ready function for the page. In define, we then call a function to set the page title and finally call WinJS.UI.Pages.define. We pass this method the URL and the page members. The WinJS.UI.Pages.define function is required to define a page control.

Hilo\Hilo\controls\pages.js

members.ready = wrapWithCommonReady(members.ready);
members.bindPageTitle = bindPageTitle;

return WinJS.UI.Pages.define(url, members);

When the app runs, the new ready function gets called automatically after a call to navigate. Then, the new ready calls the page’s ready function. Here’s the ready function for the hub page.

Hilo\Hilo\hub\hub.js

ready: function (element, options) {

    // Handle the app bar button clicks for showing and hiding the app bar.
    var appBarEl = document.querySelector("#appbar");
    var hiloAppBar = new Hilo.Controls.HiloAppBar.HiloAppBarPresenter(appBarEl, WinJS.Navigation);

    // Handle selecting and invoking (clicking) images.
    var listViewEl = document.querySelector("#picturesLibrary");
    this.listViewPresenter = new Hilo.Hub.ListViewPresenter(listViewEl, Windows.UI.ViewManagement.ApplicationView);

    // Coordinate the parts of the hub page.
    this.hubViewPresenter = new Hilo.Hub.HubViewPresenter(
        WinJS.Navigation,
        hiloAppBar,
        this.listViewPresenter,
        new Hilo.ImageQueryBuilder()
    );

    this.hubViewPresenter
        .start(knownFolders.picturesLibrary)
        .then(function () {
            WinJS.Application.addEventListener("Hilo:ContentsChanged", Hilo.navigator.reload);
        });
},

The ready function here creates presenters for the view. Hilo implements a Model-View-Presenter (MVP) pattern. For more info, see Using a separated presentation pattern.

[Top]

Loading the hub page

When the Hilo app starts up, the hub page gets loaded into the content host DIV element declared in default.html. The hub page is specified as the home page in the content host DIV using the WinJSdata-win-options attribute.

Hilo\default.html

<div id="contenthost" data-win-control="Hilo.PageControlNavigator" data-win-options="{home: '/Hilo/hub/hub.html'}">
</div>

In PageControlNavigator.js, the data-win-options value gets passed into the constructor function for the PageControlNavigator.The home page is assigned to the navigator object. In addition, this code specifies Hilo as the namespace for the navigator object.

PageControlNavigator.js

WinJS.Namespace.define("Hilo", {
    PageControlNavigator: WinJS.Class.define(

        // Define the constructor function for the PageControlNavigator.
        function PageControlNavigator(element, options) {
            var body = document.body;

            // . . . 

            this.home = options.home;
            // . . . 
            Hilo.navigator = this;
        },

To load the hub page into the content host DIV element, the app calls navigate. The app passes in the home value of the PageControlNavigator. The following code is found in the activated event handling function in default.js.

Hilo\default.js

if (nav.location) {
    nav.history.current.initialPlaceholder = true;
    return nav.navigate(nav.location, nav.state);
} else {
    return nav.navigate(Hilo.navigator.home);
}

[Top]

Establishing the Data Binding

Each page of the Hilo app includes data binding. Data binding links the model (data source) to the view. In Hilo, presenter classes are responsible for assigning the data source to the view. For more information see Using a separated presentation pattern in this guide. For additional info on declarative binding used in Hilo, see Using controls.

[Top]

Supporting portrait, snap, and fill layouts

We designed Hilo to be viewed full-screen in the landscape orientation. Windows Store appssuch as Hilo must adapt to different application view states, including both landscape and portrait orientations. Hilo supports fullScreenLandscape, filled, fullScreenPortrait, and snapped layouts. Hilo uses the Windows.UI.ViewManagement.ApplicationView class to get the current view state. It uses the resize event to handle changes to the visual display to support each layout.

The PageControlNavigator registers for the resize event in its constructor.

Hilo\Hilo\PageControlNavigator.js

    function PageControlNavigator(element, options) {

        // . . .

        window.onresize = this._resized.bind(this);

        // . . .
    },

When a resize event occurs, the handler for the event calls the updateLayout function that is implemented in Hilo pages. The appView variable contains a reference to the ApplicationView object, the value of which gets passed as the view state.

Hilo\Hilo\PageControlNavigator.js

_resized: function (args) {
    if (this.pageControl && this.pageControl.updateLayout) {
        this.pageControl.updateLayout.call(this.pageControl, this.pageElement, appView.value, this._lastViewstate);
    }
    this._lastViewstate = appView.value;
},

The following example shows the implementation of updateLayout for the hub page. In the hub page, updateLayout calls the ListView presenter’s setViewState function. It then passes along the view state.

Hilo\Hilo\hub\hub.js

updateLayout: function (element, viewState, lastViewState) {
    this.listViewPresenter.setViewState(viewState, lastViewState);
},

In the ListView presenter, setViewState sets the current layout of the hub page’s ListView control by using the value returned from selectLayout. It then forwards the view state to selectLayout.

Hilo\Hilo\hub\listViewPresenter.js

setViewState: function (viewState) {
    this.lv.layout = this.selectLayout(viewState);
},

selectLayout checks whether the current view state is in snapped mode or not. If it is, it returns a ListLayout object, which represents a vertical list. If the view state is not snapped, it returns a GridLayout object, which represents a horizontal list.

Hilo\Hilo\hub\listViewPresenter.js

selectLayout: function (viewState, lastViewState) {

    if (lastViewState === viewState) { return; }

    if (viewState === appViewState.snapped) {
        return new WinJS.UI.ListLayout();
    }
    else {
        var layout = new WinJS.UI.GridLayout();
        layout.groupInfo = function () { return listViewLayoutSettings; };
        layout.maxRows = 3;
        return layout;
    }
},

Some pages don’t need to do anything special to handle view states, so updateLayout is not implemented for all pages.

[Top]