Windows 8.1 and Windows 8.1 Phone Convergence. Part 2: App Lifecycle

12 June 2014

In this post we examine the converged lifecycle of WinRT 8.1-based Windows 8.1 and Windows Phone 8.1 apps:

  1. The basic events raised during the app lifecycle
  2. Lifecycle events and associated state management when using the SuspensionManager and NavigationHelper classes provided by Microsoft

In the next part (part three) of this series of posts I discuss app lifecycle in relation to a simple MVVM-based approach, which provides automatic load/save of view model state.

Basic Lifecycle Events

The diagram above shows the basic lifecycle events for both Windows 8.1 (Win8.1 hereafter) and Windows Phone 8.1 (WP8.1) WinRT-based apps.

Summary of WinRT 8.1 App Lifecycle

The lifecycle for WinRT 8.1 apps presents a number of important differences for Windows Phone developers coming from a WP8.0 (Silverlight) background:

  • There's only one foreground app running at a time, all other apps are suspended (in "suspended animation", but still in-memory) or terminated (no longer in-memory)
  • The lifecycles of Win8.1 and WP8.1 are almost identical (we'll highlight any differences later on)
  • When an app is suspended events are raised (e.g. App.Suspending) that give it a chance to save state
  • When an app is resumed from being suspended (either from the app tile, use of the hardware Back button, or any task switcher mechanism), the same app process is resumed - the app's memory state is preserved, so variable values are retained.

    This is different from the way things were handled in WP8.0, where by default, if a user "re-launches" a suspended app by tapping on its tile, a fresh instance of the app is created and all previous state is discarded.

    The App.Resuming event is raised when the app is resumed
  • When an app is terminated, no additional events are raised.

    A terminated app is always suspended initially, and can then be terminated in the future. The developer must treat a suspension as if it will become a termination at some point later. Thus, any important state data must be persisted during processing of the App.Suspending event
  • When an app is re-launched from having been "recently" terminated, either by the OS because of lack of system resources, or by the user (e.g. tapping the "X" button in the WP8.1 task switcher), the user will be unaware of the termination and will expect the app to be in the exact same state as previously.

    It's the job of the developer to maintain the "always-running" illusion and load/save app state as necessary. This includes transient state (e.g. a part-completed edit), page navigation history, etc.

If you create a new Visual C# Store Apps > Universal Apps > Blank App solution in Visual Studio 2013 you'll get an app template that demonstrates the basic lifecycle events.

The App.xaml.cs generated contains a few #if...#endif related to WP8.1. For this example, these sections of code can safely be removed, as they all just relate to annimated transitions.

The App.xaml.cs code will look as shown below. Note that Visual Studio automatically adds the override of OnLaunched, if required, you have to manually add handlers for the Suspending and Resuming events:

using System;
using System.Diagnostics;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace WinRT81BasicLifecycle
{
    public sealed partial class App : Application
    {
        public App()
        {
            this.InitializeComponent();
            this.Suspending += this.OnSuspending;
            this.Resuming += OnResuming;
        }

        protected override void OnLaunched(LaunchActivatedEventArgs e)
        {
            Debug.WriteLine("App.OnLaunched");

            var rootFrame = Window.Current.Content as Frame;

            if (rootFrame == null)
            {
                rootFrame = new Frame {CacheSize = 1};
                Window.Current.Content = rootFrame;
            }

            if (rootFrame.Content == null)
            {
                if (!rootFrame.Navigate(typeof(MainPage), e.Arguments))
                {
                    throw new Exception("Failed to create initial page");
                }
            }

            Window.Current.Activate();
        }

        private void OnSuspending(object sender, SuspendingEventArgs e)
        {
            Debug.WriteLine("App.OnSuspending");
        }

        private void OnResuming(object sender, object o)
        {
            Debug.WriteLine("App.OnResuming");
        }
    }
}

I added a second blank page to both the Win8.1 and WP8.1 projects, along with a button to allow navigation between pages. I also added Debug.WriteLine() statements to output the sequence of events raised. For example, here's the code from the first page (the code for both Win8.1 and WP8.1 is identical):

using System.Diagnostics;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace WinRT81BasicLifecycle
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();

            this.NavigationCacheMode = NavigationCacheMode.Required;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            Debug.WriteLine("MainPage.OnNavigatedTo");
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            Debug.WriteLine("MainPage.OnNavigatedFrom");
        }

        private void OnGotoPage2(object sender, RoutedEventArgs e)
        {
            Frame.Navigate(typeof(Page2));
        }
    }
}

To examine the sequence of lifecycle events, I:

  1. Launched the app
  2. Navigated from the first to the second page
  3. Used the Visual Studio Process > Lifecycle Events debug dropdown to Suspend and shutdown the app. This is necessary because when WinRT 8.1 apps are running under the debugger they (by design) do not raise normal suspend/resume lifecycle events:

The events raised were identical for the Win8.1 and WP8.1 apps:

  • App.OnLaunched
  • Page1.OnNavigatedTo
  • Page1.OnNavigatedFrom
  • Page2.OnNavigatedTo
  • App.OnSuspending

It can be seen that lifecycle events are raised at both the application and page level. At the app-level we have the OnLaunched, Suspending and Resuming events. At the page-level, we have OnNavigatedTo and OnNavigatedFrom.

The main uses for handling life cycle events are the loading and saving of page and app-global state, the presentation and updating of UI components, and the potential refreshing of data from various sources. In this post we're concentrating on mechanisms for the loading and saving of state, however, all the techniques described may also be used for purposes other than state management.

Using SuspensionManager and NavigationHelper

Using the basic lifecycle events outlined previously, we have the basis for a useful state management mechanism. The piece missing at the moment is a repository to use for holding state. This is where the SuspensionManager class comes in handy.

When you add a Basic page, or other non-Blank type of page to a WinRT 8.1 project Visual Studio offers to add some common support classes, including SuspensionManager and NavigationHelper:

SuspensionManager uses an app's Frame object and the app-level lifecycle events OnLaunched and OnSuspending to provide a repository and load/save mechansim for state data.

NavigationHelper makes use of SuspensionManager to store page-level state data. It does this when hooked into a page's OnNavigatedTo and OnNavigatedFrom event handlers. It also provides a simplified "forward/back" navigation mechanism.

The use of SuspensionManager requires a little bit of plumbing code to be added to App.xaml.cs (this is done automatically for you when you create a new project based on a template other than Blank Page):

namespace WP81DemoLifeCycleAdv
{
    public sealed partial class App : Application
    {
        :

        protected override async  void OnLaunched(LaunchActivatedEventArgs e)
        {
            :

            if (rootFrame == null)
            {
                rootFrame = new Frame();

                // Associate the frame with a SuspensionManager key.
                SuspensionManager.RegisterFrame(rootFrame, "AppFrame");

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

                :

        private async void OnSuspending(object sender, SuspendingEventArgs e)
        {
            Debug.WriteLine("App.OnSuspending");

            // Get SuspensionManager to save state
            var deferral = e.SuspendingOperation.GetDeferral();
            await SuspensionManager.SaveAsync();
            deferral.Complete();
        }

        :

In each page, NavigationHelper is plumbed-in as follows:


namespace WP81DemoLifeCycleAdv
{
    public sealed partial class Page1 : Page
    {
        private readonly NavigationHelper _navigationHelper;
        :

        public Page1()
        {
            :

            // Hook-up the NavigationHelper event handlers
            _navigationHelper = new NavigationHelper(this);
            _navigationHelper.LoadState += LoadState;
            _navigationHelper.SaveState += SaveState;
        }

        private void LoadState(object sender, LoadStateEventArgs e)
        {
            // Load page-level state via NavigationHelper (not available to other pages)
            if(e.PageState != null && e.PageState.ContainsKey("MyText")) 
                MyText = e.PageState["MyText"] as string;
        }

        private void SaveState(object sender, SaveStateEventArgs e)
        {
            // Save page-level state via NavigationHelper:
            e.PageState["MyText"] = MyText;

            // Save app-global state directly to SuspensionManager (available to all pages):
            SuspensionManager.SessionState["MyGlobalVar"] = 1;
        }

        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            // Tell NavigationHelper the page has been navigated to.
            // This allows it to get page state from SuspensionManager.
            // The actual data will be made available in the LoadState handler
            _navigationHelper.OnNavigatedTo(e);
        }

        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            // Tell NavigationHelper the page is going to be navigated away from.
            // This allows it to save the page state using SuspensionManager.
            // The actual data will be saved in the SaveState handler            
            _navigationHelper.OnNavigatedFrom(e);
        }

        :
    }
}

Key points to note about SuspensionManager and NavigationHelper are:

