(X) Hide this
    • Login
    • Join
      • Generate New Image
        By clicking 'Register' you accept the terms of use .

WinRT Business Apps with Prism: App State Management

(3 votes)
Brian Noyes
>
Brian Noyes
Joined Jun 10, 2010
Articles:   19
Comments:   117
More Articles
0 comments   /   posted on Apr 29, 2013

Tweet
 

This is part 3 in the series WinRT Business Apps with Prism.

Introduction

In the last article in this series, I walked you quickly through using commands, wiring up dependencies, and handling navigation using Prism in a Windows Store app. In this article I am going to focus on the application lifecycle state management features. I’ll show you how you can manage transient application data that you want to survive when your application gets suspended, terminated, and resumed. You’ll see how you can store objects that are properties in your view models, as well as anywhere else in your application such as transient data in a repository.

WinRT Application Lifecycle

WinRT apps have a number of different ways they can be started, and phases that they go through while running. An app starts in a not running state. Once the user launches it it is running. But the user can switch to a different Windows Store or desktop app at any time, which will cause your app to go off screen. When it does, it goes into a suspended state. While suspended, no threads are allocated to the program, so it is consuming zero CPU resources. But your app stays in memory and if the user switches back to your app (through Windows-key+Tab or a swiping gesture from the left side of the screen on a touch device), then the app immediately begins running again. Threads are reallocated, and the program picks right back up executing whatever it was doing before.

However, if your app is suspended and the system is running low on memory, WinRT can choose to terminate your application from its suspended state at any time. When that happens, your app is cleared from memory, and all the objects that were built up and connected together while your app was running are gone as well.  If the user launches your application again after termination, it is part of the platform guidance that the should have the state of the app restored just as if they were resuming directly from the suspended state. Only if the user explicitly shuts the application down (Alt-F4 or a swiping gesture from top to bottom of the screen on a  touch device) are you allowed to reset the application start as if it was a clean start of the application.

This lifecycle is depicted in Figure 1. The key thing to understand is that resuming from suspend or launching after termination should result in the same exact user experience – the last screen the user was on should be there, any transient state in the screen should be restored (i.e.. selections, scroll position, etc), and the navigation stack should be back to what it was when they left.

Figure1

Figure 1: Windows Store Application States

To create that user experience, it means you need to be able to save and restore state that is maintained by your view models and client side services in a Prism app. Prism makes this very easy for you through several mechanisms available to you in your view models and services.

Step 1: Add a user experience working with data

I need to have a requirement to work with data in the app before I can start designing for it. For this article, I decide that I want a simple data interaction scenario for taking a phone order from a customer. The user can select from the products that their company sells, enter a quantity, and add that to the current order. They also need to be able to see what is currently part of the current order.

I sketch this out and decide it looks something like this:

Figure2

Figure 2: The AddSalePage UI

