19 April 2013

Windows Phone 8 navigation part 1–geocoding, tombstoning–and testing

After my post about reverse geocoding I set out to make a little app to demonstrate routing in Windows Phone 8. The demo app went quite out of hand, so I decided to split the post up in a few smaller posts. In the course of it I am going to build a basic navigation app that enables the user to determine two locations, find a route between those locations, and display them on a map – all using MVVMLight, of course.

And now the 2012.2 update to Visual Studio is released, we can finally build Windows Phone MVVM apps the way things are intended to be: by writing some unit test first, getting the basic functions right, before creating an all-out app. This makes it especially handy to test one important requirement that go for all my apps – all the models and viewmodels must be serializabe, so I can tombstone using SilverlightSerializer like I have been doing for over two years now.

At this point I am not really sure how much blog posts this will take me, but I guess at least three, maybe four.

What is unit testing and why should I do that?

Professional software developers are usually all in on this. What you basically do is write code that asserts that pieces of your code are behaving the way you expect them to do. I am sure everyone has had the episode that you change one little thing that should be inconsequential and suddenly, at some seemingly totally unrelated place, things start going South. Unit tests call little pieces of of your code and test if the result of calling a method, setting a property or whatever gives the result you expect. If you write unit tests, and then change something, and test start failing in unrelated places – it’s like a smoke detector going off. Your code starts detecting bugs for you. Nice, eh? I also gives you the a way to mess around with all kinds of APIs getting things right before you start wasting time on a complex GUI that you can’t get to work because the underlying code cannot work the way you want.

What is geocoding?

Geocoding is what we GIS buffs say when we mean ‘finding a location on earth by it’s name”. If I put “Boston  USA” in a geocoder I expect to get a coordinate that puts me somewhere on the east coast of the United States, if I enter “Springerstraat 36 Netherlands” I expect a coordinate that shows me my own house, or somewhere nearby. Some geocoders can take info that’s not tied to an address, but things like, like ‘town hall Little Rock USA”. In general – in goes a descriptive text, out come one or more matches with coordinates.

Enough introduction. Let’s code.

Setting the stage

I started out doing the following:

  • Create a new Windows Phone App “NavigationDemo”. Target framework 8.0
  • Add a Windows Phone Class Library “NavigationDemo.Logic”
  • Add a Windows Phone Unit Test app “NavigationDemo.Logic.Test”
  • In NavigationDemo, create a reference to NavigationDemo.Logic
  • In NavigationDemo.Logic.Test, make a reference to NavigationDemo.Logic as well.
  • In both NavigationDemo and NavigationDemo.Logic.Test, select WMAppManifest.xml in Properties and enable the “ID_CAP_MAP” capbility

Now, because I am a lazy ******* and like to re-use I did things before, bring in the following nuget packages:

  • wp7nl (this will pull in MVVMLight Libraries-only version and the Windows Phone toolkit as well)
  • Microsoft.Bcl.Async

wp7nl also has a Windows Phone 8 version (it’s name is retained for historic reasons). Install both packages in all three projects.

GeocodeModel – take one

In “NavigationDemo.Logic”, add a folder “GeocodeModel” and put the following class in there:

using System;
using System.Collections.Generic;
using System.Device.Location;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Phone.Maps.Services;
using Wp7nl.Utilities;

namespace NavigationDemo.Logic.Models
{
  public class GeocodeModel
  {
    public GeocodeModel()
    {
      MapLocations = new List<MapLocation>();
      SearchLocation = new GeoCoordinate();
    }
    public string SearchText { get; set; }

    public GeoCoordinate SearchLocation { get; set; }

    public MapLocation SelectedLocation { get; set; }

    public List<MapLocation> MapLocations { get; set; }

    public async Task SearchLocations()
    {
      MapLocations.Clear();
      SelectedLocation = null;
      var geoCoder = new GeocodeQuery
      {
        SearchTerm = SearchText,
        GeoCoordinate = SearchLocation
      };
      MapLocations.AddRange(await geoCoder.GetMapLocationsAsync());
      SelectedLocation = MapLocations.FirstOrDefault();
    }
  }
}

