Share via


Saving and Restoring State in a Windows 8 XAML App

This post and episode 59 of my Visual Studio Toolbox show cover the same topic. Although I published this and the show on the same day, I wrote this first.

When you switch from one Windows 8 app to another, Windows suspends the first app. The app stays in memory, but it is not running and consumes no CPU time. While the app is suspended, Windows retains its state and data in memory and restores it when the app is resumed. That way, when you return to the app you can pick up where you left off.

This is very handy, but you cannot rely on it . That is because Windows won’t necessarily leave the app’s state and data in memory forever. Windows can terminate the app after some time to free up memory or to save power. When you launch the app again, you cannot return to where you were unless you saved its state. So it is up to you to save state and user data in your apps. That way, you guarantee that users can always pick up where they left off.

Let’s see how this is accomplished. First create a new C# Windows Store project and use the Grid App template. Select Build|Deploy Solution to build and deploy the app. Open the Task Manager. Select View|Status values|Show suspended status. Run the app from the Start screen and select an item, for instance Item 5 in Group 1. Alt+Tab back to the Task Manager and notice that after about ten seconds the Task Manager displays that the app has been suspended. Notice that it still has memory allocated to it. Switch back to the app. You are still on the item you selected.

What happens when Windows terminates the app after suspending it? To see this, close the app. Run the app from within Visual Studio and select any item. In Visual Studio, select Suspend and shutdown from the Suspend dropdown list. Run the app again from within Visual Studio and you will see the item you selected. The state was saved and restored.

Picture1

What code makes this happen? In the App.xaml.cs scroll to the bottom and notice the OnSuspending method. This is invoked when application execution is being suspended.

 private async void OnSuspending(object sender, SuspendingEventArgs e)
{
    var deferral = e.SuspendingOperation.GetDeferral();
    await SuspensionManager.SaveAsync();
    deferral.Complete();
}

SuspendingOperation gets the app suspending operation and GetDeferral requests that the suspending operation be delayed. The SuspensionManager class captures global session state. SaveAsync saves the current session state. After the state is saved, the call to Complete notifies Windows that the app is ready to be suspended.

SuspensionManager is not a class in the Windows Runtime. It was created by Visual Studio and is in the project’s Common folder. It has a SaveAsync method to save state and a RestoreAsync method to restore state.

Scroll up to the OnLaunched method in App.xaml.cs. This code creates a Frame instance named rootFrame. The code then calls SuspensionManager.RegisterFrame and passes rootFrame as an argument. This enables the Frame’s navigation history to be saved and restored.

The next bit of code shows you why you went back to the selected item after you ran it again after suspending and shutting down. PreviousExecutionState gets the execution state of the app before it was activated. In other words, what state was it in before you ran it again? If the state was Terminated, then the app calls RestoreAsync and restores the app’s state.

 if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
    {
        // Restore the saved session state only when appropriate
        try
        {
            await SuspensionManager.RestoreAsync();
        }
        catch (SuspensionManagerException)
        {
            //Something went wrong restoring state.
            //Assume there is no state and continue
        }
    }

We have seen so far that when the app is suspending, the state is saved and when the app is launched, the state is restored if the app was terminated. And we forced the termination from within Visual Studio. What happens if the user closes the app? Run the app from the Start screen. Select an item and press Alt+F4 to close the app. Switch over to the Task Manager and see that the app sticks around for about ten seconds, then gets suspended and then disappears. Now run the app again from the Start screen. Notice that the main page appears, not the selected item. When you shut down the app, the state was saved. However, it is not being restored. That is because the PreviousExecutionState is not Terminated. It is ClosedByUser.

If you want to restore state in the event the user closed the app, you can use the following code:

 if (args.PreviousExecutionState == ApplicationExecutionState.Terminated
    || args.PreviousExecutionState == ApplicationExecutionState.ClosedByUser)
{
    // Restore the saved session state only when appropriate
    try
    {
        await SuspensionManager.RestoreAsync();
    }

According to the official design guidance, when the app is terminated, you should restore the app to the way it was when the user left it. However, when the app is closed by the user, you should not restore state but instead should start the app in its initial state.

Now that we have established that state is being saved and restored, let’s examine where and how this happens. Open the LayoutAwarePage.cs file in the Common folder. The LayoutAwarePage class contains OnNavigatedTo and OnNavigatedFrom event handlers that are invoked when the app navigates to and from a page. GroupedItemsPage, GroupDetailPage and ItemDetailPage, the three pages in the app, each inherit from LayoutAwarePage and therefore are automatically wired up to these events.

 protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Returning to a cached page through navigation shouldn't trigger state loading
    if (this._pageKey != null) return;

    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    this._pageKey = "Page-" + this.Frame.BackStackDepth;

    if (e.NavigationMode == NavigationMode.New)
    {
        // Clear existing state for forward navigation when adding a new page to the
        // navigation stack
        var nextPageKey = this._pageKey;
        int nextPageIndex = this.Frame.BackStackDepth;
        while (frameState.Remove(nextPageKey))
        {
            nextPageIndex++;
            nextPageKey = "Page-" + nextPageIndex;
        }

        // Pass the navigation parameter to the new page
        this.LoadState(e.Parameter, null);
    }
    else
    {
        // Pass the navigation parameter and preserved page state to the page, using
        // the same strategy for loading suspended state and recreating pages discarded
        // from cache
        this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]);
    }
}
 
 
 
 protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
    var pageState = new Dictionary<String, Object>();
    this.SaveState(pageState);
    frameState[_pageKey] = pageState;
}

