Pages in Silverlight
Pages as a paradigm likely pre-date the web itself although they have been popularized by document-based markup languages deriving from SGML. Windows Presentation Foundation (WPF) with XAML further codifies Pages as a first-class UI element from its heretofore usage as a logical element. Looking back from Silverlight, this is as good a point in history to start as any. WPF has built-in support for reusable pages in its applications, and the task of navigation between them. It provides the infrastructure for declarative navigation via hyperlinks or programmatic navigation via NavigationService, and a journal that remembers which pages were navigated to or from. For XBAPs on IE7+, WPF supports integration of the journal with the browser's Back and Forward buttons, while elsewhere it displays a substitute navigation bar with this functionality (the limitation is an effect of how the XBAP host plugs into the browser). There is also a building block called PageFunction which introduces a paradigm for invoking pages as if they were methods, providing a neat little way to build wizard UI. Unlike WPF, Silverlight does not support these things, as of version 2. This post isn't about brooding that absence though -- instead we'll look at the lay of the land in Silverlight 2 with regard to pages and navigation, and look at common workarounds.
Although Silverlight does not have the Page type at this time, the term is currently used for its root visual UserControl. That root visual object is analogous to the root window in WPF, can only be set once for the lifetime of the app, and is effective once the Application's Startup event is raised. The act of navigating from one page to another is similar in principle to the act of removing one child element from an invisible root and adding another child in its place. [Actually given the journal it is more like switching Z-indices between siblings, so that one gets prominence while the other is made inconspicuous]. You can use this same principle in devising the notion of paging in a Silverlight app.
This recipe requires the following ingredients:
- App.xaml.cs: the code-behind for the Application
- Frame.cs: a user control which will serve as our "navigation frame". This has no UI.
- Page1.xaml/Page1.xaml.cs: a user control representing a unique "page". This has a button whose click event navigates away to Page 2.
- Page2.xaml/Page2.xaml.cs: similar to Page 1 in logic and function, only this represents the second page in your app.
Step 1: Create a navigation frame
This is a really simple user control. It has a public Navigate method which is passed a "page" object for which it duly resets its original content.
// Contents of Frame.cspublic class NavigationFrame : UserControl {// Navigate the frame to the specified content
public void Navigate(UIElement content)
{
// the existing content of this user control is
// discarded and the specified param is plugged in
// its place
this.Content = content;
}
}// Optional abstraction for page config
public static class Pages
{
// for simplicity we're using static properties,
// but this could just as easily be a URI to
// Type mapping table
public static UserControl HOME_PAGE = new Page1();
public static UserControl ANOTHER_PAGE = new Page2();
}
Step 2: Wire up the navigation frame as the app's RootVisual
Ordinarily the code generated by Silverlight tools (Visual Studio or Blend) will have a user control hooked up as Application.RootVisual within the Startup event handler. You will change this to hook up the navigation frame. Then hook up the user control to be the navigation frame's content (by invoking the NavigationFrame.Navigate method you created above).
// Contents of App.xaml.cs// the Startup event handler sets the root visual
private void App_Startup(object sender, StartupEventArgs e)
{
// Navigate to home page
Navigate(Pages.HOME_PAGE);
}public void Navigate(UIElement content)
{
// Create frame on an as-needed basis
if (this.frame == null)
{
this.frame = new NavigationFrame();
this.RootVisual = this.frame;
}// Navigate to content
this.frame.Navigate(content);}
Step 3: Build your pages
Now that the navigation infrastructure is ready, go ahead and create a couple pages. These are merely user controls which would otherwise have been hooked up directly as the RootVisual. That part is simple. An important attribute in this equation is the fact that pages need to provide the experience of "navigating away". You can use a hyperlink, a button, or trap mouse/key events. We'll use a button and insert the navigation logic in it's click event handler.
// Contents of Page1.xaml.cs// on btn click, this navigates to the second page
private void navigateButton_Click(object sender,
RoutedEventArgs e)
{
(Application.Current as App).Navigate(Pages.ANOTHER_PAGE);
}
Let's create another user control which will be navigated to.
// Contents of Page2.xaml.cs// on btn click, this navigates to the first page
private void navigateButton_Click(object sender,
RoutedEventArgs e)
{
(Application.Current as App).Navigate(Pages.HOME_PAGE);
}
Well, that's all there is to it really. Build and run, and you've got your simple little navigation application.
In this pattern, the navigation frame element:
- is the equivalent of the implicit navigation control
- is a container with no visible visual properties (e.g. Background, etc.)
- has only a Content element representing the current page
In the pattern, the page element:
- is the equivalent of the navigable Page
- has visuals or contains visual elements
- can be attached and detached from the tree on a navigate event (i.e. the current element is removed, and a new one added in its place)
Our example used the Navigate By Object approach. Note that we invoked the Navigate method and passed it a page object. You could tweak the logic so your pages can be given data as part of the Navigate call. You could just as easily add URI-to-type mapping into the Pages utility type. That would allow you to replicate the Navigate By URI approach which is more organic to content on the web. If you use the Navigate By Object approach and you have a journal that maintains a back/forward stack of pages you've previously navigated to, you can incur a working set hit. This is because your journal will retain references the page objects in memory for long intervals - usually the life of the app - preventing the garbage collector from reclaiming them. Using the Navigate By URI approach in such situations can offer better use of memory. This is because you can delay instantiation of page objects until when the Navigate needs to happen.
In closing, there is a good case for pages, navigation service, journalling, transition animations, deep linking etc. to be part of the Silverlight platform. This is certainly of interest to us as we look toward the next version of the runtime and it's framework. I'd like your thoughts and feedback on these features and how important they are to your scenarios.
Further reading:
- Navigation overview in WPF
- Page, Hyperlink and NavigationService
- Jesse Liberty: Multi-page applications in Silverlight
- Dave Relyea: Navigation with Transition Effects
PS: For Ben. Sorry this has been long overdue.
PPS: Thanks to Michael Weinhardt for valuable insights.
Update [06/16/2008]: Dave has a new post on navigation with transition effects. Link under the Further Reading section above.
Labels: Navigation, Silverlight, Windows Presentation Foundation, WPF