  • We give SuspensionManager a reference to the app's Frame object. This allows it to keep in-sync with the current page, as well as the navigation history. It can also restore the app to the previously-current page following a termination
  • Navigation information is used by SuspensionManager to load/save app- and page-level state data via hooks in App.OnLaunched and App.OnSuspending. NavigationHelper hooks into page-level lifecycle events and the uses SuspensionManager to load/save page-level state
  • SuspensionManager maintains state data in an XML file stored in the folder Windows.Storage.ApplicationData.Current.LocalFolder.
  • State data handled by SuspensionManager must be serializable (see later discussion)

The following diagram summarizes how the Frame, SuspensionManager and NavigationHelper objects, make use of app- and page-level lifecycle events:

Usage scenarios with SuspensionManager and NavigationHelper

Let's examine a number of usage scenarios and the app- and page-level events that are raised when we use SuspensionManager and NavigationHelper with a simple 2-page app:

App launched for first time
  • App.OnLaunched (PreviousExecutionState == NotRunning)
  • Page1.OnNavigatedTo
  • Page1.LoadState (PageState == null)
User goes to the Start screen -> App Suspended (not terminated as in WP8)
  • App.OnSuspending
  • Page1.OnNavigatedFrom
  • Page1.SaveState
User resumes the app (e.g. picks it from the task switcher)
  • OnResuming
User (or OS) terminates the app (e.g. from the task switcher)
  • App.OnSuspending
  • Page1.OnNavigatedFrom
  • Page1.SaveState
User (re)launches app
  • App.OnLaunched (PreviousExecutionState == Terminated, SuspensionManager will restore state)
  • Page1.OnNavigatedTo
  • Page1.LoadState (PageState != null, page values can be restored via NavigationHelper's PageState)
User navigates to Page2 of app (using in-app button)
  • Page1.OnNavigatedFrom
  • Page1.SaveState
  • Page2.OnNavigatedTo
  • Page2.LoadState (PageState == null)
User navigates back to Page1 of app (using in-app button or h/w Back button)
  • Page2.OnNavigatedFrom
  • Page2.SaveState
  • Page1.OnNavigatedTo
  • Page1.LoadState (see the notes below on null PageState)
User navigates forward to Page2 again
  • Page1.OnNavigatedFrom
  • Page1.SaveState
  • Page2.OnNavigatedTo
  • Page2.LoadState (see the notes below on null PageState)
App is suspended (e.g. user takes a call) and then terminated by user/OS
  • App.OnSuspending
  • Page2.OnNavigatedFrom
  • Page2.SaveState
User (re)launches app
  • App.OnLaunched (PreviousExecutionState == Terminated, SuspensionManager will restore state)
  • Page2.OnNavigatedTo (SuspensionManager saves/restores the current page (Page2 in this case) in session state)
  • Page2.LoadState (PageState != null, page values can be restored via NavigationHelper's PageState)

Notes on SuspensionManager, NavigationHelper, Navigation and PageState

There are a number of things to watch out for when using SuspensionManager and NavigationHelper:

  1. LoadStateEventArgs.PageState and Navigation

    Notice in the above example how we test e.PageState for null in the NavigationHelper.LoadState handler. Page state will be null if the page has not been visited before. This seems obvious, but the mechanism used to navigate to a page will determine if page state is available or not:

    • If you navigate to Page2 from Page1 using Frame.Navigate(typeof(Page2)), then e.PageState will always be null. This is because Frame.Navigate() creates a new instance of Page2
    • A better way to navigate programmatically from Page1 to Page2 would be:

      if(_navigationHelper.CanGoForward()) 
          _navigationHelper.GoForward();
      else 
          Frame.Navigate(typeof(Page2));
      

      In the case that we're able to use _navigationHelper.GoForward() then e.PageState in Page2 will be non-null (although you should still check for the availability of individual state items using e.PageState.ContainsKey("MyVarName"))
  2. Controlling the caching of pages

    We can control the way app pages are cached in two ways:

    1. In App.OnLaunched define the number of pages that will be cached:

      protected override void OnLaunched(LaunchActivatedEventArgs e)
      {
          :
          if (rootFrame == null)
          {
              rootFrame = new Frame {CacheSize = 2};
          :
      
    2. In the page constructor set the NavigationCacheMode:

      public Page1()
      {
          this.InitializeComponent();
      
          // Set how the page is cached:
          //
          // * Required ==> the cached instance is reused for every visit regardless 
          //                of the cache size for the frame
          //
          // * Enabled  ==> the page is cached, but the cached instance is discarded 
          //                when the size of the Frame's cache (set in App.OnLaunched) 
          //                for the frame is exceeded
          //
          // * Disabled ==> the is never cached. A new instance is created each visit
      
          NavigationCacheMode = NavigationCacheMode.Required;  
          :
      }
      
  3. Limited time to save state

    Look at the code in App.xaml.cs that triggers saving of state by SuspensionManager:

    private async void OnSuspending(object sender, SuspendingEventArgs e)
    {
        var deferral = e.SuspendingOperation.GetDeferral();
        await SuspensionManager.SaveAsync();  // Max approx 5-secs to save state
        deferral.Complete();
    }
    

    Firstly, the OS only allows the save process about five seconds to complete, so this is not the place to have calls to remote services, etc.

    Secondly, the OS will consider that the app's ready to be suspended when OnSuspending returns. However, the operation to save state to disk has to be async, because all WinRT file system operations are async in nature (i.e. SuspensionManager makes use of CreateFileAsync(), OpenStreamForWriteAsync() and CopyToAsync() - they're all async operations).

    This means that a mechanism has to be invented to signal that, although OnSuspending has returned, the suspend operation hasn't actually completed! This is the purpose of the e.SuspendingOperation.GetDeferral()...deferral.Complete() statements

MVVM considerations

In the next part (part three) of this series of posts I discuss app lifecycle in relation to a simple MVVM-based approach, which provides automatic load/save of view model state.

References