30 January 2011

ViewModel driven animations using the Visual State Manager, DataStateBehavior and Expression Blend

Leon Zandman, fellow member of the Dutch Windows Phone 7 developer community, told me that my MapMania app used too much screen real estate for secondary controls and too little for the actual map, especially in landscape mode. I got the same feedback within 24 hours of app certification from one NY Chua from Singapore, who apparently had bought MapMania (which shows, by the way, that something like the Standard About Page does work very well for user/customer feedback). So it was time to do something.

I decided to make the page title collapsible by the user, using a nice animation. And of course I wanted this to be a setting that could be tombstoned, and I should be able to drive the animation from my ViewModel. Since  Windows Phone 7 does not support DataTriggers, there are some challenges, but they are easy to overcome. I will demonstrate my solution and the technique I used by making a small application from scratch, for I like to give complete examples – so you won’t have to hunt for the missing clues. You will pick up some Blend knowledge long the way as well. I hope.

Start out the usual way:

  • Create a new Windows Phone 7 application – this one I called “ModelDrivenAnimations”
  • Add a Solution Folder “Binaries”
  • Put “GalaSoft.MvvmLight.WP7.dll”, “GalaSoft.MvvmLight.Extras.WP7.dll” and “System.Windows.Interactivity.dll” in it.

And this time add a fourth assembly: Expression.Samples.Interactivity.dll. Get it from CodePlex (or just nick it out of my sample solution). Put it in the Binaries Solution folder as well, make a reference to all four assemblies from the project and we are ready to go.

First of all, muck up the page by putting something in the ContentPanel grid, like this:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
  <TextBlock TextWrapping="Wrap" FontSize="85"> 
   This is content that could do with some more space
  </TextBlock>
</Grid>

ModelDrivenAnimations1

Which will yield a very pretty *cough* looking application screen similar to the image to the right.

Next is a very simple view model:

using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;

namespace ModelDrivenAnimations
{
  public class SampleViewModel : ViewModelBase
  {
    private string _appName = "ModelDrivenAnimations";
    public string AppName
    {
      get { return _appName; }
      set
      {
        _appName = value;
        RaisePropertyChanged("AppName");
      }
    }

    private bool _isPageTitleVisible = true;
    public bool IsPageTitleVisible
    {
      get { return _isPageTitleVisible; }
      set
      {
        _isPageTitleVisible = value;
        RaisePropertyChanged("IsPageTitleVisible");
      }
    }

    public ICommand TogglePageTitleCollapsed
    {
      get
      {
        return new RelayCommand(() =>
        {
          IsPageTitleVisible = !IsPageTitleVisible;
        });
      }
    }
  }
}

Very basic: a property “IsPageTitleVisible” to control the actual display of the Page Title, a command to fire the actual toggle, and a property with the app name to see if the binding actually works ;-) - normally this would come from a resource file, eh? If you’ve read my blog before, you should be starting to recognize the next part. First, add this to the namespace declarations of MainPage.xaml by adding

xmlns:ModelDrivenAnimations="clr-namespace:ModelDrivenAnimations"

to the namespaces, then declare the SampleViewModel just above the LayoutRoot grid:

<phone:PhoneApplicationPage.Resources>
  <ModelDrivenAnimations:SampleViewModel x:Key="SampleViewModel"/>
</phone:PhoneApplicationPage.Resources>
set the DataContext of your layout root grid to be the SampleViewModel:
<Grid x:Name="LayoutRoot" Background="Transparent" 
  DataContext="{StaticResource SampleViewModel}">

and then change Text="MY APPLICATON" into Text="{Binding AppName}" on the ApplicationTitle TextBlock. Rebuild the application, and even in design time the application name changes to “ModelDrivenAnimations”. Well done. The stage is set. Now proceed to start Expression Blend and open your project. Go on! Contrary to what most developers think, it won’t bite ;-)