To perform geocoding, we need the GeocodeQuery class. So we embed that into a class with a method to perform the actual geocoding, a search string to holds the user input, a list of MapLocation (the output of GeocodeQuery) and SelectedLocation to the user’s selection.

Note there is also a SearchLocation property of type GeoCoordinate. That’s because the GeocodeQuery also needs a location to start searching from. If the programmer using my model doesn’t set it, I choose a default value. But you can imagine this being useful if someone just enters ‘Amersfoort’ for SearchText and a coordinate somewhere in the Netherlands – that way the GeocodeQuery knows that you want to have Amersfoort in the Netherlands, and not the Amersfoort in South Africa. Anyway, it’s now time for

Writing the search test

Add a new class GeocodeModelTest to NavigationDemo.Logic.Test and let’s write our first test:

using System;
using System.Threading;
using System.Windows;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using NavigationDemo.Logic.Models;

namespace NavigationDemo.Logic.Test
{
  [TestClass]
  public class GeocodeModelTest
  {
    [TestMethod]
    public void TestFindBoston()
    {
      var m = new GeocodeModel { SearchText = "Boston USA" };
      var waitHandle = new AutoResetEvent(false);

      Deployment.Current.Dispatcher.BeginInvoke(async () =>
      {
        await m.SearchLocations();
        waitHandle.Set();
       });
      waitHandle.WaitOne(TimeSpan.FromSeconds(5));

      Assert.AreEqual(m.SelectedLocation.GeoCoordinate.Latitude, 42, 1);
      Assert.AreEqual(m.SelectedLocation.GeoCoordinate.Longitude, -71, 1);
      Assert.AreEqual(m.SelectedLocation.Information.Address.City, 
         "Boston");
      Assert.AreEqual(m.SelectedLocation.Information.Address.State,
        "Massachusetts");
      Assert.AreEqual(m.SelectedLocation.Information.Address.Country, 
        "United States of America");
    }
  }
}

The GeocodeQuery runs async and needs to run on the UI thread as well. If you have no idea what I am fooling around here with the Dispatcher and the AutoResetEvent, please read this article first. Anyway, this test works. Boston is indeed on the east coast of the United States and still in Massachusetts. Most reassuring. Now let’s see if SilverlightSerializer will indeed serialize this.

Writing the serialization test – take one

The first part is basically a repeat of the first test – writing unit test sometimes involves a lot of boring copy & paste work – but the last part is different:
 [TestMethod]
 public void TestStoreAndRetrieveBoston()
 {
   var m = new GeocodeModel { SearchText = "Boston USA" };
   var waitHandle = new AutoResetEvent(false);

   Deployment.Current.Dispatcher.BeginInvoke(async () =>
   {
     await m.SearchLocations();
     waitHandle.Set();
   });
   waitHandle.WaitOne(TimeSpan.FromSeconds(5));
   Assert.IsNotNull(m.SelectedLocation);

   // Actual test
   var h = new IsolatedStorageHelper<GeocodeModel>();
   if (h.ExistsInStorage())
   {
     h.DeletedFromStorage();
   }
   h.SaveToStorage(m);

   var retrievedModel = h.RetrieveFromStorage();
   Assert.AreEqual(retrievedModel.SelectedLocation.Information.Address.City,
	"Boston");
 }
}

Adding this test to GeocodeModelTest will reveal a major bummer – a couple of the classes that are returned by GeocodeQuery – starting with MapLocation - have private constructors and cannot be serialized. Our model cannot be serialized. The usual approach to this kind of problem is to write a kind of wrapper class that can be serialized. But… using MVVMLight you are most of the time making wrapper classes anyway – that’s what a ViewModel is, after all, so let’s use that.

Writing the serialization test - take two

