Demos from my MIX 2011 session - Part 1: Navigation Tips
I have attached the first of the demos from my MIX 2011 session. Note that I have attached two versions: the 'before' version to illustrate the problems, and the 'after' version to show the fixes. This post will walk you through each of the tips and describes the code changes being made:
Tip 1: Circular Navigation
Consider the following sequence in the demo app: Log in (empty user/pwd) -> Send a Message -> type something -> hit 'Home' button (in the App Bar) -> Get Stock Quotes -> hit 'Home' button -> Send a Message. While you do this, please monitor the helper control on the top right, which shows you the current navigation backstack. The result will look like this:
There are two small problems here that will likely confuse the user: (a) the user didn't get back to the message they were typing and (b) the user is deeper in the navigation stack than likely intended. To solve this and avoid confusion, you have two options (I chose option A for my demo):
Solution A: Avoid Circulation Navigation altogether. In this case, just get rid of the 'Home' App Bar Buttons and the user will be just fine using standard means of navigation.
Solution B: Instead of navigating forward to 'Home', walk the backstack programmatically, until you reach the Start Page. More details in this white paper by Yochay Kiriaty.
Tip 2: Dealing with transient Pages on the Backstack
If you now hit the back button a couple of times from the current state of the app, you will notice two issues: (a) we get back to the LoginPage and (b) we can't exit the app as the back key seems to get stuck. The latter is big no-no and will cause your app to fail certification. It's an easy fix though as the app was just marking the back key as handled for no valid reason, so removing the highlighted line will fix this:
protected override void OnBackKeyPress(System.ComponentModel.CancelEventArgs e)
{
e.Cancel = true;
base.OnBackKeyPress(e);
}
But how do we fix the root problem of hitting the LoginPage on our way back and exit the app cleanly? Again there are a few options to fix this. My personal preference is the following: I consider the LoginPage semantically not really a page that participates in navigation, but rather a control that overlays the StartPage and is shown only if the StartPage is navigated to without an existing user context. To implement this, I just move LoginPage code into a control (LoginControl) and include that in the StartPage. The StartPage now can control the visiblity of the LoginControl programatically as needed.
For completeness sake, alternate methods for solving the problem of dealing with transient pages on the backstack include cancelling the navigation or using UriMapper. Peter Torr has a blog post covering both.
Tip 3: Saving Page state on Tombstoning
Consider the following scenario in the demo app: We navigate to the "Send a Message" page and start typing a message. Now we want to find some data on the Internet and use copy & paste to include it in the message. For that we hit the Search button in the App Bar, do our search and then navigate back. The result is very frustrating: the message that we had typed tediously is now gone. Why is that, what happened here? Tombstone happened!
The short story is this: when the user navigates forward, away from your app, then your app will receive an entry on the global backstack, get a chance to save its state and then the process will be shut down. The longer story is documented here. So the problem in the demo app was that we didn't save our state before we got Tombstoned. The general recommendation for saving your Page's state is to persist it in OnNavigatedFrom() and restore it in OnNavigatedTo(), in MessagePage.xaml.cs:
protected override voidOnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
if (App.CurrentNavigationMode != NavigationMode.Back)
{
this.State["message"] = tbMsgBody.Text;
}
}
protected override voidOnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (this.State.ContainsKey("message"))
{
tbMsgBody.Text = this.State["message"] as string;
}
}
I want to point out one small optimization I have made here in the OnNavigatedFrom() call: I will not save the state if my navigation goes backwards. This is because when going backwards, the current page (and its state) drops off the backstack. There is no way the user could ever come back to that page instance, so there is no need really to persist that state. In this app it doesn't buy us much performance gain, but on pages that save large amounts of data, you can save precious CPU and battery with this optimization. Unfortunately, it's not completely straight forward to figure out the navigation mode, so I had to build a little static helper property. It's something we want to improve in the next version.
Tip 4: Saving Application state on Tombstoning
Since the state is being saved on a per page basis, it is important to perform Tombstone tests for each page in our app. So let's start a fresh instance of our app and test the Stock Quote Page: After the quotes are downloaded, hit the Windows button to navigate forward (which will Tombstone our app). Now watch what happens when we come back to our app: Without debugger you will just see a little flicker and then you are back at the start screen. With debugger attached you will see that we are crashing in the constructor of the StockPage class.
public StockPage()
{
InitializeComponent();
tbCurrentUser.Text = "CurrentUser: " + App.CurrentUser.Name;
this.Loaded += new RoutedEventHandler(StockPage_Loaded);
}
The reason for this is fairly simple: the constructor assumes that the static Application property for the user context is always set (since it's set during Login), however this won't be true when returning from Tombstone, if we don't preserve the application state correctly. The general recommendation for saving global app state is to do this in the Deactivated event and then restore it in the Activated event. The latter was missing in the demo app, hence the crash. So now we are adding the highlighted line to fix this problem (in App.xaml.cs):
private void Application_Activated(object sender, ActivatedEventArgs e)
{
App.CurrentUser = PhoneApplicationService.Current.State["userContext"] as UserContext;
}
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
PhoneApplicationService.Current.State["userContext"] = App.CurrentUser;
}
With these four tips/fixes, our app is now much more robust and consistent from a navigation perspective compared to where we started. There are still some other problems in the app, related to the various different phones states though. This will be the topic of my next blog post.