Set a breakpoint on the first line of code in both OnNavigatedTo and OnNavigatedFrom and run the app. Look at the NavigationEventArgs instance e in OnNavigatedTo and you can see that the app has just navigated to the grouped items page.

Picture2

_pageKey is null so the app next calls SuspensionManager.SessionStateForFrame, which provides storage for session state associated with a frame. this.Frame is the Frame that was registered with the SuspensionManager earlier. It contains the GroupedItemsPage page and is passed as the argument to SessionStateForFrame.

Picture3

Step into SessionStateForFrame. The first line checks to see if state has been saved for this Frame yet. Since the app is just starting, there is none and no additional code runs. Step out of SessionStateForFrame and back into OnNavigatedTo. Because the app just started, this.Frame.BackStackDepth is 0, which records that this was the first page you visited. The next batch of code adds the page to the navigation stack. Step into the call to LoadState. The LoadState method is empty in LayoutAwarePage. However, GroupedItemsPage has its own implementation, so the OnNavigatedTo event handler calls GroupedItemsPage.LoadState, shown below, and passes in e.Parameter, which is “AllGroups”. Therefore, the app’s main page shows all of the groups of items.

Picture4

Continue execution and from the main page, select an item. Because you are leaving the main page, the OnNavigatedFrom event fires. The call to SesstionStateForFrame returns no state because this is the first time you have been on this page. After the SaveState method is called, pageState has nothing in it. There was no state associated with the main page to save. Continue execution and the OnNavigatedTo event occurs again because you have now navigated to the item details page. Notice that this.Frame.BackStackDepth is now 1. Also notice that e.Parameter, which is passed to LoadState, denotes the item you selected, eg “Group-2, Item-1”. This is the UniqueId of the item and it is passed to the ItemDetailsPage by the ItemView_ItemClick event handler from the grouped items page.

 void ItemView_ItemClick(object sender, ItemClickEventArgs e)
{
    // Navigate to the appropriate destination page, configuring the new page
    // by passing required information as a navigation parameter
    var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
    this.Frame.Navigate(typeof(ItemDetailPage), itemId);
}

After the call to SessionStateForFrame, notice that Frame now has saved state, consisting of the fact that you started on the main page.

Picture5c

Step into LoadState, which takes you to the LoadState method in ItemDetailPage.xaml.cs. The navigation parameter is “Group-2, Item-1” and pageState is null, since this is the first time you visited this page.

Continue execution and you are on the detail page for the selected item. Now from Visual Studio, suspend and shutdown the app as you did before. Notice the OnNavigatedFrom event occurs. The code retrieves the frame’s state and then calls SaveState. Step into that method, which you will find at the bottom of ItemDetailPage.xaml.cs.

 protected override void SaveState(Dictionary<String, Object> pageState)
{
    var selectedItem = (SampleDataItem)this.flipView.SelectedItem;
    pageState["SelectedItem"] = selectedItem.UniqueId;
}

This method takes the empty pageState Dictionary and adds a SelectedItem key to it with a value of the selected item’s UniqueId.

Picture5d

When the app is suspended, SuspensionManager.SaveAsync gets called. The code calls the SaveFrameNavigationState for the registered frame.

 private static void SaveFrameNavigationState(Frame frame)
{
    var frameState = SessionStateForFrame(frame);
    frameState["Navigation"] = frame.GetNavigationState();
}

GetNavigationState serializes the Frame navigation history into a string. In this example, that string contains the following:

"1,2,1,21,App6.GroupedItemsPage,12,9,AllGroups,19,App6.ItemDetailPage,12,14,Group-2-Item-1"

The SaveAsync method next writes the state to disk. That state includes the item you were viewing when you suspended the app.