First, adorn the stuff that cannot be serialized in the GeocodeModel with the [DoNotSerialize] attribute, like this:

[DoNotSerialize]
public MapLocation SelectedLocation { get; set; }

[DoNotSerialize]
public List MapLocations { get; set; }
and the test is reduced to this:
[TestMethod]
public void TestStoreAndRetrieveBoston()
{
  var m = new GeocodeModel { SearchText = "Boston USA" };
  // Actual test
  var h = new IsolatedStorageHelper();
  if (h.ExistsInStorage())
  {
    h.DeletedFromStorage();
  }
  h.SaveToStorage(m);

  var retrievedModel = h.RetrieveFromStorage();
  Assert.AreEqual(retrievedModel.SearchText, "Boston USA");
}

Hurray, this works, but the model’s results are now no longer storing stuff. MapLocations is empty, so is SelectedLocation, if they are deserialized. Bascially we are now only testing if indeed the search test is retained after storage and retrieval. Well, it is.

Enter the viewmodels

So far I mainly showed what does not work. Now it’s time to show what does. First, we make a viewmodel around MapLocation:

using System.Device.Location;
using GalaSoft.MvvmLight;
using Microsoft.Phone.Maps.Services;

namespace NavigationDemo.Logic.ViewModels
{
  public class MapLocationViewModel : ViewModelBase
  {
    public MapLocationViewModel()
    {
    }

    public MapLocationViewModel(MapLocation model)
    {
      var a = model.Information.Address;

      Address = string.Format("{0} {1} {2} {3} {4}", 
            a.Street, a.HouseNumber, a.PostalCode,
            a.City,a.Country).Trim();
      Location = model.GeoCoordinate;
    }

    private string address;
    public string Address
    {
      get { return address; }
      set
      {
        if (address != value)
        {
          address = value;
          RaisePropertyChanged(() => Address);
        }
      }
    }

    private GeoCoordinate location;
    public GeoCoordinate Location
    {
      get { return location; }
      set
      {
        if (location != value)
        {
          location = value;
          RaisePropertyChanged(() => Location);
        }
      }
    }
  }
}

That takes care of the MapLocation not being serializable. Once it is initialized, it does no longer need the model anymore. Which is a good thing, since it cannot be serialized ;-). Next is the GeocodeViewModel itself:

using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using NavigationDemo.Logic.Models;
using Wp7nl.Utilities;
using System.Linq;

namespace NavigationDemo.Logic.ViewModels
{
  public class GeocodeViewModel : ViewModelBase
  {
    public GeocodeViewModel()
    {
      MapLocations = new ObservableCollection<MapLocationViewModel>();
    }

    public string Name { get; set; }

    public GeocodeViewModel( GeocodeModel model) : this()
    {
      Model = model;
    }

    public GeocodeModel Model{get;set;}

    public ObservableCollection<MapLocationViewModel> MapLocations { get; set; }

    [DoNotSerialize]
    public string SearchText
    {
      get { return Model.SearchText; }
      set
      {
        if (Model.SearchText != value)
        {
          Model.SearchText = value;
          RaisePropertyChanged(() => SearchText);
        }
      }
    }

    private MapLocationViewModel selectedLocation;
    public MapLocationViewModel SelectedLocation
    {
      get { return selectedLocation; }
      set
      {
        if (selectedLocation != value)
        {
          selectedLocation = value;
          RaisePropertyChanged(() => SelectedLocation);
        }
      }
    }
    
    public async Task SearchLocations()
    {
      MapLocations.Clear();
      SelectedLocation = null;
      await Model.SearchLocations();
      MapLocations.AddRange(Model.MapLocations.Select( 
        p=> new MapLocationViewModel(p)));
      SelectedLocation = MapLocations.FirstOrDefault();
    }
    
    [DoNotSerialize]
    public ICommand SearchLocationCommand
    {
      get
      {
        return new RelayCommand(async () => await SearchLocation());
      }
    }
  }
}

