This blog post shows how to implement tombstoning within a Windows Phone 7 application that following the Model-View-ViewModel pattern.
I have to admit Windows Phone 7 tombstoning had me in a bit of a muddle for a while, so many places to store state, a confusing lifecycle and navigation model. Most of the blog posts I read either detailed tombstoning for non-MVVM applications, or described how to use or adapt an existing MVVM framework for the purposes of tombstoning. I only really understood the ins-and-outs of tombstoning after writing my own simple MVVM application. I thought I would share this application here in this blog in the hope that it might help other similarly confused developers!
What is Tombstoning?
Mobile phones have limited resources compared to desktop computers. For that reason, most Smartphone OSs limit the number of applications that are currently loaded into memory and executing. For Windows Phone 7, this limit is one!
If the user hits the phone start button while your application is running, the screen lock engages, or you invoke a chooser / launcher from your application, then your application is terminated. However, when the user navigates back to your app, the screen unlocks or the chooser / launcher closed, the user expects to see your application again in its original state.
To support this, the WP7 OS maintains state information which allows you to 'restore' your application by starting a new application instance and using this state information to start the application in the same state as the one which was terminated. For a full overview of this process, I would recommend reading the Execution Model Overview for Windows Phone on MSDN, or the three part series on the Windows Phone Developer Blog (1), (2), (3).
This probably sounds like a lot of work, and to be honest, it is. You might be wondering if the new multi-tasking capabilities that the Mango update will bring (demoed at the MIX11 day 2 keynote and to be released probably late 2012) will mean that the tombstoning will disappear. I have not seen any official confirmation one way or the other, however, personally I think you will still need to tombstone in Mango. It is most likely that the number of concurrent applications will be limited and applications will still need to tombstone as a result.
The Example Application
The example application I am using for this blog post is illustrated below. The application displays a list of tweets, clicking on a tweet displays it full screen. Twitter applications are the new Hello World!
The ViewModel
The view model is pretty trivial, each tweet is represented by a FeedItemViewModel
:
public class FeedItemViewModel
{
public FeedItemViewModel()
{
}
public long Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public DateTime Timestamp { get; set; }
}
The above view model does not change state, so there is no need to implement INotifyPropertyChanged
.
The top-level view model simply exposes a collection of the above feed items. It also has an Update
method which populates this list by querying Twitter's search API for tweets that contain the #wp7 hashtag:
public class FeedViewModel
{
private readonly string _twitterUrl = "http://search.twitter.com/search.atom?rpp=100&&q=%23wp7";
private WebClient _webClient = new WebClient();
private readonly ObservableCollection<FeedItemViewModel> _feedItems =
new ObservableCollection<FeedItemViewModel>();
public FeedViewModel()
{
_webClient.DownloadStringCompleted += WebClient_DownloadStringCompleted;
}
private void WebClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
var doc = XDocument.Parse(e.Result);
var items = doc.Descendants(AtomConst.Entry)
.Select(entryElement => new FeedItemViewModel()
{
Title = entryElement.Descendants(AtomConst.Title).Single().Value,
Id = long.Parse(entryElement.Descendants(AtomConst.ID).Single().Value.Split(':')[2]),
Timestamp = DateTime.Parse(entryElement.Descendants(AtomConst.Published).Single().Value),
Author = entryElement.Descendants(AtomConst.Name).Single().Value
});
_feedItems.Clear();
foreach (var item in items)
{
_feedItems.Add(item);
}
}
public ObservableCollection<FeedItemViewModel> FeedItems
{
get { return _feedItems; }
}
public FeedItemViewModel GetFeedItem(long id)
{
return _feedItems.SingleOrDefault(item => item.Id == id);
}
public void Update()
{
_webClient.DownloadStringAsync(new Uri(_twitterUrl));
}
}
The View
The FeedView
page that is used to render the FeedViewModel
(i.e., the list of tweets) is simply a NavigationList control (an ItemsControl
optimised for WP7 navigation scenarios) that has an ItemTemplate
which renders each item:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<l:NavigationList x:Name="navigationControl"
ItemsSource="{Binding FeedItems}"
Navigation="NavigationList_Navigation">
<l:NavigationList.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical"
Height="100">
<TextBlock Text="{Binding Author}"
Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock Text="{Binding Title}"
Margin="20,0,0,0"
Style="{StaticResource PhoneTextSmallStyle}"
TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</l:NavigationList.ItemTemplate>
</l:NavigationList>
</Grid>
The FeedItemView
that is used to render the FeedItemViewModel
is even simpler:
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Author}"
Style="{StaticResource PhoneTextLargeStyle}"
Foreground="{StaticResource PhoneAccentBrush}"/>
<TextBlock Text="{Binding Title}"
Style="{StaticResource PhoneTextLargeStyle}"
TextWrapping="Wrap"/>
</StackPanel>
</Grid>
Now that we have the ViewModels
and their respective Views
, we need to bring them together by making the ViewModel
the DataContext
for each View
. There are a number of different ways you can do this (Paul Stovell documents 8 different ways in his MVVM Instantiation Approaches blog post!), however, I find the simplest approach with WP7 applications is to associated the ViewModel
with the application. Therefore, we add our view model as a property of the App
class and instantiate it when the Application_Launching
method (which handles the Launching
lifecycle event) is invoked:
public partial class App : Application
{
public FeedViewModel ViewModel { get; private set; }
...
private void Application_Launching(object sender, LaunchingEventArgs e)
{
ViewModel = new FeedViewModel();
ViewModel.Update();
RootFrame.DataContext = ViewModel;
}
...
}
The DataContext
of the RootFrame
is set to our 'top level' view model. The RootFrame
is an instance of PhoneApplicationFrame
, which contains the current PhoneApplicationPage
instance, hence when you navigate from one page to the next, the content of the PhoneApplicationFrame
is replaced within the page that has been navigated to. As a result, each of your pages will inherit the DataContext
from the application frame.
Navigation
The DataContext
of the RootFrame
is inherited by the FeedView
page, so once the tweets are loaded, they will be rendered in our NavigationList
. In order to navigate to a tweet when a user clicks on it, we need to handle the Navigation
event:
public partial class FeedView : PhoneApplicationPage
{
...
private void NavigationList_Navigation(object sender, NavigationEventArgs e)
{
var selectedItem = e.Item as FeedItemViewModel;
NavigationService.Navigate(new Uri("/FeedItemView.xaml?id=" + selectedItem.Id, UriKind.Relative));
}
}
The above code uses the NavigationService
to navigate to the FeedItemView
page. We use the querystring
to pass the id
of the selected tweet. We could have passed this information from View
to the ViewModel
by adding a SelectedItemId
property, however, we will find out later that there are some advantages to using the querystring
.
When the FeedItemView
page is loaded, we need to obtain this id from the querystring
and use it to locate the correct FeedItemViewModel
instance. This is done as follows:
public partial class FeedItemView : PhoneApplicationPage
{
public FeedItemView()
{
InitializeComponent();
}
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
long feedItemId = long.Parse(NavigationContext.QueryString["id"]);
this.DataContext = App.Current.ViewModel.GetFeedItem(feedItemId);
}
}
Tombstoning
With the above code, our simple application works just fine, you can navigate the list of tweets, click on one to see it in the FeedItemView
page, then use the back button to return to the original list of tweets. It would be great if our job was complete at this point, however, the application fails miserably if it is tombstoned. To test this, load the application, then hit the start button, followed by the back button to return to the application. This is what you are met with:
Tombstoning terminates the application, therefore the ViewModel
and all the state it contains is lost. Hence we return to an empty list.
So how do we fix this?
When your application gets tombstoned, a Deactivated
event is fired before your application terminates, and when the user navigates back to your application, a new instance is created and the Activated
event is fired. The Visual Studio WP7 application template helpfully adds event handlers for these events on your App
class.
Note that when an application is re-activated, the Launching
event is not fired, therefore the code we added to construct a new view model is not executed in this case.
The fix to this problem is rather simple, the framework provides a PhoneApplicationService
class which has a State
property which allows you to store your application state within a dictionary
. The WP7 OS will persist this dictionary
of state on your behalf when your application is tombstoned. You can place anything within this dictionary
as long as it is serializable. Therefore, a simple solution to this problem is to simply place our view model into this dictionary
, retrieving it when the application is re-activated:
private readonly string ModelKey = "Key";
private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
PhoneApplicationService.Current.State[ModelKey] = ViewModel;
}
private void Application_Activated(object sender, ActivatedEventArgs e)
{
if (PhoneApplicationService.Current.State.ContainsKey(ModelKey))
{
ViewModel = PhoneApplicationService.Current.State[ModelKey] as FeedViewModel;
RootFrame.DataContext = ViewModel;
}
}
With this simple change, the application now returns to its previous state when the user hits the back button. Note that this also works when the application is tombstoned on the FeedItemView
page:
This is because when the application is re-activated, the NavigationService
Journal which records the pages that have been visited is also restored. The 'back stack' contains the URI of each page which in our case contains the query string which holds the Id
of the tweet which is currently being viewed.
Persisting State Between Sessions
In the above example, the application state was saved as tombstoned state. But what if the user simply hits the back button until they navigate back out of our application? In this context, your application is closed, rather than tombstoned. The PhoneApplicationService.Current.State
dictionary
which we used to store our view model is not persisted in this context. For long-term persistence, you should save your data to the phones isolated storage.
We can serialize our view model to isolated storage when the application exits by adding code to the Application_Closing
methods which handles the Closing
event. In the example below, I am using the framework XmlSerializer
, which is the same serializer which is used to persist data in the application state dictionary. However, as you have full control over serialization, you might choose to use a serializer with better performance.
private void Application_Closing(object sender, ClosingEventArgs e)
{
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
using (var stream = new IsolatedStorageFileStream("data.txt",
FileMode.Create,
FileAccess.Write,
store))
{
var serializer = new XmlSerializer(typeof(FeedViewModel));
serializer.Serialize(stream, ViewModel);
}
}
The code for launching the application now needs to be modified to first try and load the data from isolated storage. If no data is found, a new view model is created:
private void Application_Launching(object sender, LaunchingEventArgs e)
{
using (var store = IsolatedStorageFile.GetUserStoreForApplication())
using (var stream = new IsolatedStorageFileStream
("data.txt", FileMode.OpenOrCreate, FileAccess.Read, store))
using (var reader = new StreamReader(stream))
{
if (!reader.EndOfStream)
{
var serializer = new XmlSerializer(typeof(FeedViewModel));
ViewModel = (FeedViewModel)serializer.Deserialize(reader);
}
}
if (ViewModel == null)
{
ViewModel = new FeedViewModel();
ViewModel.Update();
}
RootFrame.DataContext = ViewModel;
}
NOTE: This is not a fully functional Twitter application. In a real application, you would probably want to update the data that is loaded from isolated storage to add the latest tweets, however this blog post is about tombstoning, not twitter application development!
Tombstoning UI State
So, now our application saves state during tombstoning and to isolated storage when it exits. Surely we're all done now?
Well … almost.
If you start the application and scroll down the list of tweets, then hit the start button, tombstoning the application, then hit the back button to re-activate it, our tweets are all there, but our list is scrolled back to the top again:
Why is this? Well, when you simply navigate back from one page within your application to another, the original page is still present in memory and as a result, any state held by the controls within the application UI is maintained.
However, as we have seen already, when an application is tombstoned, it is terminated. The only state that remains is that which you manually place into the State
dictionary. Therefore, in order to maintain the scroll location, which we should do, we need to determine its location and store it in the tombstone state.
A while back, I blogged about exposing a ScrollViewers
scroll location as an attached property in order to permit data binding. This approach could be used here to bind the scroll location to a property on the view model. However, in this case, I think something a bit more lightweight is more appropriate.
In the previous sections, we have used the application-level code > PhoneApplicationService.Current.State
for storing tombstone state. You can also store state on a page-level via the PhoneApplicationPage.State
property. This is a much more appropriate place for storing state relating to UI controls within pages, rather than state which is more closely related with your applications business logic.
It is still a little tricky to extract the required state information from list controls. The code below uses Linq-to-VisualTree to locate the VirtualizingStackPanel
that can be used to get / set the scroll position. The scroll location is placed in the State
dictionary when the user navigates away from the page.
private VirtualizingStackPanel ItemsPanel
{
get
{
return navigationControl.Descendants<VirtualizingStackPanel>()
.Cast<VirtualizingStackPanel>()
.SingleOrDefault();
}
}
private static readonly string ScrollOffsetKey = "ScrollOffsetKey";
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
var scroll = ItemsPanel;
if (scroll != null)
{
State[ScrollOffsetKey] = ItemsPanel.ScrollOwner.VerticalOffset;
}
}
Restoring this state is simply a matter of checking for the existence of this key in the State
dictionary when the page is navigated to:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
this.Loaded += (s, e2) =>
{
if (State.ContainsKey(ScrollOffsetKey))
{
var scroll = ItemsPanel;
if (scroll != null)
{
ItemsPanel.SetVerticalOffset((double)State[ScrollOffsetKey]);
}
}
};
}
Note that the state is restored after the Loaded
event is fired because the VirtualizingStackPanel
which manages the scroll location is defined within the NavigationList
template and hence will not be present in the visual tree when the page is not initially constructed (the same is true
if you use a ListBox
as well).
So ... finally, we are done!
Conclusions
This blog post feels like it has turned into something of an epic! As I mentioned in the introduction, tombstoning within WP7 applications is complex and it is easy to get confused with all the various places where state can be stored (It even seems to have Silverlight guru Jesse Liberty in a bit of a muddle!).
It is worth noting that this example tombstones the entire ViewModel
state, and in more complex applications, it might be more appropriate to tombstone an abstraction of the ViewModel
, especially if it is not serializable. However, I think the simple example that this blog presents is still a useful starting point for understanding the tombstoning process.
You can download the source code for this example application here.
Regards,
Colin E.