The XAML for this looks like this:

   1: <ComboBox ItemsSource="{Binding Products}"
   2:           SelectedValue="{Binding SelectedProductId, Mode=TwoWay}"
   3:           DisplayMemberPath="ProductName"
   4:           SelectedValuePath="ProductId"
   5:           HorizontalAlignment="Left"
   6:           Margin="120,60,0,0"
   7:           Grid.Row="1"
   8:           VerticalAlignment="Top"
   9:           Width="434" />
  10: <TextBox Text="{Binding Quantity, Mode=TwoWay}"
  11:          HorizontalAlignment="Left"
  12:          Margin="580,60,0,0"
  13:          Grid.Row="1"
  14:          TextWrapping="Wrap"
  15:          VerticalAlignment="Top"
  16:          Width="99" />
  17: <Button Content="Add to Order"
  18:         Command="{Binding AddToOrderCommand}"
  19:         HorizontalAlignment="Left"
  20:         Margin="750,60,0,0"
  21:         Grid.Row="1"
  22:         VerticalAlignment="Top" />
  23: <TextBlock Style="{StaticResource SubheaderTextStyle}"
  24:            HorizontalAlignment="Left"
  25:            Margin="120,15,0,0"
  26:            Grid.Row="1"
  27:            TextWrapping="Wrap"
  28:            Text="Product"
  29:            VerticalAlignment="Top"
  30:            Height="25"
  31:            Width="215" />
  32: <TextBlock Style="{StaticResource SubheaderTextStyle}"
  33:            HorizontalAlignment="Left"
  34:            Margin="580,15,0,0"
  35:            Grid.Row="1"
  36:            TextWrapping="Wrap"
  37:            Text="Quantity"
  38:            VerticalAlignment="Top"
  39:            Height="25"
  40:            Width="105" />
  41:  
  42: <Grid HorizontalAlignment="Left"
  43:       Height="416"
  44:       Margin="120,165,0,0"
  45:       Grid.Row="1"
  46:       VerticalAlignment="Top"
  47:       Width="868">
  48:     <Grid.RowDefinitions>
  49:         <RowDefinition Height="50" />
  50:         <RowDefinition Height="*" />
  51:     </Grid.RowDefinitions>
  52:     <Grid Grid.Row="0"
  53:           Margin="5">
  54:         <Grid.ColumnDefinitions>
  55:             <ColumnDefinition Width="300" />
  56:             <ColumnDefinition Width="150" />
  57:             <ColumnDefinition Width="150" />
  58:             <ColumnDefinition Width="300" />
  59:         </Grid.ColumnDefinitions>
  60:         <TextBlock Grid.Column="0"
  61:                    Text="Product"
  62:                    Style="{StaticResource SubheaderTextStyle}" />
  63:         <TextBlock Grid.Column="1"
  64:                    Text="Price"
  65:                    Style="{StaticResource SubheaderTextStyle}" />
  66:         <TextBlock Grid.Column="2"
  67:                    Text="Quantity"
  68:                    Style="{StaticResource SubheaderTextStyle}" />
  69:         <TextBlock Grid.Column="3"
  70:                    Text="Category"
  71:                    Style="{StaticResource SubheaderTextStyle}" />
  72:     </Grid>
  73:     <ListView x:Name="OrderItemsList"
  74:               Grid.Row="1"
  75:               ItemsSource="{Binding CurrentOrderItems}">
  76:         <ListView.ItemTemplate>
  77:             <DataTemplate>
  78:                 <Grid>
  79:                     <Grid.ColumnDefinitions>
  80:                         <ColumnDefinition Width="300" />
  81:                         <ColumnDefinition Width="150" />
  82:                         <ColumnDefinition Width="150" />
  83:                         <ColumnDefinition Width="300" />
  84:                     </Grid.ColumnDefinitions>
  85:                     <TextBlock Grid.Column="0"
  86:                                Text="{Binding Product.ProductName}" />
  87:                     <TextBlock Grid.Column="1"
  88:                                Text="{Binding Product.UnitPrice}" />
  89:                     <TextBlock Grid.Column="2"
  90:                                Text="{Binding Quantity}" />
  91:                     <TextBlock Grid.Column="3"
  92:                                Text="{Binding Product.Category}" />
  93:                 </Grid>
  94:             </DataTemplate>
  95:         </ListView.ItemTemplate>
  96:  
  97:     </ListView>
  98: </Grid>

 

Step 2:  Add some data to the app

The demo code at the completion of the last article didn’t have much going on in terms of application state, we just had two pages that we were navigating between with commands. The first thing I need to start fleshing out some state management is some data that the application is managing. I added two repositories to the client application, one for showing a collection of products, and one for managing the state of the current order.

The ProductsRepository exposes a collection of products:

   1: public class ProductsRepository : IProductsRepository
   2: {
   3:     public IEnumerable<Product> AllProducts()
   4:     {
   5:         return new List<Product>
   6:         {
   7:             new Product { ProductId = 1, ProductName = "Surface Pro", 
   8:                 Category="Tablets", UnitPrice = 999.00m, UnitsInStock=42},
   9:             new Product { ProductId = 2, ProductName = "Surface RT", 
  10:                 Category="Tablets", UnitPrice = 629.00m, UnitsInStock=33},
  11:             new Product { ProductId = 3, ProductName = "Lumia 920", 
  12:                 Category="Phones", UnitPrice = 400.00m, UnitsInStock=12},
  13:             new Product { ProductId = 4, ProductName = "iPhone 5", 
  14:                 Category="Phones", UnitPrice = 629.00m, UnitsInStock=54},
  15:             new Product { ProductId = 5, ProductName = "Razr", 
  16:                 Category="Phones", UnitPrice = 59.99m, UnitsInStock=987},
  17:         };
  18:     }
  19: }