Notice that the only attribute that is serialized by the model, is now marked [DoNotSerialize]. This is really important – since the model may not be around yet when deserializing takes place, it would result in a null reference. If you pass things to the model, let the model serialize it. If you don’t let the viewmodel take care of it.

Writing the search test for the viewmodel

So since we are now no longer testing the model but the viewmodel, I added a new class “GeocodeViewModeTest” to, well, test the viewmodel.

using System;
using System.Threading;
using System.Windows;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using NavigationDemo.Logic.Models;
using NavigationDemo.Logic.ViewModels;
using Wp7nl.Utilities;
namespace NavigationDemo.Logic.Test { [TestClass] public class GeocodeViewModelTest { [TestMethod] public void TestFindBostonWithViewModel() { var vm = new GeocodeViewModel( new GeocodeModel { SearchText = "Boston USA" }); var waitHandle = new AutoResetEvent(false); Deployment.Current.Dispatcher.BeginInvoke(async () => { await vm.SearchLocations(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.AreEqual(vm.SelectedLocation.Address, "Boston United States of America"); Assert.AreEqual(vm.SelectedLocation.Location.Latitude, 42, 1); Assert.AreEqual(vm.SelectedLocation.Location.Longitude, -71, 1); } } }

Lo and behold, this test succeeds as well. Now the second test is actually a lot more interesting:

[TestMethod]
public void TestStoreAndRetrieveBostonWithViewModel()
{
  var vm = new GeocodeViewModel(
    new GeocodeModel { SearchText = "Boston USA" });
  var waitHandle = new AutoResetEvent(false);

  Deployment.Current.Dispatcher.BeginInvoke(async () =>
  {
    await vm.SearchLocations();
    waitHandle.Set();
  });
  waitHandle.WaitOne(TimeSpan.FromSeconds(5));
  Assert.IsNotNull(vm.SelectedLocation);

  var h = new IsolatedStorageHelper<GeocodeViewModel>();
  if (h.ExistsInStorage())
  {
    h.DeletedFromStorage();
  }
  h.SaveToStorage(vm);

  var retrievedViewModel = h.RetrieveFromStorage();
  Assert.AreEqual(retrievedViewModel.SelectedLocation.Address,
    "Boston United States of America");
  Assert.AreEqual(
    retrievedViewModel.SelectedLocation.Location.Latitude, 42, 1);
  Assert.AreEqual(
    retrievedViewModel.SelectedLocation.Location.Longitude, -71, 1);
}

And indeed, after retrieving the viewmodel from storage, the same asserts are fired and the test passes. Success: we can now find location and tombstone

Conclusion

I showed you some basic geocoding – how to find a location using a text input. I hope I have showed you also that unit tests are not only a way to assure some basic code quality and behavior, but are also a way to determine ahead if things are going to work the way you envisioned. Unit test make scaffolding and proof-of-concept approach of development a lot easier – you need a lot less starting up an app, clicking the right things and then finding breakpoint-by-breakpoint what goes wrong. Quite early in my development stage I ran into the fact that some things were not serializable. Imagine finding that out when the whole app was already mostly done, and then somewhere deep down something goes wrong with the tombstoning. Not fun.

Complete code – that is, complete for such an incomplete app – can be found here. Next time, we will do some actual navigation.

To prevent flames from Test Driven Design (TDD) purists: a variant of unit tests are integration test. Technically a unit test tests only tiny things that have no relation to another, like one object, method or property. Integration tests test the workings of larger pieces of code. So technically I am mostly writing integration tests. There, I’ve said it.

10 April 2013

ViewModel driven multi-state animations using DataTriggers and Blend on Windows Phone

Long long time ago I wrote how to drive animations from your ViewModel using DataStateBehavior, and I explicitly stated this was the only way to do it, since (quoting myself), “Windows Phone 7 does not support DataTriggers”. That was then, and this is now. The drawback of DataStateBehavior is that you basically can only do on/off animations, which makes more complex multi-state animations impossible. There was another behavior that could do that, but I could not find that anymore and I could not quite remember the name. And then I suddenly stumbled upon the Microsoft.Expression.Interactions assembly – and in its Microsoft.Expression.Interactions.Core namespace there is indeed a DataTrigger. And that seems to have been present in the 7.1 framework as well. *Cough*.

So in this blog post I am going to demonstrate how to animate a few ‘popup windows’ via a single Visual State block and a ViewModel, using DataTriggers. I am going to show this using Visual Studio 2012, MVVMLight and mostly Blend. It’s time to give this unsung hero some love again, so I am going to follow the tutorial-style again.

imageSetting the stage