Picture7

Put a breakpoint on the call to RestoreAsync in the OnLaunched method in App.xaml.cs. Now run the app again from within Visual Studio. Step into RestoreAsync. This method retrieves the state from disk and stores it in _sessionState.

Picture7b

Step into RestoreFrameNavigationState. The SetNavigationState method of the Frame class restores the navigation history of the suspended app. This history is contained in frameState[“Navigation”]. SetNavigationState calls the OnNavigatedTo method of the current page, which it knows by traversing the navigation history.

Picture8

Continue execution and you will be back in the OnNavigatedTo event handler. And notice that the page is ItemDetailPage and the parameter is the unique id of the item you were viewing.

Picture9

And that is why you wind up back on the item you selected!

How can you use these techniques in your apps? To see this, I added state management to my SQLite demo app. This is a simple app showing customers and projects. I first made two changes to the app, which you can find here:

  1. In the original app, the code to retrieve data for each page is in the page’s OnNavigatedTo method. I moved that code into the LoadState method instead.
  2. When you are on the main page and you tap a customer, you navigate to the projects page and you see the projects for that customer. Similarly, when are on the projects page and you tap a project, you see the details for that project. In the original app, the customer is passed to the projects page and the project is passed to the projects page. I changed this so that only the customer and project id are passed. I did this for two reasons. First, I did not want to have to store the entire customer or project in state. Secondly, in between the time the app was suspended and resumed, the customer or project information could have changed. So I pass the id and then retrieve the customer or project when the page loads.

In ProjectsPage.xaml.cs, the LoadState method contains the following code:

 if (pageState != null && pageState.ContainsKey("CurrentProjectId"))
{
    navigationParameter = pageState["CurrentProjectId"];
}

If the user is viewing projects for a customer and the app terminates, the user will return to the projects page and that customer’s projects will display.

The SaveState method for the projects page contains the following code to save the current customer’s id:

 pageState["CurrentCustomerId"] = App.CurrentCustomerId;

In CustomerPage.xaml.cs, the LoadState method contains the following code:

 if (pageState != null && pageState.ContainsKey("CurrentCustomerId"))
{
    navigationParameter = pageState["CurrentCustomerId"];
}
if (navigationParameter == null)
{
    customer = new CustomerViewModel();
    PageTitle.Text = "New customer";
}
else
{
    customerViewModel = new CustomerViewModel();
    customer = customerViewModel.GetCustomer((int)navigationParameter);
    App.CurrentCustomerId = customer.Id;
    PageTitle.Text = customer.Name;
}
this.DataContext = customer;

if (pageState != null)
{
    if (pageState.ContainsKey("Name"))
    {
        NameTextBox.Text = pageState["Name"].ToString();
    }
    if (pageState.ContainsKey("City"))
    {
        CityTextBox.Text = pageState["City"].ToString();
    }
    if (pageState.ContainsKey("Contact"))
    {
        ContactTextBox.Text = pageState["Contact"].ToString();
    }
}
 
 
 
 
 
 

If the user is viewing the information for a customer and the app terminates, the user will return to the customer page and see that customer. But what if the user was making changes to the customer’s information? The code above restores the contents of the three textboxes on the page so the changes are not lost.

The values in the textboxes are saved in the page’s SaveState method:

 if (customer.Id > 0)
{
    pageState["CurrentCustomerId"] = customer.Id;
}
pageState["Name"] = NameTextBox.Text;
pageState["City"] = CityTextBox.Text;
pageState["Contact"] = ContactTextBox.Text;

Similar code is used in ProjectPage.xaml.cs to save project state including any pending edits.

Comments

  • Anonymous
    January 31, 2013
    This is a very valuable and helpful article. I really enjoy it.

  • Anonymous
    June 17, 2013
    This is a very well written and useful article about a critical part of Windows Store app development. Thanks.

  • Anonymous
    July 10, 2013
    It Helps me understand that part so much Thank you Robert

  • Anonymous
    July 26, 2014
    i like this article

  • Anonymous
    March 29, 2015
    I'm not sure this code is completely correct.  Suspending causes the state to be saved, and OnNavigatedFrom to be called without calling OnNavigatingFrom. When a resume occurs, your code above doesn't call SuspensionManager.RestoreAsync() except in particular cases.  As a result, on a normal resume, any event handlers that might be wired up in OnNavigatedTo don't get rewired. This forum post social.msdn.microsoft.com/.../bug-in-suspensionmanagersaveasync-or-framegetnavigationstate suggests that OnNavigatingFrom might be the place to unwire. I'm sure tons of apps are affected by this.