First of all, we are going to bind the command. I decided to let the PageTitle disappear as the user taps on the ApplicationTitle, so select the ApplicationTitle on the design pane screen. Then proceed as follows:

  • Top left, select the “Assets” tab, and start to type “EventToCommand” in the search box. Long before you are even halfway done, the “EventToCommand” behavior appears.
  • Drag the EventToCommand command on top of the ApplicationTitle in the “Objects and Timeline” panel about halfway left on the screen.
  • Go to the right top side of the screen. Select the “Properties” tab. In the panel “Trigger” is a dropdown “EventName” which is currently set to “Loaded”. Change that to “MouseLeftButtonDown” – we want the effect appear when the user taps the title, not when it is loaded ;-).
  • Select the tab “Data” at the top right of the screen. Way down below at the bottom right a panel “Data Context” with “SampleViewModel” in it will appear. Expand this, find the “TogglePageTitleCollapsed" (last entry) and drag this onto the EventToCommand.
  • Hit “Ok” on the popup that appears.

You have just created a command binding without even typing a line of code. Expression Blend even added all the namespace prefixes for you.

One little piece of preparation – once again, select the PageTitle and select the “Properties” tab. In the panel “Layout” there is a property “Height” and that is set to “Auto (95.765625)”. This needs to be a fixed value, or else the Visual State Manager cannot infer how to change states. So change this value to “95”.

Now we are going to have some fun with the Visual State Manager itself:

  • Looking back at the top left of the screen you will notice a “States” tab. Select that, and two very small icons will appear.
  • Clicking the first one will create a new Visual State Group and three more icons.
  • Click on the middle one (“Add state”) and add a state “Expanded”. The top of the design pane in the middle will now show a little red button with the text “Expanded state recording is on”.
  • Click the red button to turn off the recording.
  • Add another Visual State called “Collapsed” but don’t turn off the recording now. In stead, select the PageTitle again (either in design view or in the Objects and Timeline panel).
  • Click tab “Properties” on the right top side of the screen.
  • Change the Height property again – this time to 0 (zero). The PageTitle will collapse to a thin blue indicator line.
  • Look down, and find the panel “Transform”. It’s likely to be collapsed at this point, so expand it first. Select the third tab, called “Scale”.
  • Change the Y value to 0 .

Click on visual state “Expanded” in the tab “States” on the left top of the screen and the PageTitle will reappear again. Now turn of the recording for visual states by clicking the red button on top of the design pane, and commence to the last step: connecting the states to the ViewModel by using DataStateBehavior.

  • Select “Assets” again in the top left panel, type “DataState” in the search box and “DataStateBehavior” and “DataSwitchBehavior” will appear.
  • Drag DataStateBehavior on top of “ApplicationTitle” in the “Objects and Timeline” panel.
  • On the right side of the screen, select the Properties tab again, and the properties of the behavior will appear.
  • Click the little grey square behind “Binding”, select “Data binding” in the popup menu, and then “IsPageTitleVisible” in the popup.
  • Click the down pointing arrow on the bottom of the dialog for more options, and select TwoWay.
  • Click OK.
  • Enter “Collapsed” for “FalseState”,
  • Enter “Expanded” for TrueState,
  • Enter “True” for Value.

The odd thing is – sometimes Blend does not let you enter “True” for Value. I am not sure if this is a bug. Please switch to XAML in that case and enter the value manually (use the small button with “< >” on the top right of the design pane to switch to XAML and find the DataStateBehavior; the button above (with the circle) will take you back to design view).

Then hit F5. Yes, hit F5. Even designers need to test stuff runtime, and they can do so by hitting F5, just like a developer. If you’ve done everything right, the Page Title will disappear and reappear when you click on the application title. But that’s hardly an animation. So switch back to Blend one more time:

  • Select the “States” tab once more,
  • Select “Default Transition”
  • Change the “0 s” in the little textbox to the right into “1”.

Then hit F5 again. The Visual State Manager now takes 1 second to change from one state to another, and infers an animation from that. Your PageTitle now appears to fold away in itself. The net effect, as I implemented it in MapMania, looks like this:

Animated header as implemented on MapMania

Notice the little visual cue next to the app name ;-). And this is about the simplest of animations. There are lots of other possibilities – you can even make your own transitions, let things rotate in or out of existence, whatever. I really encourage you to explore the possibilities of Transform tab – I just started doing that myself ;-).

 

 

 

The Visual State Manager is, when all is said and done, nothing more than a State Machine in which you can define states and transitions, that can be easily controlled from your View Model – i.e. code, the developer’s comfort zone  – by using DataStateBehavior. And DataSwitchBehavior – this can handle more states than just true or false, as far as I understood.