  • Open Visual Studio, create a “Windows Phone app”, and target 8.0 (it should work in 7.1 as well BTW)
  • Click Tools/Library Package Manager/Manage NuGet Packages for Solution.
  • Search for MvvmLightLibs, select “MVVM Light Libraries only”
  • Click “Install”, “Ok” and “I Accept”

Building the ViewModel

The ViewModel actually consist out of two files – an enumeration describing the states and the actual ViewModel itself. First, create a folder “ViewModel” in your solution, and the create the enumeration like this:

namespace DataTriggerAnimation.ViewModel
{
  public enum DisplayState
  {
    Normal = 0,
    ShowPopupHello = 1,
    ShowPopupBye = 2,
  }
}

And then the ViewModel like this:

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

namespace DataTriggerAnimation.ViewModel
{
  public class DisplayViewModel : ViewModelBase
  {
    private DisplayState displayState;
    public DisplayState DisplayState
    {
      get { return displayState; }
      set
      {
        if (displayState != value)
        {
          displayState = value;
          RaisePropertyChanged(() => DisplayState);
        }
      }
    }

    public ICommand DisplayPopupCommand
    {
      get
      {
        return new RelayCommand<string>(
            (p) =>
              {
                DisplayState = (DisplayState)Enum.Parse(typeof(DisplayState), p);
              });
      }
    }

    public ICommand CloseCommand
    {
      get
      {
        return new RelayCommand(() 
           => DisplayState = DisplayState.Normal);
      }
    }
  }
}

The important thing to note is that the command “DisplayPopupCommand” requires a parameter to determine the popup that must be displayed. The CloseCommand is equivalent to a DisplayPopupCommand with parameter “Normal”, and is just there for making the designer’s life easier.

… and that’s all the coding we are going to do. Build your application and close Visual Studio. The rest, just like my last animation post is done in Blend! All of it.

Creating the first panel

  • imageDrag a grid on the empty rectangle “ContentPanel”. Like all GUI objects, these can be found on the Assets tab on the left top.
  • Click on the “Grid” in the “Objects on Timeline” pane left, and rename it to “Panel1”
  • Right-click on the “Panel1” grid, hit ‘Reset Layout/All”
  • Go to the right-hand pane, select “Properties” and expand the “Layout” pane if it’s collapsed.
  • Then click “Top” for Vertical Alignment.
  • Then enter “150” for height

Then proceed to add a text:

  • imageDrag a TextBlock on the “Panel1” grid. You can do this by either dragging it on the design surface on top of “Panel1”, or on the “Objects and Timeline” pane (also on top of “Panel1”)
  • Change the text from “TextBlock” to “Hello this is popup one”
  • Do Reset Layout/All on this text as well
  • In the Layout Pane, select Horizontal Alignment “Center” and Vertical Alignment “Top”

And finally add a button:

  • imageDrag a button on Panel1
  • Do Reset Layout/All on this button as well
  • Change the caption to “Done”
  • In the Layout Pane, select Horizontal Alignment “Center” and Vertical Alignment “Bottom”

And then finally do something that seems like pretty bonkers, but trust me: it will all become clear in the end:

  • imageSelect Panel1 in the Objects and Timeline panel.
  • Go to the Properties pane on the right hand side, and find the “Transform” pane. It’s usually collapsed. Expand it first.
  • Select Center Point tab - the 2nd tab from the right under “Rendertransform” (with the dot on in)
  • For both X and Y type “0.5”. This sets the transformation point dead in the middle of the panel
  • Then also select the Global offset tab – that’s the 2nd tab from the right under “Projection” (with the up and left pointing arrow on it)
  • imageEnter “500” for X.

Your design surface now should look like showed on the right. The panel is sitting well right of the phone. Bonkers, I said it. ;-).

Creating the second panel

Going to be a bit lazy here. I don’t want to to the whole sequence again

  • Select Panel 1 in “Objects and Timeline”
  • Hit CTRL-C, CTRL-V. This will result in a Panel1_Copy below Panel 1
  • imageRename that to “Panel2”
  • Go to the Properties tab again on the right, and enter “160” for top margin. This should result in Panel2 appearing under Panel1
  • Then, for an encore, go to the Transform panel again and change “500” for X Projection to -500

The second panel should jump to the left and imageresulting design surface should now look like this:

Creating the popup buttons

At this time I am going to assume you now understand the layout panel, so I am not going to make screenshots of every button you need to click and number you need to enter in the layout panels ;-)

  • Drag a StackPanel on the design surface, near the bottom of the screen.
  • Do Reset Layout/All,
  • Select Vertical Alignment Bottom, and enter a height of 220.
  • Proceed to drag three buttons on top of the StackPanel. These should appear under each other, taking the full width of the screen.
  • Change the captions of the buttons to (from top to bottom) “Popup 1”, “Popup 2” and “Done”.

The final design surface, including the objects tree, should look like this:

image

Defining the Visual States

We have three visual states:

  • None of the popups are displayed
  • Popup 1 is displayed
  • Popup 2 is displayed

To create these, proceed as follows:

  • imageAt the top left, click the “States” Tab.
  • Click the “Add state Group” Button
  • Rename “VisualStateGroup” to "PopupGroup”
  • Enter “0.5” for “Default Transition”. This indicates any state transitions will be automatically animated over a 0.5 second time period.

Next steps:

  • imageClick the “Add state” Button
  • Rename the state “VisualState” to “Normal”
  • Add two more states, “ShowPopupHello” and “ShowPopupBye”
  • Click the red dot before “ShowPopupBye”.  The red color disappears. The main design surface should now have a caption “ShowPopupBye state recording is off”

Now the next things are tricky, so pay close attention and make sure you do this exactly right.

  • Select “Panel1” in “Objects and Timeline”
  • Click state “ShowPopupHello”. The main design surface should now have a caption “ShowHelloPopupstate recording is on” and have a red border.
  • Go to the Transform panel again, select under projection the Global offset (2nd tab from the right) again and change 500 back to 0. Panel 1 should now appear in the phone screen
  • Now select state “ShowPopupBye”. Panel 1 disappears again
  • Select Panel2
  • Change its global offset to 0 as well. Now panel 2 appears in the phone screen
  • Select State “Normal”
  • Select the red dot before “Normal” to turn state recording off. Both panels now should be outside of the phone screen again.
  • Select “Base” on top of the State panel.

Bringing in the ViewModel

Before we are going to connect the Visual States to the ViewModel’s actions, let’s first bring it in. That’s pretty easy.

  • imageGo top right and select the data tab.
  • Select the “Create data source” button all to the right and select “Create object data source”
  • On about the 8th line you should see “DisplayViewModel”. Select that
  • Enter “DisplayViewModelDataSource” in the “Data source name” box
  • Hit OK.
  • Net result should be as displayed to the right.

Setting up initial data binding

This is so ridiculously easy in Blend it always makes me happy when I get to this state.