The OrderRepository manages the state of the current order:

   1: public class OrderRepository : IOrderRepository
   2: {
   3:     readonly ObservableCollection<OrderItem> _OrderItems = 
   4:         new ObservableCollection<OrderItem>();
   5:  
   6:     public void AddToOrder(Product product, int quantity)
   7:     {
   8:         var orderitem = (from oi in _OrderItems where oi.Product.ProductId 
   9:                             == product.ProductId select oi).FirstOrDefault();
  10:         if (orderitem == null)
  11:             _OrderItems.Add(new OrderItem { Product = product, Quantity = quantity });
  12:         else orderitem.Quantity += quantity;
  13:     }
  14:  
  15:     public ObservableCollection<OrderItem> CurrentOrderItems 
  16:     { 
  17:         get { return _OrderItems; } 
  18:     }
  19: }

 

Step 3: Expose properties from the view model to support the view

If you wade through the XAML in Step 1, you will see that the view has bindings that expect certain things exposed from the ViewModel for data binding. It needs a collection of Products, a SelectedProductId, a Quantity, an AddToOrderCommand, and CurrentOrderItems collection.

Some of these will be fed from the repository, so the resulting view model looks like this so far.

 

   1: public class AddSalePageViewModel : ViewModel
   2: {
   3:     private readonly INavigationService _NavService;
   4:     private readonly IProductsRepository _ProductsRespository;
   5:     private readonly IOrderRepository _OrderRepository;
   6:     private int _SelectedProductId;
   7:     private int _Quantity = 1;
   8:  
   9:     public AddSalePageViewModel(INavigationService navService, 
  10:         IProductsRepository productsRespository, 
  11:         IOrderRepository orderRepository)
  12:     {
  13:         _OrderRepository = orderRepository;
  14:         _ProductsRespository = productsRespository;
  15:         _NavService = navService;
  16:         GoBackCommand = new DelegateCommand(
  17:             () => _NavService.GoBack(),
  18:             () => _NavService.CanGoBack());
  19:         AddToOrderCommand = new DelegateCommand(OnAddToOrder, CanAddToOrder);
  20:     }
  21:  
  22:     public DelegateCommand GoBackCommand { get; private set; }
  23:     public DelegateCommand AddToOrderCommand { get; private set; }
  24:  
  25:     public IEnumerable<Product> Products
  26:     {
  27:         get { return _ProductsRespository.AllProducts(); }
  28:     }
  29:  
  30:     public ObservableCollection<OrderItem> CurrentOrderItems
  31:     {
  32:         get { return _OrderRepository.CurrentOrderItems; }
  33:     }
  34:  
  35:     public int SelectedProductId
  36:     {
  37:         get { return _SelectedProductId; }
  38:         set 
  39:         { 
  40:             SetProperty(ref _SelectedProductId, value);
  41:             AddToOrderCommand.RaiseCanExecuteChanged();
  42:         }
  43:     }
  44:  
  45:     public int Quantity
  46:     {
  47:         get { return _Quantity; }
  48:         set { SetProperty(ref _Quantity, value); }
  49:     }
  50:  
  51:     private bool CanAddToOrder()
  52:     {
  53:         var prod = (from p in _ProductsRespository.AllProducts() where 
  54:             p.ProductId == _SelectedProductId select p).FirstOrDefault();
  55:         return prod != null;
  56:     }
  57:  
  58:     private void OnAddToOrder()
  59:     {
  60:         var prod = (from p in _ProductsRespository.AllProducts() where 
  61:             p.ProductId == _SelectedProductId select p).FirstOrDefault();
  62:         _OrderRepository.AddToOrder(prod, Quantity);
  63:     }
  64: }

At this point you should be able to run and get the user experience expected.

Step 4: Admit you have a problem

No, I’m not suggesting you find yourself a chapter of Coder’s Anonymous and start going to meetings because you are addicted to code (although that may be appropriate too!). The problem is that our app does not behave correctly in the face of application suspend – terminate – resume. You could see this by adding the code in steps 1-3 to the ending point from the second article in this series and running.

You would navigate to the AddSalePage and select a product and enter a quantity – two pieces of transient state in the view. Then you can force a terminate while debugging a Windows Store app by using the “Debug Location” toolbar in Visual Studio. To do so, switch back to Visual Studio from your running Windows Store app, and select “Suspend and Shutdown”.

Figure3

This causes the app to suspend and then be terminated in the same way as if the OS were running low on memory while your app was suspended and it decided to terminate your app.