For those who lost the way in text, a little “Blend Road Map” showing everything we have used to get to this result:Blend

A little rant to conclude this post: some of you may have noticed that since we’ve hooked up the ViewModel to the View we did not type a single line of code at all. I can hear some of you think “maybe this dolt should rename his blog “Blend by Example” and leave us alone” ;-)

I am convinced that, as a WPF, Silverlight or Windows Phone developer, you should have at least a basic understanding of what you can do with Blend. First of all, even if you work without a separate designer, it saves you a lot of XAML typing, which I can assure you is not very much fun, and every good developer is a lazy developer. Second, creating things like animations and pretty user interfaces is pretty hard, and Blend helps you a lot with it. Maybe you as a developer like bare-bones GUI’s, but your users don’t, so better use a tool that can help with it. Third, as you do work with a separate designer, you need to know what he or she needs to have to make the application tick. Developer and designer need to communicate and understand each other. Now what do you think is more likely – that a designer understands what you as a developer are doing, or the other way around? And who will be calling the shots in a development process – the one that only understands his own side of the designer/development fence, or the one that can look over the fence and stomp around at the other side, if only a little? So get going and get to grips with Blend. Or else – heaven forbid - have designers commandeering you around ;-)

If something went wrong along the way, please refer to the sample solution.

21 January 2011

Displaying an image in Windows Phone 7 accent color

One of the customizing features of Windows Phone 7 is the accent color, which is used to display tiles and certain user interface elements. Default is blue, but I like to play with it (indeed, as the text on the ‘theme’ settings page suggests, to match my mood) and am currently hooked on purple green. Sue me ;-)

When you use vector data or controls, it is possible to use this color by referring to PhoneAccentBrush. For example, if you want a TextBlock’s color to match the accent color, simply add

Foreground="{StaticResource PhoneAccentBrush}"
to the TextBlock. An example of this effect can be seen in my previous post - there is the text "About Sample" shown in blue - the default accent color (screen shot way way below at the end of the post)

handBut it is also possible to show an entire image in the accent color, and it is surprisingly simple. Suppose you want a “thumbs up” image like showed to the right, which I use on the Map Mania credits page. I started out with a simple white-on-transparent background image, and added that to my Windows Phone 7 page like this:

 

<Rectangle Fill="{StaticResource PhoneAccentBrush}" 
     Width="55" Height="68">
  <Rectangle.OpacityMask>
    <ImageBrush ImageSource="icons/thumbsup.png" Stretch="Fill" />
  </Rectangle.OpacityMask>
</Rectangle>

In stead of adding the image directly to your XAML, you add a Rectangle of the same size with a PhoneAccentBrush Fill color, and the image itself as a brush. Presto. Image in accent color – that automatically changes when you change the accent color of your phone.

And that’s all that’s to it. It’s not always complicated architectural things that lighten up your App. ;-)

Update 20-02-2012 Is has been brought to my attention that an older post with nearly identical XAML code has appeared earlier on the "Accidental Scientist" by Simon Cooke. It's uncanny that he even used an identically named image. As my regular readers know I take great pains to link to blog posts or people that inspired me or on whose work I based my examples. For the record: I never saw this blog post when I posted this over a year ago, possibly because of the title, but Mr. Simon Cooke appeared to have this idea earlier than I did. So I hereby acknowledge he has 'prior art credits'.

20 January 2011

A Standard About Page for Windows Phone 7 applications

Update - if your read this, you might want to read this update too when you are done

I admit – I fell for it too. I was a bit too lax reading the Windows Phone 7 Application Certification Requirements and missed section 5.6:

An application must include the application name, version information, and technical support contact information that are easily discoverable.”

Like most, I got away with it the first three times since Microsoft initially did not enforce it very strictly - because the rule came in place very late in the process or something like that. But I fell flat on my face the fourth time. I spotted a very nice about page on the Slim Tanken App by Manfred Dalmeijer, fellow member of the Dutch Windows Phone 7 developer community. He was kind enough to send me a sample solution from which I happily copied a lot of XAML. I decided to not only include it in my own Map Mania App, but to build a kind of standard page and MVVMLight model around it – so that everyone who wanted to quickly add a functional About page with support info, review and buy links for his or her App could easily do so. So here goes, the Standard About Page:

I started out the usual way:

  • Create a new Windows Phone application – this one I called “StandardAboutPage”
  • Add a Solution Folder “Binaries”
  • Put “GalaSoft.MvvmLight.WP7.dll”, “GalaSoft.MvvmLight.Extras.WP7.dll” and “System.Windows.Interactivity.dll” in it.

Repeat after me, kids: ” Thou shalt not make unto thee any Windows Phone 7 application without MVVMLight”

Then I added a new Windows Phone 7 class library that I called “LocalJoost.Utilities” – but feel free to pick your own name ;-) . I added references to “Microsoft.Phone.dll” to this library, and thus the library was ready for the AboutViewModelBase class. This class has the following string properties, which I will not write out for the sake of brevity:

  • AppTitle
  • About
  • Buy
  • BuyTheApp
  • CompanyUrl
  • Copyright
  • Review
  • ReviewTheApp
  • Support
  • SupportEmail
  • SupportMessage

There are also three properties that retrieve information from the application itself:

private bool? _trialMode;
public bool IsTrialMode
{
  get
  {
    if (_trialMode == null)
    {
      var s = new LicenseInformation();
#if DEBUG
      _trialMode = true;
#else
      _trialMode = new bool?(s.IsTrial());
#endif
    }
    return _trialMode.Value;
  }
}

public Visibility BuyPanelVisible
{
  get { return IsTrialMode || DesignerProperties.IsInDesignTool ? 
    Visibility.Visible : Visibility.Collapsed; }
}

public string ApplicationVersion
{
  get
  {
    if (DesignerProperties.IsInDesignTool)
      return "version x.x.x";

    Assembly assembly = Assembly.GetExecutingAssembly();
    var name = new AssemblyName(assembly.FullName);
    return name.Version.ToString(3);
  }
}

The rest of the class looks like this:

using System.ComponentModel;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using Microsoft.Phone.Marketplace;
using Microsoft.Phone.Tasks;

namespace LocalJoost.Utilities
{
  public class AboutViewModelBase : ViewModelBase
  {

    public ICommand BuyCommand
    {
      get
      {
        return new RelayCommand(() => 
          new MarketplaceDetailTask().Show());
      }
    }

    public ICommand ReviewCommand
    {
      get
      {
        return new RelayCommand(() => 
          new MarketplaceReviewTask().Show());
      }
    }

    public ICommand ViewWebsiteCommand
    {
      get
      {
        return new RelayCommand(() =>  
          new WebBrowserTask { URL = CompanyUrl }.Show());
      }
    }

    public ICommand SupportQuestionCommand
    {
      get
      {
        return new RelayCommand(() =>
          {
            var emailComposeTask = new EmailComposeTask
            {
              To = SupportEmail,
              Subject =
                Support + " " + AppTitle + " " +
                ApplicationVersion
            };
            emailComposeTask.Show();
          });
      }
    }

    public void LoadValuesFromResource<T>()
    {
      var targetType = GetType();
      var sourceType = typeof(T);
      foreach (var targetProperty in targetType.GetProperties())
      {
        var sourceProperty =  sourceType.GetProperty(targetProperty.Name, 
                      BindingFlags.Static | BindingFlags.Public);
        if (sourceProperty != null)
        {
          targetProperty.SetValue(this, 
             sourceProperty.GetValue(null, null), null);
        }
      }
    }
  }
}

These are the four commands corresponding to the four basic tasks the user accessing the about page should be able to perform:

  • buy the App (if the current version is a trial version)
  • review the App
  • view the support website
  • send a support question e-mail

The LoadValuesFromResource<T> method is a typical implementation of the paradigm that every good programmer is a lazy programmer –  if you make sure your resource key names match the AboutViewModelBase property names, this method will automatically copy values from the resource to the AboutViewModel class. This will be demonstrated later on.

Anyway, in my StandardAboutPage project I made a Windows Phone Portrait Page “AboutPage” and a resource file “AppResources” with the following entries:

resources

I always go to the resource file's properties and set "Custom Tool" to "PublicResXFileCodeGenerator". This is not the default. Anyway, in that same project I make a very simple class AboutViewModel:

using LocalJoost.Utilities;

namespace StandardAboutPage
{
  public class AboutViewModel : AboutViewModelBase
  {
    public AboutViewModel()
    {
      LoadValuesFromResource<AppResources>();
    }
  }
}

In the constructor I simply call the LoadValuesFromResource method from the base class, pass my resource type and the ViewModel is ready and filled. In order to make this work, your StandardAboutPage project needs to have references to three dll’s in the “Binaries” solution folder al well as to the “LocalJoost.Utilities” project.

All that’s left now is a ‘bit’ of XAML. First of all, you add the following namespaces to your AboutPage.xaml:

xmlns:LJUtilities="clr-namespace:StandardAboutPage"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:GalaSoft_MvvmLight_Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP7"

Be aware that in this blog text between double quotes is split over two lines due to space limitations, but in your code they should be on one lines. Then, just above the “LayoutRoot” grid, you add the following resource

<phone:PhoneApplicationPage.Resources>
  <LJUtilities:AboutViewModel x:Key="AboutViewModel"/>
</phone:PhoneApplicationPage.Resources>
And then basically you are free to make up your XAML. I replaced the whole default LayoutRoot grid by what I got form Manfred, which I basically only adapted to use the ViewModel's properties and commands:
<Grid x:Name="LayoutRoot" DataContext="{StaticResource AboutViewModel}">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
  </Grid.RowDefinitions>

  <!--TitlePanel contains the name of the application and page title-->
  <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,12">
    <TextBlock x:Name="ApplicationTitle" Text="{Binding AppTitle}" 
      Style="{StaticResource PhoneTextNormalStyle}" />
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="60" />
        <ColumnDefinition />
      </Grid.ColumnDefinitions>
      <Canvas xmlns="http://schemas.microsoft.com/client/2007" 
     Width="15.583" Height="60.000">
        <Path Fill="{StaticResource PhoneAccentBrush}" Data=" M 15.393,3.596 C 17.718,19.307 -2.038,16.232 0.173,5.387 C 1.435,-0.804 11.028,-2.002 15.393,3.596 Z" />
        <Path Fill="{StaticResource PhoneAccentBrush}" Data=" M 1.963,18.816 C 5.843,18.816 9.722,18.816 13.602,18.816 C 13.602,32.545 13.602,46.272 13.602,60.000 C 9.722,60.000 5.843,60.000 1.963,60.000 C 1.963,46.272 1.963,32.545 1.963,18.816 Z" />
      </Canvas>
      <TextBlock Grid.Column="1" x:Name="PageTitle" Text="{Binding About}" 
      Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" />
    </Grid>
  </StackPanel>

  <!--ContentPanel - place additional content here-->
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <Grid.RowDefinitions>
      <RowDefinition />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <StackPanel>
      <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding AppTitle}" 
          FontSize="{StaticResource PhoneFontSizeLarge}" 
          FontFamily="{StaticResource PhoneFontFamilySemiBold}" 
          VerticalAlignment="Bottom" 
        Foreground="{StaticResource PhoneAccentBrush}" />
        <TextBlock Text="{Binding ApplicationVersion}" Margin="12,0,0,2" 
        FontSize="{StaticResource PhoneFontSizeMediumLarge}" 
          VerticalAlignment="Bottom" 
        FontFamily="{StaticResource PhoneFontFamilySemiBold}" />
      </StackPanel>
      <TextBlock TextWrapping="Wrap" Text="{Binding Copyright}" />
      <ScrollViewer Margin="0,10,0,0">
        <StackPanel Orientation="Vertical">
          <TextBlock Text="{Binding SupportMessage}" TextWrapping="Wrap" 
            d:LayoutOverrides="Width" />
          <Button x:Name="MailButton" Padding="17,12">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <GalaSoft_MvvmLight_Command:EventToCommand 
                  Command="{Binding SupportQuestionCommand, Mode=OneWay}"/>
              </i:EventTrigger>
            </i:Interaction.Triggers>
            <Canvas xmlns="http://schemas.microsoft.com/client/2007" 
              Width="23.903" Height="18.441">
              <Path Fill="{StaticResource PhoneForegroundBrush}" Data="F1 M 2.446,15.994 L 2.446,5.334 L 11.021,12.021 C 11.243,12.193 11.510,12.279 11.774,12.279 C 12.038,12.279 12.303,12.193 12.524,12.023 L 21.457,5.100 L 21.457,15.994 L 2.446,15.994 Z M 20.883,2.447 L 11.776,9.506 L 2.728,2.447 L 20.883,2.447 Z M 22.678,0.000 L 1.221,0.000 C 0.547,0.000 0.000,0.547 0.000,1.223 L 0.000,17.217 C 0.000,17.893 0.547,18.441 1.221,18.441 L 22.678,18.441 C 23.354,18.441 23.903,17.893 23.903,17.217 L 23.903,1.223 C 23.903,0.547 23.354,0.000 22.678,0.000" />
            </Canvas>
          </Button>
          <StackPanel x:Name="BuyPanel" Orientation="Vertical" 
            Visibility="{Binding BuyPanelVisible}">

            <TextBlock TextWrapping="Wrap" Text="{Binding BuyTheApp}"/>
            <Button x:Name="BuyButton" Content="{Binding Buy}">
              <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                  <GalaSoft_MvvmLight_Command:EventToCommand 
                    Command="{Binding BuyCommand, Mode=OneWay}"/>
                </i:EventTrigger>
              </i:Interaction.Triggers>
            </Button>
          </StackPanel>
          <TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" 
             Text="{Binding ReviewTheApp}"/>
          <Button x:Name="ReviewButton" Content="{Binding Review}">
            <i:Interaction.Triggers>
              <i:EventTrigger EventName="Click">
                <GalaSoft_MvvmLight_Command:EventToCommand 
                  Command="{Binding ReviewCommand, Mode=OneWay}"/>
              </i:EventTrigger>
            </i:Interaction.Triggers>
          </Button>
        </StackPanel>
      </ScrollViewer>
    </StackPanel>
    <HyperlinkButton Content="{Binding CompanyUrl}" Grid.Row="1" 
       Margin="0,0,0,6" >
      <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
          <GalaSoft_MvvmLight_Command:EventToCommand 
            Command="{Binding ViewWebsiteCommand, Mode=OneWay}"/>
        </i:EventTrigger>
      </i:Interaction.Triggers>
    </HyperlinkButton>
  </Grid>