  • Drag “DisplayViewModel” from the data tab on top of the LayoutRoot panel in the Objects and Timeline panel
  • Drag “CloseCommand” on top of all three “Done” buttons. You can do that either on the design surface or on the Objects and Timeline panel, whatever you like.
  • Proceed to drag “PopupCommand” on top of both the “Popup 1” and “Popup 2” button.
  • Now select the “Popup 1” button, and select the “Properties” tab again.
  • On top there’s a “Search properties” box. The developers of Blend soon recognized the number of things you can set it so big you can easily loose track. Enter “Command” in that search box to limit the number of properties it shows.
  • The Properties box now should only show “Command” and “CommandParameter”. Enter value “ShowPopupHello” for “CommandParameter”
  • Now select the “Popup 2” button, and enter “ShowPopupBye” for “CommandParameter”
  • Clear the text “Command” from “Search Properties” so you can see all the properties again.

Programming by dragging stuff on top of each other. Ain’t life fun sometimes?

Furioso dragon-13-Enter the dragon: datatriggers for putting it all together

And now for the really scary part – the datatriggers. Just kidding of course – just more dragging stuff and filling in some fields. The odd thing is – from the Blend perspective, data triggers are hardly visible. We are using GotoStateActions. Finish the app by following these final steps:

  • Drag a GotoStateAction from Assets box on top of ContentPanel. If you can’t find it: type “goto” in the search box of the Asset panel. It will popup in the list to the right of the panel
  • Under Properties, Click on the “New” button next to “TriggerType” and select “DataTrigger” in the box that pops up.
  • Behind “Binding”, click the barrel like icon. A dialog pops up with the properties of your DisplayViewModel. Select “DisplayState” and hit OK
  • Enter “0” for value
  • imageFor StateName, select “Normal”
  • Drag another GotoStateAction from Assets box on top of ContentPanel. Make this a DataTrigger to, select the same property to bind to, but
    • Enter “1” for “Value”
    • Select “ShowPopupHello” for StateName
  • And finally, a third GotoStateAction with 2 as value and “ShowPopupBye” for StateName.

And that’s all. If you run your application (you should be able to hit F5 from Blend) you will get a simple app that scrolls Popup 1 from the left in half a second when you hit “Popup 1”, and scroll it back when you hit on of the done buttons. If you hit the “Popup 2” button when Popup 1 is visible, it will scroll the second popup into the screen while simultaneously scrolling the first on out of the screen.

The data triggers look like this in XAML:

<i:Interaction.Triggers>
  <ec:DataTrigger Binding="{Binding DisplayState}" Value="0">
    <ec:GoToStateAction StateName="Normal"/>
  </ec:DataTrigger>
  <ec:DataTrigger Binding="{Binding DisplayState}" Value="1">
    <ec:GoToStateAction StateName="ShowPopupHello"/>
  </ec:DataTrigger>
  <ec:DataTrigger Binding="{Binding DisplayState}" Value="2">
    <ec:GoToStateAction StateName="ShowPopupBye"/>
  </ec:DataTrigger>
</i:Interaction.Triggers>

Some things to note

  • We hardly did program anything at all, and what’s more – we did not even make animations or storyboards. By simply setting the default transition to 0.5 seconds and indicating where we want stuff to be once a state is reached, the Windows Phone framework automatically infers and creates an animation when changing state, moving the GUI elements from one place to another in half a second (in stead of flashing them from one place to another).
  • In the Command I was able to use names as defined in the enumeration because I did an Enum.Parse in the ViewModel, in the data triggers I had to use the real value (0,1,2) to be able to compare. Something that can be fixed using a converter, but I did not want to complicate things.
  • The fact that the visual state names have the same name as the enumeration values, does not bear any significance. I could have named them Huey, Dewey, and Louie for all I cared. Only the CommandParameter values need to be the same as the Enumeration string, and the DataTriggers need to have the same numeric value.

That’s it. Although the Visual Studio 2012 designer is light years ahead of the 2010 designer, Blend still rocks when defining a lot of stuff in XAML. Have fun making beautifully animated apps with this.

If you don’t feel like following all these steps yourself, find, as usual, the completed solution here.