Page Navigation
Windows Phone navigation in Silverlight is based on the Silverlight for the browser navigation model. The navigation class model looks a little different in the phone SDK however. Rather than Silverlight 4’s Frame and Page controls, Silverlight for Windows Phone apps use the subclasses PhoneApplicationFrame and the PhoneApplicationPage (see Figure 3.4).
Figure 3.4. The PhoneApplicationFrame and PhoneApplicationPage classes are derived from the Frame and Page classes, respectively.
Page navigation in Silverlight for Windows Phone works in much the same way as page navigation in a web browser. PhoneApplicationFrame is analogous to the web browser, coordinating page transitions within your app.
Figure 3.5 depicts the display elements of a Silverlight for Windows Phone app.
Figure 3.5. Display elements of a Silverlight for Windows Phone app
The PhoneApplicationFrame is the host for PhoneApplicationPages and reserves space for the system tray and the application bar. The PhoneApplicationPage consumes all remaining space after the system tray and the application bar.
Navigation Using Unmapped URIs
There are numerous ways of allowing the user to perform page navigation. This section looks at using Buttons with code-beside to open external URLs, and at HyperlinkButtons, which can rely solely on XAML. In subsequent chapters you explore other techniques to perform navigation including the use of commands and a custom navigation service.
Internal URIs
When navigating to PhoneApplicationPages within an application, URIs either must be relative to the root directory of the project, and use the relative path syntax, as shown in the following excerpt:
Uri uri =
new
Uri(
"/DirectoryName/PageName.xaml",
UriKind.Relative);
or they must use the relative component URI format, such as that used in the following example:
Uri uri =
new
Uri
("/AssemblyName;component/PageName.xaml",
UriKind.Relative);
The assembly name segment must be the name of an assembly that is locatable at runtime. The name of a project’s output assembly can be found in the project properties editor by right-clicking on the project node in the Solution Explorer and selecting Properties, or by pressing Alt+Enter.
The HyperlinkButton control can be used to allow the user to navigate directly to a page within your application, as shown:
<
HyperlinkButton
NavigateUri
="/Directory/PageName.xaml" Content="Internal Page" />
External Navigation Using the Button Control
The Button control is a flexible way for determining user intent. A Button can be used for navigation by subscribing to its Click event, as shown in the following excerpt from the ProductDetailsView.xaml in the downloadable sample code:
<
Button
Click
="Button_ExternalLink_Click"
Tag
="{
Binding
Product
.ExternalUrl}"
Content
="External Page" />
The WebBrowserTask allows you to navigate to external URIs using the phone’s built-in web browser: Internet Explorer. This causes your app to be deactivated while the user views the page. You explore tasks in more detail in Chapter 12.
To provide the WebBrowserTask with the location of the web page, use the button’s Tag property. The Click event handler, which initiates the WebBrowserTask, is shown in the following excerpt:
void
Button_ExternalLink_Click(object
sender,RoutedEventArgs
e) {FrameworkElement
button = senderas
FrameworkElement
;if
(button ==null
|| button.Tag ==null
) {return
; }WebBrowserTask task =
new
WebBrowserTask
{ URL = button.Tag.ToString() }; task.Show(); }
External Navigation Using the HyperlinkButton Control
The disadvantage of using a Button control for links to external content is that it does not provide the familiar look and feel of a hyperlink. The HyperlinkButton control provides an easier way for navigating to pages within an application. There is a trick to using the HyperlinkButton with external URIs. Set its TargetName property to _blank, as shown in the following example:
<
HyperlinkButton
TargetName
="_blank"
NavigateUri
="http://create.msdn.com"
Content
="http://create.msdn.com" />
Hosting Web Content Within an App
An alternative to using the phone’s built-in Internet Explorer app is to host the content in a Microsoft.Phone.Controls.WebBrowser control.
The following excerpt from the WebBrowserView.xaml page, in the downloadable sample code, shows a WebBrowser placed within the main content grid of a page:
<
Grid
x
:
Name
="ContentGrid"
Grid.Row
="1">
<
phone:
WebBrowser
Source
="{
Binding
Url
}"/>
</
Grid
>
Here, the Source property of the WebBrowser is bound to the Url property of the viewmodel. The WebBrowser control is discussed in greater detail in Chapter 7, “Media and Web Elements.”
A dedicated web browser page can be used in your app to host all external content. To launch the dedicated web browser page, a relative URI can be constructed using the Binding.StringFormat property, as shown in the following excerpt:
<
HyperlinkButton
NavigateUri
="{
Binding
ExternalUrl,
StringFormat
=/
WebBrowser/\
{0\
}}"
Content
="External Page" />
Backslashes are used to escape the curly brackets in the StringFormat value.
The StringFormat property transforms the HyperlinkButton’s binding expression into the following:
string
.Format("/WebBrowser/{0}",
ExternalUrl);
URI mapping is used to pass the external URL as a query string parameter. This is explored further in a later section.
Passing Page Arguments Using Query Strings
Query strings allow for key value pairs to be embedded in a URL and passed to a page. Just like HTML web applications, Silverlight uses query string parameters for interpage communication.
The PhoneApplicationPage.NavigationContext property, which is initialized after the page is created, is used to retrieve the query string. Its QueryString property is an IDictionary of string key and value pairs. The following excerpt from the WebBrowserView.xaml, in the downloadable sample code, demonstrates how to retrieve a query string value:
void
OnLoaded(object
sender,RoutedEventArgs
e) {string
url;if
(NavigationContext.QueryString.TryGetValue("url"
,out
url)) { ViewModel.LoadPage(url); } }
Navigation History Stack
The Silverlight navigation infrastructure maintains a history of pages that have been loaded. Each time an app navigates to a different page, the current page’s OnNavigatedFrom method is called and the page is placed on the history stack (see Figure 3.6).
Figure 3.6. Pages are placed on the history stack.
While on the history stack, the page remains in memory unless the application is tombstoned or closed. This means that subscribing to the PhoneApplicationService.Deactivated event provides the page with the opportunity to save its transient state. It is, however, preferable to use the page’s OnNavigatedFrom method for the saving of page state.
Using the Deactivate event to save page state runs the risk of slowing down deactivation when all pages on the history stack are saving their state simultaneously.
Restoration of transient state should occur with the page’s OnNavigatedTo method. The OnNavigatedTo method is called when the PhoneApplicationFrame navigates to the page. This is triggered by the following actions:
- Navigation to a specified page URI occurs using one of various navigation methods such as the PhoneApplicationFrame.Navigate method.
- The NavigationService.GoBack method is called.
- The user presses the hardware Back button.
- The page is the current page when the app moves from the tombstoned or dormant state to the running state.
- The page is the app’s start page and the app is launched.
URI Mapping
Relying on URIs that include the full path to each page in your app can make your app brittle and makes it harder to change the physical location of individual pages. If a page is moved, all references to that file must be updated. This can lead to maintainability issues as the size of the project grows.
The URI mapping system of Silverlight allows requests for a URI to be routed to another URI, and uses a single configuration point for the management of page URIs. Mapped URIs can be made shorter and are, thus, less subject to typographical errors. They also allow the exclusion of technology specific information, such as the .xaml page file extension, making it easier to retarget business logic for different platforms.
To use URI mapping, you must assign a System.Windows.Navigation.UriMapper instance to the UriMapper property of an app’s PhoneApplicationFrame. This can be done in XAML, as shown in the following excerpt from the App.xaml file in the downloadable sample code:
<
Application
x
:
Class
="DanielVaughan.WindowsPhone7Unleashed.Examples.App"
xmlns
="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns
:
x
="http://schemas.microsoft.com/winfx/2006/xaml">
<
Application.RootVisual
>
<
phone
:
PhoneApplicationFrame
x
:
Name
="RootFrame">
<
phone
:
PhoneApplicationFrame.UriMapper
>
<
navigation
:
UriMapper
>
<
navigation
:
UriMapper.UriMappings
>
<
navigation
:
UriMapping
Uri
="/ProductDetails/{productId}"
MappedUri
="/Navigation/ProductDetailsView.xaml?productId={productId}" />
<
navigation
:
UriMapping
Uri
="/WebBrowser/{url}"
MappedUri
="/WebBrowser/WebBrowserView.xaml?url={url}" />
</
navigation
:
UriMapper.UriMappings
>
</
navigation
:
UriMapper
>
</
phone
:
PhoneApplicationFrame.UriMapper
>
</
phone
:
PhoneApplicationFrame
>
</
Application.RootVisual
>
<!-- Content omitted. -->
</
Application
>
The UriMapping class contains a Uri property and a MappedUri property. When navigation is requested from the Uri value, it is rerouted to the MappedUri property.
By using the curly brace syntax, as shown in the previous excerpt, a substring of the requested URI can be transplanted into the rerouted MappedUri value. This is especially useful when you want to target the same page using different URIs and allows the query string to be used to convey the action to be undertaken by the page.
In the previous excerpt you see a UriMapping for the ProductDetailsView page. The ProductDetailsView displays detailed information for a particular product, identified by a query string parameter. When navigating to the ProductDetails page, if the requested URL is /ProductDetails/2, this is rerouted to /Navigation/ProductDetailsView.xaml?productId=2.
If you were to request the ProductDetailsView page using the NavigationService, as shown in the following example, the request would be rerouted accordingly:
NavigationService.Source =new
Uri(
"/ProductDetails/2",
UriKind
.Relative);
The product to be displayed can then be determined in the ProductsDetailsView by reading the productId query string parameter, as demonstrated in the following excerpt:
protected override void
OnNavigatedTo(NavigationEventArgs
e) {base
.OnNavigatedTo(e);string
productIdString = NavigationContext.QueryString["productId"
];int
productId =int
.Parse(productIdString); ViewModel.LoadProduct(productId); }
You see later in this chapter how the viewmodel uses the product ID to retrieve the product information from a WCF service.
Navigation Using the NavigationService
The PhoneApplicationPage class exposes a public NavigationService property, which allows direct control over navigation.
The NavigationService.Navigate method causes the frame to load the specified PhoneApplicationPage, like so:
NavigationService
.Navigate(new
Uri("/DirectoryName/PageName.xaml",
UriKind
.Relative));
The URI must be either a path relative to the project’s root directory, as shown in the previous example, or a relative component URI such as in the following example:
NavigationService
.Navigate(new
Uri("/AssemblyName;component/PageName.xaml",
UriKind
.Relative));
The NavigationService.Source property allows you to retrieve the URI of the current page. Setting the Source property performs the same action as using the Navigate method; the frame loads the page at the specified URI. See the following example:
NavigationService
.Source =new
Uri("/DirectoryName/PageName.xaml"
,UriKind
.Relative);
Routing is also enabled for the NavigationService, which means that mapped URIs can be used instead of relative URIs.
If you examine the API of the NavigationService, you will likely wonder what the difference is between the CurrentSource property and the Source property. The answer is that the CurrentSource property does not change until navigation has completed. Conversely, the Source property changes as soon as navigation is initiated.
Backward Navigation
The NavigationService maintains the app’s navigation history, via an internal Journal instance. This allows the GoBack method of the NavigationService to move to the previous page in the history stack.
Forward Navigation
Unlike Silverlight for the browser, the GoForward method of the NavigationService does not allow forward navigation and raises an InvalidOperationException when called. Consequently, the CanGoForward property always returns false.
Handling Page Navigation
The PhoneApplicationPage extends the System.Windows.Controls.Page class, which has a number of virtual methods called when the page is brought into view or removed from view by the PhoneApplicationFrame (see Figure 3.7).
Figure 3.7. PhoneApplicationPage inherits Page navigation methods.
The OnNavigatingFrom method is called before a page is removed from view by the PhoneApplicationFrame, and the OnNavigatedFrom method is called after navigation occurs. Conversely, the OnNavigatedTo method is called when the frame brings the page into view.
Cancelling Navigation
The OnNavigatingFrom method offers the opportunity to cancel navigation using the NavigatingCancelEventArgs parameter.
NavigatingCancelEventArgs has the following properties:
- NavigationMode—An enum value that indicates the type of navigation. This value may be Back, Forward, New, or Refresh.
- Uri—The destination URI.
- Cancel—Setting this value to true cancels the navigation.
The NavigationCancelEventArgs class subclasses CancelEventArgs, which provides the Cancel property. By setting this property to true, the page can prevent the navigation from occurring, as shown in the following excerpt:
protected override void
OnNavigatingFrom( System.Windows.Navigation.NavigatingCancelEventArgs
e) { base.OnNavigatingFrom(e);MessageBoxResult
boxResult =MessageBox
.Show("Leave this page?"
,"Question"
,MessageBoxButton
.OKCancel); if (boxResult !=MessageBoxResult
.OK) { e.Cancel =true;
} }
Cross-Page Communication
Once navigation has occurred, there remains an opportunity for the previous page to interact with the current page from the previous page’s OnNavigatedFrom method. This is achieved using the Content property of the NavigationEventArgs, which provides the destination PhoneApplicationPage object.
To see this in action, place a breakpoint in the OnNavigatedFrom method. When the breakpoint is hit, notice that the page being navigated to has already been instantiated and is provided in the Content property of the NavigationEventArgs (see Figure 3.8).
Figure 3.8. The Content property of the NavigationEventArgs contains the page being navigated to.
The Uri property of the NavigationEventArgs contains the URI of the destination page, including any query string that may be present.
Page Redirection
The OnNavigatingFrom method allows you to intercept a navigation event and to even cancel the navigation if needed. Additionally, there may be times when you want to redirect the user to a different URI based on some conditional logic.
The NavigationService, however, does not support overlapping navigation. That is, you are unable to cancel an existing navigation and immediately commence another.
You can, however, cancel navigation and schedule navigation to a different Uri using the page’s Dispatcher property, as shown in the following excerpt:
protected override void
OnNavigatingFrom(NavigatingCancelEventArgs
e) {if
(e.Uri.ToString().Contains("RequestedUrl"
)) { e.Cancel =true;
/* Perform the redirect on the UI thread. */
Dispatcher.BeginInvoke(() => NavigationService.Navigate(new
Uri("RedirectUrl"
,UriKind
.Relative))); }base
.OnNavigatingFrom(e); }
By using the Dispatcher to invoke the lambda expression, which performs the call to the NavigationService, you allow the current navigation to complete first. This works because the UI thread can be thought of as a queue of prioritized delegates, all waiting in turn to be executed. Once all the Navigating event handlers have been serviced, the delegate represented by the lambda expression will be taken out of the queue and performed by the UI thread. This technique, of using the Dispatcher to enqueue an action, is also useful when working with some UI controls, whose event handlers may be called before the control is finished reacting to a user action.
Hardware Back Button
The hardware Back button is analogous to the Back button on a web browser. However, when the user presses the Back button, past the first page of a phone app, the app is closed. This is in contrast to the phone’s hardware start button, which merely causes an app to be deactivated.
To determine whether navigation is occurring because the hardware Back button was pressed or the navigation was initiated by a call to NavigationService.GoBack, use the NavigatingEventArgs.NavigationMode property as shown:
protected override void
OnNavigatedFrom(NavigationEventArgs
e) {base
.OnNavigatedFrom(e);if
(e.NavigationMode ==NavigationMode
.Back) {// Back button pressed.
} }
The Back key button can also be cancelled by overriding the PhoneApplicationPage.OnBackKeyPress, as shown in the following excerpt:
protected override void
OnBackKeyPress(CancelEventArgs
e) {base.OnBackKeyPress
(e); e.Cancel =true;
}
OnBackKeyPress is called before OnNavigatedFrom, and if the Back button is cancelled, then OnNavigatedFrom is not called at all.
Creating an Application Splash Screen
Windows Phone Silverlight projects have baked-in support for application splash screens. To create a splash screen it is simply a matter of placing a jpg image called SplashScreenImage.jpg, with the dimensions of 480 by 800 pixels, in the root directory of your project. Ensure that its Build Action is set to Content (see Figure 3.9).
Figure 3.9. Creating an application splash screen
Using an image for a splash screen does not, however, prevent an application from being closed by the OS if the first page takes longer than 10 seconds to load. If your application’s first page takes longer than this to load, it is best to overlay the content with a loading indicator and perform the time consuming initialization on a background thread. Once loading is complete, the indicator can be dismissed.
The ProductsView and ProductsViewModel classes, located in the Navigation directory of the WindowsPhone7Unleashed.Examples project in the downloadable sample code, demonstrate this principle (see Figure 3.10).
Figure 3.10. A custom loading screen
The ProductsView page uses a StackPanel to present an indeterminate progress bar to the user while the viewmodel is loading, as shown in the following excerpt:
<
StackPanel
Grid.Row
="1"
Visibility
="{
Binding
Loaded
,
Converter
={
StaticResource
BooleanToVisibilityConverter
},
ConverterParameter
=Collapsed}"
Height
="150" >
<
TextBlock
Text
="Loading..."
Style
="{
StaticResource
PhoneTextTitle2Style
}"
HorizontalAlignment
="Center"
Margin
="20"/>
<
toolkit
:
PerformanceProgressBar
IsIndeterminate
="True" />
</
StackPanel
>
The Visibility property of the StackPanel is assigned via a binding to the viewmodel’s Loaded property. To convert the boolean Loaded property to a Visibility type, a custom IValueConverter called BooleanToVisibilityConverter is used (see Listing 3.3). The class is located in the ValueConverters directory of the WindowsPhone7Unleashed project, in the downloadable sample code.
Listing 3.3. BooleanToVisibility Class
public class
BooleanToVisibilityConverter
:IValueConverter
{public object
Convert(object
value,Type
targetType,object
parameter,CultureInfo
culture) {string
paramValue = (string
)parameter;if
(value ==null
|| (bool
)value) {return
paramValue =="Collapsed"
?Visibility
.Collapsed :Visibility
.Visible; }return
paramValue =="Collapsed"
?Visibility
.Visible :Visibility
.Collapsed; }public object
ConvertBack(object
value,Type
targetType,object
parameter, CultureInfo culture) {string
paramValue = (string
)parameter;if
(value ==null
|| (Visibility
)value ==Visibility
.Visible) {return
paramValue !="Collapsed"
; }return
paramValue =="Collapsed"
; } }
The ConverterParameter attribute determines what value to assign to the Visibility property if the binding value is true. If the Loaded property of the viewmodel is true, then the Visibility property will set to Visibility.Visible.
To hide the rest of the content during loading, the same technique is employed for the main content control.
<StackPanel
Grid.Row
="1"
Margin
="10"
Visibility
="{
Binding
Loaded
,
Converter
={
StaticResource
BooleanToVisibilityConverter
},
ConverterParameter
=Visible}">
<
ScrollViewer
>
<!-- Content omitted. -->
<
ScrollViewer
>
</
StackPanel
>
Here the ConverterParameter attribute is set to Visible, so that its Visibility is set to Visible when the viewmodel’s Loaded property is true and Collapsed when it is false.
The code listings for the ProductsView page and associated files are provided in the following section.