</Grid>

which you might as well forget about and just copy it from the sample solution which I uploaded. The net result is something like this in trial mode:

aboutsample

If your app is not running in trial mode, the text “If you prefer the full version… “ and the “buy” button are invisible. Of course, you can change the texts that are displayed by changing the resource, or you can have a ball and make your own stylish look & feel for an about page. It’s up to you. But using a resource file, my base class and Manfred’s XAML you can make a fully functional about page in next to no time, and there is no excuse now for failing section 5.6 ever again.

Update: you might want to change "trail" into "trial" in your resource file ;-). Thanks to Matthijs Hoekstra for spotting this.

12 January 2011

Tombstoning MVVMLight ViewModels with SilverlightSerializer on Windows Phone 7

I’ve been down this road before but I thought it wise to revisit this subject, since I see a lot of people in the Windows Phone 7 developer community still struggling with the concept of tombstoming. In my previous attempt to make some universal way of tombstoning ViewModels on Windows Phone 7 I used DataContractSerializer, which is a nice idea but does not work very well with MVVMLight since the ViewModelBase is not serializable. And sometimes you have to go trough a lot of hooplah if stuff you use in your code does not turn out to be serializable after all.There is a better way, I think. So here it is: universal tombstoning for MVVMLight, take two.

Enter SilverlightSerializer by the as far as I am concerned immortal Mike Talbot. Download this thing and store it somewhere in your sources. To use it for tombstoning, I have created the following extension methods:

using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Windows;
using GalaSoft.MvvmLight;

namespace LocalJoost.Utilities
{
  public static class ApplicationExtensions
  {
    private static string GetIsFile( Type t)
    {
      return string.Concat(t.Name, ".dat");
    }

    public static T RetrieveFromIsolatedStorage<T>(this Application app) 
      where T : class
    {
      using (var userAppStore = 
         IsolatedStorageFile.GetUserStoreForApplication())
      {
        var dataFileName = GetIsFile(typeof(T));
        if (userAppStore.FileExists(dataFileName))
        {
          using (var iss = userAppStore.OpenFile(dataFileName, FileMode.Open))
          {
             return SilverlightSerializer.Deserialize(iss) as T;
          }
        }
      }
      return null;
    }

