Windows Phone 7 Tombstoning with MVVM and Sterling

Sterling makes tombstoning very easy because it handles serialization of just about any type of object. To show an example, we'll start with the concept of a view model that holds a set of categories (that a pivot is bound to) and a set of items that are filtered by category. When tombstoned, the application must remember the category as well as any item that is selected.

Build the Tombstone

First thing to do is create the generic tombstone model to hold values. This is the model: 

public class TombstoneModel
{
    public TombstoneModel()
    {
        State = new Dictionary<string, object>();
    }

    public Dictionary<string, object> State { get; set; }

    public T TryGet<T>(string key, T defaultValue)
    {
        if (State.ContainsKey(key))
        {
            return (T)State[key];
        }
        return defaultValue;
    }
}

But wait ... what's the point? Isn't that just like the application settings object? Sure ... sort of. There are a few problems with application settings. First, they don't handle complex object graphs and the objects must be serializable. They don't recognize things like "foreign keys" or sub lists. Second, they load into memory as opposed to isolated storage files which can be loaded on demand.

Sterling, on the other hand, can handle almost any type of object (and for the ones it can't handle, you can write acustom serializer). 

Define the Tombstone Table

A full lesson on Sterling is outside the scope of this post. To learn about Sterling, read the User's Guide. To define a table in Sterling, you pass the type of the table and a key. Because there will only ever be one instance of the tombstone model, you cheat the key and make it always have a "true" value like this:

CreateTableDefinition<TombstoneModel,bool>(c=>true)

Finally, because the tombstone model is only to hold values that must be saved during a tombstone event, the model should be cleared when the application truly closes (as opposed to being deactivated due to a tombstone event). Therefore, in the Application_Closing method you can delete the tombstone model, if it exists:

private void Application_Closing(object sender, ClosingEventArgs e)
{
    Database.Delete(typeof (TombstoneModel), true);
}

Now everything is set to handle the tombstone properties.

Making your View Model a Caretaker

What other way is there to describe a view model that is "tombstone friendly?" To make a view model friendly, decorate it with the ITombstoneFriendly interface that looks like this:

public interface ITombstoneFriendly
{
    void Deactivate();
    void Activate();
}

These will be implemented in a moment.

Hooking into the Page

The best way to manage tombstone events is to hook into the OnNavigatedTo and OnNavigatedFrom overrides in the page code-behind. In fact, because forward navigation always generates a new instance of the view, this is the perfect way to maintain state of the view during navigation even when tomstoning isn't taking place.

Like before, we'll take advantage of some extension methods to make this easy. Here's a little caveat: you may be familiar with some "gotchas" with tombstoning if you've read Jeff Prosise's Real World Tombstoning series (here'spart 2part 3, and Part 4). Oh, and did you know that Sterling automatically serializes WriteableBitmaps using the "fast" technique Jeff describes in those posts? You can easily add one to the tombstone collection and it will serialize for you! 

The issue with the pivot index happens due to the order of loading. If you restore values on the view model before the view is loaded, the view itself will reset any two-way bindings and you'll find a pivot simply snap to the first item. This is no good! Therefore, the extension method for the activate waits until the page is loaded, then calls the activate method on the view model.

Here are the extension methods: 

public static void DeactivatePage(this PhoneApplicationPage phonePage, IViewModel viewModel)
{
    if (viewModel is ITombstoneFriendly)
    {
        ((ITombstoneFriendly)viewModel).Deactivate();
    }
}

public static void ActivatePage(this PhoneApplicationPage phonePage, IViewModel viewModel)
{
    RoutedEventHandler loaded = null;
    loaded = (o, e) =>
                    {                             
                        ((PhoneApplicationPage) o).Loaded -= loaded;
                        if (viewModel is ITombstoneFriendly)
                        {
                            ((ITombstoneFriendly) viewModel).Activate();
                        }
                    };
    phonePage.Loaded += loaded;
}

The loaded event doesn't take any conditional arguments, so it would be impossible to pass the view model to that event. Therefore, an anonymous method is used instead. By using a local variable, it can be unhooked inside the event call so it is not fired again. Once the page is loaded, the activate method is called on the view model. Because the page has been loaded, binding a pivot control item will work perfectly if the pivot SelectedItem is synchronized with a value on the view model.

Here's what the calls look like from the code-behind for the MainPage.xaml, hooking into the navigation events and passing the correct view model:

protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
    this.ActivatePage(GlobalManager.GetViewModel<IMainViewModel>());
    base.OnNavigatedTo(e);
}
        
protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{
    this.DeactivatePage(GlobalManager.GetViewModel<IMainViewModel>());
    base.OnNavigatedFrom(e);
}

Raising the Dead

The only thing left is to perform the actual deed. When the view model is flagged for death, it will hydrate out key values. In this case, it is the current category and current item: 

public void Deactivate()
{
    var tombstone = new TombstoneModel();
    if (CurrentCategory != null)
    {
        tombstone.State.Add(ExtractPropertyName(() => CurrentCategory), CurrentCategory.Id);
    }
    if (CurrentItem != null)
    {
        tombstone.State.Add(ExtractPropertyName(()=>CurrentItem), CurrentItem.Id);
    }
    App.Database.Save(tombstone);
}

A new model is created because you never care about the old model - this is saving state for the currenttombstone event, and the state from any other don't matter (they will be wiped when the application closes anyway). The dictionary is loaded with the key values for the current catalog and the current item. 

Now when the application is revived:

public void Activate()
{
    var saved = App.Database.Load<TombstoneModel>(true);

    if (saved == null) return;

    var categoryId = saved.TryGet(ExtractPropertyName(() => CurrentCategory), 0);

    if (categoryId > 0)
    {
        CurrentCategory = (from c in Categories where c.Id == categoryId select c).FirstOrDefault();
    }
    var currentItemId = saved.TryGet(ExtractPropertyName(() => CurrentItem), 0);            
    if (currentItemId > 0)
    {
        CurrentItem = (from i in Items where i.Id == currentItemId select i).FirstOrDefault();
    }
}

In this case, the category list is always loaded in the constructor of the view model. That doesn't change regardless of tombstoning or not. The items is a filter by category, so when the category is updated, the items filter is also updated. When the old state is loaded, first the category is checked and if it exists, the current category is set to the corresponding category item. The same happens with the item. 

That's it. When testing the application, going into a pivot page always returns to the correct pivot column, and the selection state of the list is always maintained. If the application is exited by backing out, the close event deletes the tombstone key and upon re-entering, the pivot and list start on the first page with no selection as expected.

Tombstoning with MVVM can be dead simple with a few helper methods and Sterling to serialize the data.

by@Jeremy Likness

posted @ 2013-01-10 17:08  NSDefaultRunLoopMode  阅读(302)  评论(1编辑  收藏  举报