Now run the program again (F5 or click the Start Debugging button in the toolbar). It will start on the correct page (AddtoSalePage), but the selection and the quantity you entered will be gone. This would be a violation of the guideline that says it should be transparent to the user when they launch the app after termination compared to resuming after suspend without termination.

Step 5: Add RestorableState to properties

So what can we do to fix it? Well, Prism makes it really easy to deal with this. The things that cause the selection and that hold the quantity are properties on your ViewModel. You ViewModel inherits (or should) from the Prism ViewModel base class, which has built in wiring to make this simple. All you need to do is decorate your SelectedProductId and Quantity properties with the [RestorableState] attribute.

   1: [RestorableState]
   2: public int SelectedProductId
   3: {
   4:     get { return _SelectedProductId; }
   5:     set 
   6:     { 
   7:         SetProperty(ref _SelectedProductId, value);
   8:         AddToOrderCommand.RaiseCanExecuteChanged();
   9:     }
  10: }
  11:  
  12: [RestorableState]
  13: public int Quantity
  14: {
  15:     get { return _Quantity; }
  16:     set { SetProperty(ref _Quantity, value); }
  17: }

Now you could run that same scenario again – Suspend and Shutdown from the Debug Location toolbar, start the app again.

Now when you restart, your selection is retained, as is the quantity that you entered. Nice!

What if the property you want to put [RestorableState] on is a model object or complex type? No problem, you just need to do one more thing to make that work. The way this works under the covers is that the Prism FrameNavigationService is aware when the app is suspending. Likewise, when you navigate away from a view, especially one that you could navigate back to through the back button, you probably want to store and restore state on those transitions as well, and the FrameNavigationService is in charge of that whole process.

So whenever Prism sees that the app is suspending or that your ViewModel is being navigated away from, it will find all of the [RestorableState] properties in your ViewModel through reflection, and it will persist the state of them in Local Storage. It uses the WCF DataContractSerializer to do this, just like the SuspensionManager class that gets injected into your Common folder by Visual Studio does. That means the DataContractSerializer needs to know about all the types it will be serializing. For primitives, this is automatic. But for complex types, you need to tell it about your types.

To do so, you override a method in the MvvmAppBase class and pass the type information for the model objects you will be using as restorable state like so:

   1: protected override void OnRegisterKnownTypesForSerialization()
   2: {
   3:     base.OnRegisterKnownTypesForSerialization();
   4:     SessionStateService.RegisterKnownType(typeof(Product));
   5:     SessionStateService.RegisterKnownType(typeof(OrderItem));
   6:     SessionStateService.RegisterKnownType(typeof(OrderItem[]));
   7: }

Now just so you don’t get yourself confused like I have often done to myself, you might be tempted to change the AddSalePageViewModel to use a SelectedProduct property instead of a SelectedProducyId property and bind the SelectedItem property of the ListView to it instead of the SelectedValue property. Just realize that if you do, you won’t get what you expect when the app starts back up. This is because the SelectedItem property has to point to an instance of an object that is in the bound collection that ItemsSource points to. For this demo, that is the Products collection. But when the [RestorableState] attribute kicks in, it is really going to be populating your SelectedProduct property (assuming it had that attribute) with a new instance of an object based on the data serialized from a previous session. This object is not going to be part of the new collection of Products that are getting returned separately from the ProductsRepository, so the selection will not show up. Just be warned. This is why I went with SelectedValue, which is more resilient to that subtle fact because it is matching by value instead of by object reference.

Now if you poke and prod the app and play with Suspend and Shutdown some more, you will realize your still have a problem. Add a couple items to the order and then suspend and terminate. Start the app again and the items are gone. Darn it, we still have more work to do.

Step 6: Use ISessionStateService in your client services