    public static void SaveToIsolatedStorage(this Application app, 
                                      ViewModelBase model)
    {
      var dataFileName = GetIsFile((model.GetType()));
      using (var userAppStore = 
               IsolatedStorageFile.GetUserStoreForApplication())
      {
        if (userAppStore.FileExists(dataFileName))
        {
          userAppStore.DeleteFile(dataFileName);
        }
        using (var iss = userAppStore.CreateFile(dataFileName))
        {
          SilverlightSerializer.Serialize(model,iss);          
        }
      }
    }
  }
}

Then I go to the App.xaml.cs and modify it as depicted next:

private void Application_Launching(object sender, LaunchingEventArgs e)
{
  LoadModel();
}

private void Application_Activated(object sender, ActivatedEventArgs e)
{
  LoadModel();
}

private void LoadModel()
{
  try
  {
    MyMainViewModel.Instance = this.RetrieveFromIsolatedStorage<MyMainViewModel>();
  }
  catch (Exception){ }
  if( MyMainViewModel.Instance == null) MyMainViewModel.CreateNew(); 
}

private void Application_Deactivated(object sender, DeactivatedEventArgs e)
{
  this.SaveToIsolatedStorage(MyMainViewModel.Instance);
}

private void Application_Closing(object sender, ClosingEventArgs e)
{
  this.SaveToIsolatedStorage(MyMainViewModel.Instance);
}

In red you see my additions to the standard generated App.xaml.cs. You will also need to add at least one “using” statement (using LocalJoost.Utilities) and a second one with the namespace in which you have put MyMainViewModel. Notice the fact that there is an empty try-catch around the RetrieveFromIsolatedStorage call. This is intentional. Since binary deserialization tends to throw exceptions if you have changed the ViewModel’s code, you always want to make sure your application gets served a working ViewModel where ever it needs to come from.

The skeleton of a MainViewModel looks something like this in my household:

public class MyMainViewModel : ViewModelBase
{
  public MyMainViewModel( )
  {
  }  

  private static MyMainViewModel _instance;
  public static MyMainViewModel Instance
  {
    get {return _instance;}
    set { _instance = value; }
  }

  public static void CreateNew()
  {
    if (_instance == null)
    {
      _instance = new MyMainViewModel();
    }
  }
}

If your want certain properties of your ViewModel specifically not to be serialized (because they come from a – in this example omitted – application model) you can mark them with the [DoNotSerialize] attribute that comes with SilverlightSerializer.

You can now always bind to MyMainViewModel.Instance and you will either get a fresh new or a retrieved from isolated storage ViewModel. The fun thing is that although MVVMLight’s ViewModelBase is not serializable, the SilverlightSerializer does in fact serialize it which makes life a whole lot easier.

You can, by the way, also decide to store the ViewModel on the PhoneApplicationService on deactivation and retrieve it there on activation. IMHO always storing on isolated storage gives a more consistent feeling to an application. But that’s just me.

A few important points to conclude this post:

  • A ViewModel that is to be serialized on isolated storage should always have a default constructor.
  • Properties of the ViewModel that you want to have serialized need to have a getter and a setter (believe me, you can spend some time overlooking this).
  • Do not try something clever like this on the static instance property of your ViewModel:
public static MyMainViewModel Instance
{
  get {return _instance ?? (_instance = new MyMainViewModel());}
  set { _instance = value; }
}
If you do this, MyMainViewModel.Instance = this.RetrieveFromIsolatedStorage will first call the getter, thus creating a new instance of MyMainViewModel whatever happens next, then overwrite it with the retrieved version. But if you do something like registering a message in the constructor of MyMainViewModel, the initially created model will stay alive ‘somewhere’ - you will end up with two instances of your supposedly singleton instance running in you App, both listening to the same message and reacting to it - and spend, like me, a not so nice and probably extended period of  time scratching your head and wondering what the hell is going on.

I hope that with this post I once and for all made it easier for the Windows Phone 7 community to tackle something apparently tricky as tombstoning thus improving the quality of what we are all going to put in the App Hub.