Colin Eberhardt's Technology Adventures

A Simple Windows Phone 7 MVVM Tombstoning Example

May 23rd, 2011

This blog post shows how to implement tombstoning within a Windows Phone 7 application that following the Model-View-ViewModel pattern.

UPDATE: I have published a update to this blog post to handle the new dormant state in Windows Phone 7.1 (Mango).


Colin Eberhardt is a Scott Logic technical Evangelist and a Technical Architect for Visiblox, suppliers of high-performance WPF and Silverlight charts.

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 smartphones 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 you 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 the that 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 
{
  // Twitter search for #WP7
  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;
  }
 
  /// <summary>
  /// Parses the response from our twitter request, creating a list of FeedItemViewModelinstances
  /// </summary>
  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);
    }
  }
 
  /// <summary>
  /// Gets the feed items
  /// </summary>
  public ObservableCollection<FeedItemViewModel> FeedItems
  {
    get { return _feedItems; }
  }
 
  /// <summary>
  /// Gets a feed item by its Id
  /// </summary>
  public FeedItemViewModel GetFeedItem(long id)
  {
    return _feedItems.SingleOrDefault(item => item.Id == id);
  }
 
  /// <summary>
  /// Update the feed items
  /// </summary>
  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 scenarious) 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 blogpost!), 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
{
 
  /// <summary>
  /// Gets the ViewModel
  /// </summary>
  public FeedViewModel ViewModel { get; private set; }
 
  ...
 
  private void Application_Launching(object sender, LaunchingEventArgs e)
  {
    ViewModel = new FeedViewModel();
    ViewModel.Update();
 
    // set the frame DataContext
    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;
 
    // navigate to the feed items page
    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 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);
 
    // parse the querystring to extract the id
    long feedItemId = long.Parse(NavigationContext.QueryString["id"]);
 
    // obtain this item from the view model.
    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 an 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-activeated 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 querystring 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 tomstoned 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.

// Code to execute when the application is closing (eg, user hit Back)
// This code will not execute when the application is deactivated
private void Application_Closing(object sender, ClosingEventArgs e)
{
    // persist the data using isolated storage
    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:

// Code to execute when the application is launching (eg, from Start)
// This code will not execute when the application is reactivated
private void Application_Launching(object sender, LaunchingEventArgs e)
{
  // load the view model from isolated storage
  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 the view model is not loaded, create a new one
  if (ViewModel == null)
  {
    ViewModel = new FeedViewModel();
    ViewModel.Update();
  }
 
  // set the frame DataContext
  RootFrame.DataContext = ViewModel;
}

NOTE: This is no 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 tomstoning, not twitter application development!

Tombstoning UI state

So, now our application saves state during tomstoning 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, tomstoning 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 to 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 tomstoned 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 databinding. 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.

/// <summary>
/// Gets the NavigationList ItemsPanel
/// </summary>
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);
 
  // persist the scroll position within the page state
  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);
 
  // restore the scroll position
  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 tomstoning 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 sourcecode for this example application: WP7MVVMTombstone.zip

Regards, Colin E.

Metro In Motion #5 – SandwichFlow

May 16th, 2011

For the past few months I have been writing a Metro-In-Motion blog series which describes how to recreate some of the fluid effects found in native Windows Phone 7 applications within your own applications. So far I have covered fluid list animations, ‘peel’ animations, flying titles and a ’tilt’ effect – and now … I am all out of ideas!

The previous blog posts used a pretty simple little application to demonstrate their functionality, so I thought it might be fun to put these effects together within a more meaningful application. Rather than simply re-hashing an old idea, like a Twitter app, I decided to create something original, something inspiring, something which explores a subject area close to my heart – and stomache. This is how I came up with the (entirely original) SandwichFlow … an application for the sandwich aficionado!!!

SandwichFlow – As good as sandwiches get on a mobile … Brought to you by Team Distraction and ColinDoesIt(AsWell):

Random Points Of Interest

The important bits of this application are covered in the previous Metro-In_motion blog posts, so the following is just a random bunch of things that your might find interesting about this application:

Recipe Thief

The recipes were scraped from the fantastic BBC Food web-pages, which are published in XHTML format, making it easy for me to grab the various component parts of each recipe. The scraped output was saved as an XML file which the application uses to build a view model using Linq to XML:

XDocument doc = XDocument.Parse(xml);
Sandwiches = doc.Descendants("sandwich")
                .Select(el => new Sandwich()
                {
                  Title = el.Attribute("title").Value,
                  Id = id++,
                  Keywords = el.Descendants("keyword")
                                .Select(l => Regex.Replace(l.Value, @"\b(\w)", m => m.Value.ToUpper()))                                      
                                .ToList(),
                  Ingredients = el.Descendants("ingredient")
                                  .Select(l => l.Value)
                                  .ToList(),
                  Instructions = el.Descendants("instruction")
                                  .Select(l => new InstructionStep()
                                  {
                                    Text = l.Value,
                                    Step = l.ElementsBeforeSelf().Count() + 1
                                  })
                                  .ToList()
 
                })
                .ToList();

Jump then slide

The front-page of the application uses the JumpList control I wrote a few months back, with DataTemplates supplied for rending recipes or keywords that have the MetroInMotion.AnimationLevel property set on various elements so that they slide gracefully as the pivot control is animated from one item to the next:

<DataTemplate x:Key="SandwichTemplate">
  <StackPanel Orientation="Vertical"
              Margin="0,5,0,5"
              mim:MetroInMotion.Tilt="3">
    <TextBlock  Text="{Binding Title}"
                mim:MetroInMotion.AnimationLevel="1"
                x:Name="Title"
                Foreground="Black"
                FontSize="{StaticResource PhoneFontSizeLarge}"/>
    <TextBlock Text="{Binding KeywordSummary}"
                mim:MetroInMotion.AnimationLevel="2"
                FontSize="{StaticResource PhoneFontSizeNormal}"
                Foreground="#888"/>
  </StackPanel>
</DataTemplate>

Bring it back

I originally added the background image to each page, then I realised a better place to put this is the application frame:

<Style x:Key="mainFrameStyle"
    TargetType="phone:PhoneApplicationFrame">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="phone:PhoneApplicationFrame">
        <Grid Background="White">
          <!-- background image -->
          <Image Source="background.jpg"
                HorizontalAlignment="Stretch"
                VerticalAlignment="Stretch"/>
          <Border x:Name="ClientArea"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}"
                  Background="{TemplateBinding Background}"
                  HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                  VerticalAlignment="{TemplateBinding VerticalAlignment}">
            <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}"
                              Content="{TemplateBinding Content}"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                              Margin="{TemplateBinding Padding}"
                              VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
          </Border>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

The application frame remains throughout the application lifetime, it hosts the currently displayed page. I have a feeling that the application frame is a little under-used. Rather than adding your application title to each page, why not add it to the frame?

The same but different

The code to find similar sandwiches uses an interesting Linq method which I have not used before, Intersect. It is always good to learn something new!

// locate related sandwiches
foreach (var sandwich in Sandwiches)
{
  sandwich.Similar = Sandwiches.Where(s => s != sandwich)
                      .Select(s =>
                        new
                        {
                          Sandwich = s,
                          Score = KeywordCorrelation(s.Keywords, sandwich.Keywords)
                        })
                      .OrderBy(s => s.Score).Reverse()
                      .Take(5)
                      .Select(s => s.Sandwich)
                      .ToList();
}
 
private double KeywordCorrelation(List<string> keywordsOne, List<string> keywordsTwo)
{
  var commonKeywords = keywordsOne.Intersect(keywordsTwo);
  var allKeywords = keywordsOne.Union(keywordsTwo).Distinct();
  return (double)commonKeywords.Count() / (double)allKeywords.Count();
}

The KeywordCorrelation methods finds the number of keywords that both sandwiches have in common, then divides this by the distinct collection of keywords for both sandwiches. Yummy.

Slippery Ingredients

When you view a recipe, the ingredients slide in from the right. I didn’t really have time to find images for all of the ingredients on the web, so each recipe has the ingredients of my favourite – the cheese and pickle sandwich. Each ingredient slides in using a storyboard, with their BeginTime staggered:

<Image Source="tom.png"
        Margin="150,70,0,0"
        mim:MetroInMotion.AnimationLevel="2">
  <Image.RenderTransform>
    <TranslateTransform x:Name="tomTrans"
                          X="400"/>
  </Image.RenderTransform>
  <Image.Triggers>
    <EventTrigger RoutedEvent="Image.Loaded">
      <BeginStoryboard>
        <Storyboard BeginTime="00:00:0.2">
          <DoubleAnimation Duration="00:00:0.7"
                            Storyboard.TargetName="tomTrans" 
                            Storyboard.TargetProperty="X" 
                            From="400" To="0">
            <DoubleAnimation.EasingFunction>
              <SineEase EasingMode="EaseOut"/>
            </DoubleAnimation.EasingFunction>
          </DoubleAnimation>
        </Storyboard>
      </BeginStoryboard>
    </EventTrigger>
  </Image.Triggers>
</Image>

They also have the MetroInMotion.AnimationLevel attached property set so that they slide with the pivot control.

You can grab the code here: SandwichFlow.zip

Regards, Colin E.

White Paper: Flex, Silverlight or HTML5? Time to decide…

May 5th, 2011

When meeting current and prospective clients the subject of web technology choice often arises. There has been a great deal of confusion and uncertainty out there, even before Microsoft’s perceived change of stance with respect to Silverlight emerged.

This white paper is intended to help technology decision makers come to an informed choice when faced with this difficult decision. You can view the paper below, or download it in PDF format: Flex-Silverlight-HTML5.pdf (1.3 MBytes)

Thoughts? comments? leave them below …

Regards, Colin E.

Metro In Motion Part #4 – Tilt Effect

May 3rd, 2011

This blog post describes the implementation of a metro ’tilt’ effect for Windows Phone 7 which causes element to respond to user interactions by tilting in 3D

So far in the “Metro In Motion” series I have covered fluid list animations, ‘peel’ animations and flying title. In this blog post I am looking at the ’tilt’ effect seen in native Windows Phone 7 applications where list items, tiles, check-boxes and other user interface element tilt slightly when the user presses them. This is a subtle 3D effect that makes the phone interface more tactile and playful.

I was in two minds about whether to implement my own tilt effect since there is an implementation of this effect available via the Silverlight (for Windows Phone 7) Toolkit. However, I am not that keen on the toolkit implementation of this feature. Firstly, the effect is applied by specifying the types of element that should tilt, unfortunately this fails in various scenarios; secondly, the tilt is a bit over-the-top. Personally, I think this effect works best when it is at its most subtle.

The video below shows the tilt effect in action, together with the other Metro In Motion effects:



The implementation of this effect is actually rather simple, relying heavily on the PlaneProjection transform that makes 3D transformations accessible to mere mortals like myself (for more powerful and flexible 3D effects I would thoroughly recommend René Schule’s Matrix3DEx library). The effect is added to any element by applying an attached property:

<Button local:MetroInMotion.Tilt="6"/>

The value of the Tilt property defines how pronounced the effect is. When the Tilt property is attached, event handlers are added to the UI element:

// The extent of the tilt action, the larger the number, the bigger the tilt
private static double TiltAngleFactor = 4;
 
// The extent of the scaling action, the smaller the number, the greater the scaling.
private static double ScaleFactor = 100;
 
