Part 3: PageControl objects and navigation (Windows Phone Store apps using JavaScript)
[ 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 ]
The "Hello, world" app you created in Part 1 and Part 2 of this series of tutorials has only a single page of content. Most real-world apps contain multiple pages.
You have multiple navigation patterns to choose from when you create a Windows Phone Store app. The Navigation patterns topic will help you choose the best navigation pattern for your app.
Note
See the two primary navigation patterns, Flat navigation and Hierarchical navigation, in action in the App features, start to finish series.
In this tutorial, you copy the code from your "Hello, world" app into a new app that uses the Navigation App template, and then you add another page.
Learn how to:
- Use the Navigation App project template to create an app that contains multiple pages of content.
- Use PageControl objects to separate your code into modular units.
- Use the single-page navigation model to navigate between pages.
- Use an NavBar to provide navigation commands.
Before you start...
- This is the third tutorial in a 5-part series. Before you begin this tutorial, read Part 2: Manage app lifecycle and state. This tutorial builds on the code you created in Part 1 and Part 2.
Navigation in apps
Nearly every website provides some form of navigation, usually in the form of hyperlinks that you click to go to a different page. Each page has its own set of JavaScript functions and data, a new set of HTML to display, style information, and so on. This navigation model is known as multi-page navigation. This design is fine for most websites, but it can pose problems for an app because it can be difficult to maintain state across multiple pages.
Another navigation model is single-page navigation, in which you use a single page for your app and load additional data into that page as needed. You still split your app into multiple files, but instead of moving from page to page, your app loads other documents into the main page. Because your app's main page is never unloaded, your scripts are never unloaded, which makes it easier to manage state, transitions, or animations. We recommend that apps use the single-page navigation model.
To help you create apps that use the single-page navigation model, the Windows Library for JavaScript (WinJS) provides the WinJS.UI.Pages.PageControl object. There's also the Navigation App project template, which provides some additional navigation infrastructure. In the following steps, you use this template to create a new project.
Step 1: Create a new Navigation App in Visual Studio
First, create a new app named HelloWorldWithPages
that uses the Navigation App template.
To create a new Navigation App
In Microsoft Visual Studio Express 2013 for Windows, on the File menu, click New Project.
In the left pane of the New Project dialog, expand Installed, expand Templates, expand JavaScript, expand Store Apps, and then select the Windows Phone template type. The dialog's center pane displays a list of project templates for JavaScript.
For this tutorial, use the Navigation App template.
In the center pane, select the Navigation App template.
In the Name text box, type "HelloWorldWithPages".
Click OK to create the project.
Visual Studio creates your project and displays it in Solution Explorer.
Your new Navigation App contains a few more files than your "Hello, world" app. It includes these new files:
/pages/home/home.css, /pages/home/home.html, and /pages/home/home.js
These three pages define a PageControl for the app's home page. A PageControl is made up of an HTML file, a JavaScript file, and a Cascading Style Sheets (CSS) file. A PageControl is a modular unit of HTML, CSS, and JavaScript that can be navigated to (like an HTML page) or used as a custom control. You can use PageControl objects to split a large app into smaller, more manageable portions.
PageControl objects support several methods that makes using them in your app easier than using a collection of loose HTML, CSS, and JavaScript pages. You'll learn more about these methods in a later step.
/js/navigator.js
This file provides the
PageControlNavigator
helper class that you can use to display PageControl objects and navigate between them. You don't need it to display a PageControl, but it can make using them easier.
This is the default.html page of your new app:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>HelloWorldWithPages</title>
<!-- WinJS references -->
<!-- At runtime, ui-themed.css resolves to ui-themed.light.css or ui-themed.dark.css
based on the user’s theme setting. This is part of the MRT resource loading functionality. -->
<link href="/css/ui-themed.css" rel="stylesheet" />
<script src="//Microsoft.Phone.WinJS.2.1/js/base.js"></script>
<script src="//Microsoft.Phone.WinJS.2.1/js/ui.js"></script>
<!-- HelloWorldWithPages references -->
<link href="/css/default.css" rel="stylesheet" />
<script src="/js/default.js"></script>
<script src="/js/navigator.js"></script>
</head>
<body class="phone">
<div id="contenthost" data-win-control="Application.PageControlNavigator"
data-win-options="{home: '/pages/home/home.html'}"></div>
<!-- <div id="appbar" data-win-control="WinJS.UI.AppBar">
<button data-win-control="WinJS.UI.AppBarCommand"
data-win-options="{id:'cmd', label:'Command', icon:'placeholder'}" type="button"></button>
</div> -->
</body>
</html>
The body of the file contains two elements: A div element for the PageControlNavigator
and a commented-out div for an AppBar. You can ignore the app bar for now and take a closer look at the first div element.
<div id="contenthost" data-win-control="Application.PageControlNavigator"
data-win-options="{home: '/pages/home/home.html'}"></div>
This div element creates a PageControlNavigator
control. The PageControlNavigator
loads and displays your home page. Use the data-win-options attribute to tell it which page to load (/pages/home/home.html).
Run the app.
Although it's not obvious, the app is actually showing both default.html and home.html. It's similar to using an iframe to display a HTML page inside another HTML page.
Step 2: Copy your HTML and CSS content from your "Hello, world" app
Your new app has two HTML pages: default.html and home.html. Where do you put your content?
Use default.html for UI that should always be present, no matter which page the app is displaying. For example, you can use default.html to host a navigation bar.
Use pages, such as home.html, for content that makes up an individual screen in the app.
Open home.html and take a look at some of the markup it contains.
It has a head element that contains references to the Windows Library for JavaScript code and style sheets. It also contains references to the app's default style sheet (default.css) and to the other files that make up the home page (home.css and home.js).
<head> <meta charset="utf-8" /> <title>homePage</title> <!-- WinJS references --> <link href="//Microsoft.Phone.WinJS.2.1/css/ui-dark.css" rel="stylesheet" /> <script src="//Microsoft.Phone.WinJS.2.1/js/base.js"></script> <script src="//Microsoft.Phone.WinJS.2.1/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>
It has a page header area that includes a title element.
<header aria-label="Header content" role="banner"> <h1 class="titlearea win-type-ellipsis"> <span class="pagetitle">Welcome to App3!</span> </h1> </header>
It has a section for your main content.
<section aria-label="Main content" role="main"> <p>Content goes here.</p> </section>
Next, add the content from your "Hello, world" app to the home page (home.html) of your new HelloWorldWithPages
project.
To add the HTML and CSS content from your "Hello, world" app
Copy your final HTML content from the default.html file of your "Hello, world" app into the main content section of the /pages/home/home.html in your new project.
<body> <!-- The content that will be loaded and displayed. --> <div class="fragment homepage"> <header aria-label="Header content" role="banner"> <button data-win-control="WinJS.UI.BackButton"></button> <h1 class="titlearea win-type-ellipsis"> <span class="pagetitle">Welcome to HelloWorldWithPages!</span> </h1> </header> <section aria-label="Main content" role="main"> <!-- Copied from "Hello, world" --> <h1 class="headerClass">Hello, world!</h1> <div class="mainContent"> <p>What's your name?</p> <input id="nameInput" type="text" /> <button id="helloButton">Say "Hello"</button> <div id="greetingOutput"></div> <br /> <div id="toggleControlDiv" data-win-control="WinJS.UI.ToggleSwitch" data-win-options="{title: 'Greeting Color'}"></div> </div> </section> </div> </body>
Move the heading content that you copied to the h1 element that home.html provides for you. Because home.html already contains a main content section, remove the "mainContent" div element that you copied (but leave its contents).
<body> <!-- The content that will be loaded and displayed. --> <div class="fragment homepage"> <header aria-label="Header content" role="banner"> <button data-win-control="WinJS.UI.BackButton"></button> <h1 class="titlearea win-type-ellipsis"> <span class="pagetitle">Welcome to HelloWorldWithPages!</span> </h1> </header> <section aria-label="Main content" role="main"> <p>What's your name?</p> <input id="nameInput" type="text" /> <button id="helloButton">Say "Hello"</button> <div id="greetingOutput"></div> <br /> <div id="toggleControlDiv" data-win-control="WinJS.UI.ToggleSwitch" data-win-options="{title: 'Greeting Color'}"></div> </section> </div> </body>
Each PageControl has its own CSS file.
Copy the
greetingOutput
andtoggleOn
styles from the default.css file you created in Part 1: Create a "Hello, world!" app to home.css..homepage section[role=main] { margin-left: 24px; margin-right: 24px; } .fragment section[role=main] { width: calc(100% - 48px) } .fragment header[role=banner] .titlearea { -ms-grid-column: 3; margin-top: 37px; margin-left: -96px; } #greetingOutput { font-size:x-large; } .toggle-on { color:blue; }
Run the app.
You've recreated the content from your original "Hello, world" app. Next, add interactivity by copying your "Hello, world" event handlers.
Step 3: Copy your event handlers
Each PageControl has its own JavaScript file. Take a look at the JavaScript file that Visual Studio created for your "home" PageControl, home.js:
(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) {
// TODO: Initialize the page here.
}
});
})();
This file looks quite different than your default.js file. For one thing, it's much shorter. That's because default.js already handles activation and core app logic. Each PageControl only needs to contain logic for the page itself.
One of the first lines of code, a call to the WinJS.UI.Page.define function, creates the PageControl object. This function takes two parameters: the URI of the page ("/pages/home/home.html" in this example), and an object that defines the members of the PageControl. You can add any type of member you want. You can also implement a set of special members, described by the IPageControlMembers interface, that automatically get called by the app when you use the PageControl.
The home.js file created by the template defines one of these special members, the ready function. The ready function is called after your page is initialized and rendered. This is a good place to attach event handlers.
You might notice that the code doesn't include a call to WinJS.UI.processAll. That's because the PageControl calls it for you automatically. By the time the ready function is called, WinJS.UI.processAll has already been called and has completed its processing.
To add event handlers
In Part 1: Create a "Hello, world" app and Part 2: Manage app life cycle and state, you defined three event handlers:
buttonClickHandler
,toggleChanged
, andnameInputChanged
. Copy these event handlers to your home.js file and make them members of your PageControl. Add them after the ready function that the template created for you.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) { // TODO: Initialize the page here. }, buttonClickHandler: function (eventInfo) { // Get the user's name input var userName = document.getElementById("nameInput").value; // Create the greeting string and set the greeting output to it var greetingString = "Hello, " + userName + "!"; document.getElementById("greetingOutput").innerText = greetingString; // Save the session data. WinJS.Application.sessionState.greetingOutput = greetingString; }, toggleChanged: function (eventInfo) { // Get the toggle control var toggleControl = document.getElementById("toggleControlDiv").winControl; // Get the greeting output var greetingOutput = document.getElementById("greetingOutput"); // Set the CSS class for the greeting output based on the toggle's state if (toggleControl.checked == true) { greetingOutput.setAttribute("class", "toggle-on"); } else { greetingOutput.removeAttribute("class", "toggle-on"); } // Store the toggle selection for multiple sessions. var appData = Windows.Storage.ApplicationData.current; var roamingSettings = appData.roamingSettings; roamingSettings.values["toggleSelection"] = toggleControl.checked; }, nameInputChanged: function (eventInfo) { var nameInput = eventInfo.srcElement; // Store the user's name input WinJS.Application.sessionState.nameInput = nameInput.value; } });
Now you need to attach your event handlers. In Part 1 and Part 2 you created a then function for the Promise returned by WinJS.UI.processAll. Things are a little simpler now because you can use the ready function to attach your event handlers. The ready function is called after the PageControl has automatically called WinJS.UI.processAll.
Copy the code that attaches your event handlers to the ready function in home.js, modifying the reference to each handler to include the this prefix.
// 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. // Retrieve the div that hosts the Toggle control. var toggleControlDiv = document.getElementById("toggleControlDiv"); // Retrieve the actual Toggle control. var toggleControl = toggleControlDiv.winControl; // Register the event handler. toggleControl.addEventListener("change", this.toggleChanged, false); // Retrieve the button and register your event handler. var helloButton = document.getElementById("helloButton"); helloButton.addEventListener("click", this.buttonClickHandler, false); // Retrieve the input element and register your // event handler. var nameInput = document.getElementById("nameInput"); nameInput.addEventListener("change", this.nameInputChanged); },
Run the app. It should work just as it did before: when you enter a name and click the button, it displays a greeting. When you flip the ToggleSwitch, the color of the greeting changes.
Step 4: Restore the app's state
You've almost recreated the functionality you had in your "Hello, world" app. The only thing you need to do now is restore the app's state when the user launches it.
You might remember that you had two types of app state to restore:
- The ToggleSwitch's state. You restore this regardless of how the app was shut down.
- The user name and greeting. You only restore this state if the app was successfully terminated the last time it ran.
To restore app state
Copy the code that restores the state of the ToggleSwitch from the "Hello, world" app. Add the code to the ready function in home.js.
// 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. // Retrieve the div that hosts the Toggle control. var toggleControlDiv = document.getElementById("toggleControlDiv"); // Retrieve the actual Toggle control. var toggleControl = toggleControlDiv.winControl; // Register the event handler. toggleControl.addEventListener("change", this.toggleChanged, false); // Retrieve the button and register our event handler. var helloButton = document.getElementById("helloButton"); helloButton.addEventListener("click", this.buttonClickHandler, false); // Retrieve the input element and register our // event handler. var nameInput = document.getElementById("nameInput"); nameInput.addEventListener("change", this.nameInputChanged); // Restore app data. var roamingSettings = Windows.Storage.ApplicationData.current.roamingSettings; // Restore the Toggle selection. var toggleSelection = roamingSettings.values["toggleSelection"]; if (toggleSelection) { toggleControl.checked = toggleSelection; // Apply the properties of the toggle selection var greetingOutput = document.getElementById("greetingOutput"); if (toggleControl.checked == true) { greetingOutput.setAttribute("class", "toggle-on"); } else { greetingOutput.removeAttribute("class", "toggle-on"); } }
You only want to restore the input box contents and personalized greeting if the app was successfully terminated the last time it ran. Unfortunately, your PageControl doesn't provide a built-in way to check the app's previous execution state: that info is provided to the onactivated event handler in your default.js file. But there's an easy solution to this problem: you just need to save the app's previous execution state in the sessionState object so that your PageControl can access it.
In your default.js file, add code to your onactivated handler to save the previous execution state. Save the state by adding a property to the sessionState object named
previousExecutionState
.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. } // Save the previous execution state. WinJS.Application.sessionState.previousExecutionState = args.detail.previousExecutionState; nav.history = app.sessionState.history || {}; nav.history.current.initialPlaceholder = true; // Optimize the load of the application and while the splash screen is shown, // execute high priority scheduled work. ui.disableAnimations(); var p = ui.processAll().then(function () { return nav.navigate(nav.location || Application.navigator.home, nav.state); }).then(function () { return sched.requestDrain(sched.Priority.aboveNormal + 1); }).then(function () { ui.enableAnimations(); }); args.setPromise(p); } });
In the home.js file, add code to your ready method that checks the
previousExecutionState
data. If the previous execution state is terminated, restore the personalized greeting (you can copy your code that does this from the default.js file in your "Hello, world" app.)// If the app was terminated last time it ran, restore the personalized // greeting. if ( WinJS.Application.sessionState.previousExecutionState === Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) { var outputValue = WinJS.Application.sessionState.greetingOutput; if (outputValue) { var greetingOutput = document.getElementById("greetingOutput"); greetingOutput.innerText = outputValue; } }
Here's the complete ready method.
// 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. // Retrieve the div that hosts the Toggle control. var toggleControlDiv = document.getElementById("toggleControlDiv"); // Retrieve the actual Toggle control. var toggleControl = toggleControlDiv.winControl; // Register the event handler. toggleControl.addEventListener("change", this.toggleChanged, false); // Retrieve the button and register our event handler. var helloButton = document.getElementById("helloButton"); helloButton.addEventListener("click", this.buttonClickHandler, false); // Retrieve the input element and register our // event handler. var nameInput = document.getElementById("nameInput"); nameInput.addEventListener("change", this.nameInputChanged); // Restore app data. var roamingSettings = Windows.Storage.ApplicationData.current.roamingSettings; // Restore the Toggle selection. var toggleSelection = roamingSettings.values["toggleSelection"]; if (toggleSelection) { toggleControl.checked = toggleSelection; // Apply the properties of the toggle selection var greetingOutput = document.getElementById("greetingOutput"); if (toggleControl.checked == true) { greetingOutput.setAttribute("class", "toggle-on"); } else { greetingOutput.removeAttribute("class", "toggle-on"); } } // If the app was terminated last time it ran, restore the greeting // and name output if ( WinJS.Application.sessionState.previousExecutionState === Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) { var outputValue = WinJS.Application.sessionState.greetingOutput; if (outputValue) { var greetingOutput = document.getElementById("greetingOutput"); greetingOutput.innerText = outputValue; } var inputValue = WinJS.Application.sessionState.nameInput; if (inputValue) { var nameInput = document.getElementById("nameInput"); nameInput.value = inputValue; } } },
Run the app. You've now duplicated the functionality in your original "Hello, world" app.
Step 5: Add another page
Most apps contain several pages. Now add another page to your app. Because you're using the Navigation App template, it's easy to add additional pages.
To add another page
In Solution Explorer, right-click the pages folder, and then click Add > New Folder.
Rename the folder "page2".
Right-click the page2 folder, and then click add Add > New Item....
In the Add New Item dialog, click Page Control from the list. In the Name text box, type "page2.html".
Click Add to add the PageControl. The new PageControl appears in Solution Explorer.
The new PageControl has three files: page2.css, page2.html, and page2.js.
Add the default WinJS CSS and JavaScript library references to the page header (you can copy and paste them from your home.html page).
<head> <meta charset="utf-8" /> <title>page2</title> <!-- WinJS references --> <link href="//Microsoft.Phone.WinJS.2.1/css/ui-dark.css" rel="stylesheet" /> <script src="//Microsoft.Phone.WinJS.2.1/js/base.js"></script> <script src="//Microsoft.Phone.WinJS.2.1/js/ui.js"></script> <link href="page2.css" rel="stylesheet" /> <script src="page2.js"></script> </head>
You've created a new page. In the next step, you learn how to navigate to it.
Step 6: Use the navigate function to move between pages
Right now, you have a second page but no way for the user to get to it. You need to update your home.html page by adding a link to page2.html.
To navigate between pages
Open your home.html page and add a link to page2.html.
<body> <!-- The content that will be loaded and displayed. --> <div class="fragment homepage"> <header aria-label="Header content" role="banner"> <h1 class="titlearea win-type-ellipsis"> <span class="pagetitle">Hello, world!</span> </h1> </header> <section aria-label="Main content" role="main"> <p>What's your name?</p> <input id="nameInput" type="text" /> <button id="helloButton">Say "Hello"</button> <div id="greetingOutput"></div> <br /> <div id="toggleControlDiv" data-win-control="WinJS.UI.ToggleSwitch" data-win-options="{title: 'Greeting Color'}"></div> <!--A hyperlink to page2.html. --> <p><a href="/pages/page2/page2.html">Go to page 2.</a></p> </section> </div> </body>
Run the app and click the link. It seems to work: the app displays page2.html.
However, there's a problem: the app performed a top-level navigation. Instead of navigating from home.html to page2.html, it navigates from default.html to page2.html.
What you want instead is to replace the content of home.html with page2.html.
Fortunately, the PageControlNavigator
control makes performing this type of navigation fairly easy. The PageControlNavigator
code (in your app's navigator.js file) handles the WinJS.Navigation.navigated event for you. When the event occurs, the PageControlNavigator
loads the page specified by the event.
The WinJS.Navigation.navigated event occurs when you use the WinJS.Navigation.navigate, WinJS.Navigation.back, or WinJS.Navigation.forward functions to navigate.
You need to call WinJS.Navigation.navigate yourself rather than using the hyperlink's default behavior. You could replace the link with a button and use the button's click event handler to call WinJS.Navigation.navigate. Or you could change the default behavior of the hyperlink so that when the user clicks a link, the app uses WinJS.Navigation.navigate to navigate to the link target. To do this, handle the hyperlink's click event and use the event to stop the hyperlink's default navigation behavior, and then call the WinJS.Navigation.navigate function and pass it the link target.
To override the default hyperlink behavior
In your home.js file, define a click event handler for your hyperlinks and make it a member of your PageControl. Name it
linkClickEventHandler
, and then add it after thenameInputChanged
handler.nameInputChanged: function (eventInfo) { var nameInput = eventInfo.srcElement; // Store the user's name for multiple sessions. var appData = Windows.Storage.ApplicationData.current; var roamingSettings = appData.roamingSettings; roamingSettings.values["userName"] = nameInput.value; }, linkClickEventHandler: function (eventInfo) { }
Call the preventDefault method to prevent the default link behavior (navigating directly to the specified page).
linkClickEventHandler: function (eventInfo) { eventInfo.preventDefault(); }
Retrieve the hyperlink that triggered the event.
linkClickEventHandler: function (eventInfo) { eventInfo.preventDefault(); var link = eventInfo.target; }
Call the WinJS.Navigation.navigate function and pass it the link target. (Optionally, you could also pass a state object that describes the state for that page. For more information, see the WinJS.Navigation.navigate page.)
linkClickEventHandler: function (eventInfo) { eventInfo.preventDefault(); var link = eventInfo.target; WinJS.Navigation.navigate(link.href); }
In the home.js file's ready function, attach the event handler to your hyperlinks.
The WinJS provides a WinJS.Utilities.query function that makes it easy to retrieve a number of elements on the page. The WinJS.Utilities.query function returns a QueryCollection, which provides additional methods for attaching and removing event handlers. Use the WinJS.Utilities.query collection and the listen method to attach your
linkClickEventHandler
.// 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. WinJS.Utilities.query("a").listen("click", this.linkClickEventHandler, false);
The nice thing about this approach is that it will work for any number of links on the page. You only have one link right now, but with this approach, you could add more links and you wouldn't have to change your code.
Run the app and click the link for page2.html.
Now the page displays using the proper navigation pattern.
Summary
Congratulations, you've finished the third tutorial! You learned how to create a project that uses the Navigation App template and how to use PageControl objects.
Download the sample
Do you need more info, or want to check your work? Download the Getting started with JavaScript sample.
Related topics
Getting started with JavaScript: Complete code for the tutorial series
For designers
For developers