ViewModels are not the only objects in an MVVM app that need to retain state across termination. Your views might too (i.e. a scrollbar position – see the HubPage in the AdventureWorks Shopper sample in Prism for an example of this. They have built in support for this through the Prism VisualStateAwarePage and its Page base class.

But what about repositories and other client side services that sit logically behind the ViewModel. They often retain transient state as well.

The way you handle this in Prism is to have those objects take a dependency on the Prism ISessionStateService that is managing all the automatic state load/save as the app starts up and shuts down. If you read and write your data into that state service (which exposes a Dictionary API), then it will survive terminations correctly as well.

So I just need to modify the OrderRepository that is caching the order data to do this. First I add a dependency on the ISessionStateService:

   1: private readonly ISessionStateService _StateService;
   2: private const string CurrentOrderKey = "CurrentOrderKey";
   3: public OrderRepository(ISessionStateService stateService)
   4: {
   5:     _StateService = stateService;
   6: }

Notice that I need a unique string key that I will end up associating the state of this service with.

Then whenever my state changes, I also write it into the state service:

   1: public void AddToOrder(Product product, int quantity)
   2: {
   3:     var orderitem = (from oi in _OrderItems where oi.Product.ProductId 
   4:         == product.ProductId select oi).FirstOrDefault();
   5:     if (orderitem == null)
   6:         _OrderItems.Add(new OrderItem { Product = product, Quantity = quantity });
   7:     else orderitem.Quantity += quantity;
   8:     // Write the collection out to the state service
   9:     _StateService.SessionState[CurrentOrderKey] = _OrderItems.ToArray();
  10: }

Notice the last line of code there, simply using the exposed SessionState Dictionary to write the current state of the service into it.

Finally, I need to make sure that the state of the service is initialized based on the state service if there is any state there from a previous session. So I modify the constructor of the service, which will only be called once the first time some other code (i.e. the ViewModel) takes a dependency on it because it is set up as a singleton through the container. When the app starts up after termination, it does still have to reconstruct everything because everything about the app is cleared from memory on termination. So the repository constructor now looks like this:

   1: public OrderRepository(ISessionStateService stateService)
   2: {
   3:     _StateService = stateService;
   4:     if (_StateService.SessionState.ContainsKey(CurrentOrderKey))
   5:     {
   6:         IEnumerable<OrderItem> sessionItems = 
   7:             _StateService.SessionState[CurrentOrderKey] as IEnumerable<OrderItem>;
   8:         if (sessionItems != null)
   9:         {
  10:             foreach (OrderItem item in sessionItems)
  11:             {
  12:                 _OrderItems.Add(item);
  13:             }
  14:         }
  15:     }
  16:     
  17: }

And because this is the first time a ViewModel has taken a dependency on the ISessionStateService, we need to make sure the Container knows how to resolve it. So we add one more registration to our OnInitialize method in the App.xaml.cs file to expose the singleton SessionStateService created by the MvvmAppBase class to anyone who wants to dependency inject ISessionStateService. If you were not using a container, you could pass that SessionStateService reference to the constructor of the OrderRepository through manual dependency injection if you were constructing it in the app class as a singleton.

   1: protected override void OnInitialize(IActivatedEventArgs args)
   2: {
   3:     base.OnInitialize(args);
   4:     _Container.RegisterInstance<ISessionStateService>(SessionStateService);
   5:     ...
   6: }
   7:  

Now you should be able to run, add items to the order, suspend and terminate, re-launch the app, and all your transient state is right where it was when the user left the app.

Summary

In this article I have demonstrated how to use the RestorableState attribute in your ViewModels in Prism for Windows Runtime as well as how to use the ISessionStateService. There is one thing I didn’t show – in the last article I showed the OnNavigatedTo and OnNavigatedFrom overrides in the ViewModel base class. Those both get passed a dictionary as one of their arguments. This is basically the dictionary that the view can write state into as well. You could explicitly read and write values into those dictionary when you are navigated to or from, and it would survive termination as well. The RestorableState attribute just makes it a lot more declarative and easy for your ViewModels since the state you care about will typically be in properties anyway.

So remember to use RestorableState on properties that affect what the user sees on the screen if it is transient data. If you are already proactively loading and saving state as you are navigated to and from through a service, or to disk with Local Storage or into roaming settings, you just need to make sure you are symmetrical about that as your ViewModels are navigated to and from. But for those things that are logically just “held in memory”, realize they won’t be if you are terminated, but your app is still supposed to pretend they were. [RestorableState] and ISessionStateService make it easy to do that.

In the next article I'll take a look at starting to validate data when the user enters it.

About the Author

Brian Noyes is CTO of Solliance (www.solliance.net), a software development company offering Architecture as a Service, end-to-end product development, technology consulting and training. Brian is a Microsoft Regional Director and MVP, Pluralsight author, and speaker at conferences worldwide.  Brian worked directly on the Prism team with Microsoft patterns and practices. Brian got started programming as a hobby while flying F-14 Tomcats in the U.S. Navy, later turning his passion for code into his current career. You can contact Brian through his blog at http://briannoyes.net/ or on Twitter @briannoyes.


Subscribe

Comments

No comments

Add Comment

Login to comment:
  *      *       

From this series