private static void OnTiltChanged(DependencyObject d,
  DependencyPropertyChangedEventArgs args)
{
  FrameworkElement targetElement = d as FrameworkElement;
 
  double tiltFactor = GetTilt(d);
 
  // create the required transformations
  var projection = new PlaneProjection();
  var scale = new ScaleTransform();
  var translate = new TranslateTransform();
 
  var transGroup = new TransformGroup();
  transGroup.Children.Add(scale);
  transGroup.Children.Add(translate);
 
  // associate with the target element
  targetElement.Projection = projection;
  targetElement.RenderTransform = transGroup;
  targetElement.RenderTransformOrigin = new Point(0.5, 0.5);
 
  targetElement.MouseLeftButtonDown += (s, e) =>
    {
      var clickPosition = e.GetPosition(targetElement);
 
      // find the maximum of width / height
      double maxDimension = Math.Max(targetElement.ActualWidth, targetElement.ActualHeight);
 
      // compute the normalised horizontal distance from the centre
      double distanceFromCenterX = targetElement.ActualWidth / 2 - clickPosition.X;
      double normalisedDistanceX = 2 * distanceFromCenterX / maxDimension; 
 
      // rotate around the Y axis 
      projection.RotationY = normalisedDistanceX * TiltAngleFactor * tiltFactor;
 
      // compute the normalised vertical distance from the centre
      double distanceFromCenterY = targetElement.ActualHeight / 2 - clickPosition.Y;
      double normalisedDistanceY = 2 * distanceFromCenterY / maxDimension;
 
      // rotate around the X axis, 
      projection.RotationX = -normalisedDistanceY * TiltAngleFactor * tiltFactor;
 
      // find the distance to centre
      double distanceToCentre = Math.Sqrt(normalisedDistanceX * normalisedDistanceX
        + normalisedDistanceY * normalisedDistanceY);
 
      // scale accordingly
      double scaleVal = tiltFactor * (1 - distanceToCentre) / ScaleFactor;
      scale.ScaleX = 1 - scaleVal;
      scale.ScaleY = 1 - scaleVal;
    };
 
  targetElement.ManipulationCompleted += (s, e) =>
    {
      var sb = new Storyboard();
      sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationY", projection));
      sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationX", projection));
      sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleX", scale));
      sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleY", scale));
      sb.Begin();
    };
 
}

When the element is clicked, the distance of the click location to the centre is computed. A PlaneProjection is used to tilt the element based on the click location, with a click at the top causing it to tilt around the X axis, and a click on the side tilting around the Y axis. A ScaleTransform is used to make the element shrink slightly, giving a user the feeling that the element has been pushed into the phone slightly.

When the manipulation is complete the properties of these various transforms are animated back to their original values using
CreateAnimation which is a simple utility method for creating DoubleAnimation instances:

private static DoubleAnimation CreateAnimation(double? from, double? to, double duration,
  string targetProperty, DependencyObject target)
{
  var db = new DoubleAnimation();
  db.To = to;
  db.From = from;
  db.EasingFunction = new SineEase();
  db.Duration = TimeSpan.FromSeconds(duration);
  Storyboard.SetTarget(db, target);
  Storyboard.SetTargetProperty(db, new PropertyPath(targetProperty));
  return db;
}

The magnitude of the ScaleTransform and PlaneProjection depends on the click location, with clicks to the centre of the element resulting in a scaling, but no rotation, and clicks at the edge causing rotation but no scaling. The net result can be observed below where the effect of clicking at various locations of an element is shown:

You can also see the effect of clicking a square element below:

Finally, native phone applications have another subtle effect where elements that are tilted at the bottom of the screen appear to be viewed from above, whilst ones at the top are viewed from below. This can be achieved by applying a local offset to the PlaneProjection and an equal and opposite vertical TranslateTransform. The following code is added …

targetElement.MouseLeftButtonDown += (s, e) =>
  {
    // ... scale and plane projection transforms show earlier go here ...
 
    // offset the plane transform
    var rootElement = Application.Current.RootVisual as FrameworkElement;
    var relativeToCentre = (targetElement.GetRelativePosition(rootElement).Y - rootElement.ActualHeight / 2) / 2;
    translate.Y = -relativeToCentre;
    projection.LocalOffsetY = +relativeToCentre;
 
  };
 
targetElement.ManipulationCompleted += (s, e) =>
  {
    var sb = new Storyboard();
    sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationY", projection));
    sb.Children.Add(CreateAnimation(null, 0, 0.1, "RotationX", projection));
    sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleX", scale));
    sb.Children.Add(CreateAnimation(null, 1, 0.1, "ScaleY", scale));
    sb.Begin();
 
    translate.Y = 0;
    projection.LocalOffsetY = 0;
  };

Which results in the following effect:

With that, the effect is complete!

Please note, the images in this blog post use a Tilt ‘factor’ of 6 in order to make the effect easier to see in these static images. I would urge you to use a much lower factor, perhaps 2, to make this effect much more subtle. I think it works best if the user almost doesn’t notice the effect, rather they ‘feel’ it.

You can download the full sourcecode for this blog post here: MetroInMotion4.zip

Regards,
Colin E.