07 July 2010

Using MVVM Light to drive a Windows Phone 7 / Silverlight 4 map viewer

Updated for Windows Phone 7 beta tools!

Traditionally, the first application any developer tries out on a new platform is "Hello World". Working with spatial data daily, I tend to take this a wee bit more literal than most people. So when I wanted to try out MVVM and Windows Phone 7 at the same time, I went for the less-than-trivial task of making a generic map viewer from scratch, based upon the MultiScaleImage which – fortunately – is available on Windows Phone 7 as well. For an encore, to prove the point of MVVM, this viewer’s model should be usable by an ‘ordinary’ Silverlight application as well. My intention was to make a case study for MVVM in a less-than-ordinary environment, but I ended up with a more or less fully functional – although pretty limited – application.

Prerequisites

This example uses:
  1. Visual Studio 2010 Professional
  2. Windows Phone 7 Developer Tools Beta
  3. Silverlight 4 SDK
  4. Silverlight 4 tools for Visual Studio 2010
  5. The MVVM Light framework by Laurent Bugnion, both for Windows Phone 7 and for Silverlight 4. The parts you need are already include in the sample.

On my machine I also have the Silverlight 4 Toolkit April 2010 installed as well but I don’t think that is necessary for this sample.

Download, compile and run the source code

Solution structurSince the application itself contains a bit more code than my usual samples (as well as a bucketload of XAML), I cannot write out all source code in the sample. In order be able to follow what is going on, you will need to download the code first, see if it runs and compiles on your machine, and then read the rest of this article. You will end up with a solution that, with the proper parts expanded and collapsed, will look like the solution as displayed to the right. In the Binary Dependencies solutions folder you will see Laurent’s stuff (Galasoft.*), both for Windows Phone 7 and Silverlight 4, as well as System.Windows.Interactivity, which comes from the Blend SDK. The bottom to projects, SLViewer.Web and WP7Viewer, are the actual startup projects. If you fire up one , you will get one of these screens below:

 MobileMapLoader Silverlight screen Now that’s what I call “Hello World”!

Using the applications

Double-tapping (or clicking) anywhere on the map on both applications zooms in on the location of the double-tap. If you (single) tap/click on the text “Double tap zooms in” it will toggle with “Double tap zooms out” and then double tap… you guessed it.
In addition, you can move the map around by dragging it – either with your finger or the mouse. By tapping/clicking on the map name (initially it will show the Open Source Map OsmaRender) you can change the map you are viewing: OSM OsmaRender, OSM Mapnik, Google Maps Street, Satellite, and Hybrid, and Bing Maps Road and Aerial. The Silverlight application just responds to a click and moves to the next map – the Windows Phone 7 more or less supports swipes and you can go to the next or the previous map my moving your finger from right to left or left to right over the map title. At least, it does so when I use the mouse, lacking a proper test device it is of course impossible to test how it behaves in the real world.

Solution structure

From top to bottom you will see:

  • LocalJoost.Models, this is a Silverlight 4 class library without any content, only links to source files in LocalJoost.Model.WP7
  • LocalJoost.Model.WP7, a Windows Phone 7 class library, this contains the actual model and additional items as well as the definition of attached dependency properties needed to drive the map,
  • LocalJoost.TileSource, again a codeless Silverlight class library, with links only to
  • LocalJoost.TileSource.WP7, a Windows Phone 7 class library, containing classes that translate requests from the MultiScaleImage to the image structure for various Tile Map Servers
  • SLViewer, the Silverlight 4 application
  • SLViewer.Web, the web application hosting SLViewer
  • WP7Viewer, the Windows Phone 7 application

Since LocalJoost.Models and LocalJoost.TileSource only contain links to source files in the Windows Phone 7 counterparts, both class libraries have identical code.

Tile sources

MultiScaleImage features a property Source of type MultiScaleTileSource. This class sports an abstract method GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, IList<object>tileImageLayerSources) that you can override. Each descendant essentially tells the MultiScaleImage: if you are on zoomlevel tileLevel, and need to have the Xth horizontal tile and the Yth vertical tile, then I will add an URI to the tileImageLayerSources where you can find it. So all classes in the LocalJoost.TileSource.WP7 are simply translations from what the MultiScaleImage asks into URIs of actual map images on various map servers on the internet. Nice if you are into it (like me), but not essential to understand the workings of MVVM as a driver for this application

Binding properties and commands

The key thing about MVVM is that nothing, or at least very little, ends up in what ASP.NET WebForms veterans like me tend to call the ‘codebehind file’ of the XAML. The idea is that you make your controls in XAML, and let them communicate by binding to properties and commands of the model. You click on a button or whatever, the button sends a command to the model, the model responds by changing some properties, and via the magic of INotifyPropertyChanged they automatically end up being displayed on the user interface.

Problem 1: In Silverlight you only have events, and you cannot bind events to a model. You need commands
Problem 2: The MultiScaleImage control is mostly driven by methods, to which you cannot bind, and for some reason the Source property does not want to bind to MultiScaleTileSource either (I get an error “Object does not match target type”).

The solution for problem 1 is MVVM Light. The solution for problem 2 are attached dependency properties.

Attached dependency properties for driving the MultiScaleImage

To get this application to work, there are quite some dependency properties necessary. They are all in the class BindingHelpers in LocalJoost.Models.WP7. From top to bottom you will see:

  • MapTileSource: used to bind a MultiScaleTileSource to the Source property of a MultiScaleImage
  • ZoomEventPoint: used to bind the result of an event (the location where the user tapped on the MultiScaleImage and the zoom factor) to the MultiScaleImage.
  • StartDragEventData: when the user starts dragging the map, the event starting of the dragging is stored in here. Doubles as state setting: if this property is not set, the user is not dragging. The callback method of this property also stores data in the next property:
  • DragData: stores the position and offset where the user started the dragging. This property is for storage only and you will not see it bound to anything in XAML.
  • MoveEventData: stores the event when the user actually moves the map – in conjunction with the data stored in DragData the new map origin is calculated, and the ViewportOrigin of the MultiScaleImage gets a kick to the right place.

Using MVVM Light to kick the model

If you look at the XAML of MainPage.xaml of WP7Viewer, there are a few things you should take notice of:

<phone:PhoneApplicationPage 
  x:Class="WP7Viewer.MainPage"
  <!--- omitted stuff --->
  xmlns:MapMvvm="clr-namespace:LocalJoost.Models;assembly=LocalJoost.Models"
  xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
  xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP7">

This way, I add my own assembly, with the model and the dependency property types, to be used in XAML with prefix “MapMvvm”. I also add System.Windows.Interactivity to be used with prefix “i” and MVVMLight with prefix “cmd”. And now the place where it all happens:

<MultiScaleImage x:Name="msi" 
   ViewportOrigin ="{Binding ViewportOrigin}"
   MapMvvm:BindingHelpers.MapTileSource="{Binding CurrentTileSource.TileSource}" 
   MapMvvm:BindingHelpers.ZoomEventPoint="{Binding ZoomEventPoint}"
   MapMvvm:BindingHelpers.StartDragEventData="{Binding StartMoveEvent}"
   MapMvvm:BindingHelpers.MoveEventData="{Binding EndMoveEvent}">

 <i:Interaction.Triggers>
   <i:EventTrigger EventName="MouseLeftButtonDown">
     <cmd:EventToCommand Command="{Binding MouseLeftButtonDown}"
      PassEventArgsToCommand="True" />
   </i:EventTrigger>
   <i:EventTrigger EventName="MouseLeftButtonUp">
     <cmd:EventToCommand Command="{Binding MouseLeftButtonUp}"
      PassEventArgsToCommand="True" />
    </i:EventTrigger>
    <i:EventTrigger EventName="MouseMove">
      <cmd:EventToCommand Command="{Binding MouseMove}"
       PassEventArgsToCommand="True" />
     </i:EventTrigger>
   </i:Interaction.Triggers>
</MultiScaleImage>

In the top part, I bind my MapTileSource, ZoomEventPoint, StartDragEventData and MoveEventData attached dependency properties of the MultiScaleImage to properties in the model, enabling the model to communicate stuff to the View – of which the MultiScaleImage is part -without having to resort to calling actual methods of the MultiScaleImage.

Now to enable the View to communicate back to the model – remember I wrote earlier that you have only events in Silverlight, no proper commands? Look at the bottom part where Laurent’s magic comes into play: I capture the event using an EventTrigger from System.Windows.Interactivity, and by using the EventToCommand from MVVM Light I can bind that to an actual command in the model – and optionally pass the parameters to the model as well. For instance, the event MouseLeftButtonDown is passed to the command MouseLeftButtonDown in MapViewModel:

public ICommand MouseLeftButtonDown
{
  get
  {
    return new RelayCommand<MouseEventArgs>(e =>
    {
      var t = DateTime.Now - _lastMouseDown;
      if (t < _timeSpanForDoubleClick)
      {
        // assume double click - Zoom
        ZoomEventPoint = 
          new ZoomEventPoint 
           { Event = e, ZoomFactor = ZoomAction.ZoomFactor };
      }
      else
      {
        // Assume start of dragging 
        StartMoveEvent = e;
      }
      _lastMouseDown = DateTime.Now;
    });
  }
}

Thus the model can communicate with the view via properties (albeit attached dependency properties) and the view can notify the model to do something by binding to its commands. The circle is closed and nothing needs to be in your ‘code behind’ file anymore. Well, almost nothing.

Instantiating the model

A minor detail: before I can bind a view to a model, there must be a model to bind to. I instantiate the model in the ‘classic’ MVVM way, via XAML only. In the phone:PhoneApplicationPage.Resources I instantiate the model like this:

<phone:PhoneApplicationPage.Resources>
  <Storyboard x:Name="LayoutrootStartup">
  <!-- ommitted stuff -->
  </Storyboard>
  <MapMvvm:MapViewModel x:Key="MapViewerModel" />
</phone:PhoneApplicationPage.Resources>
The model constructor is fired, and the list of maps (_allTileSources) and zoomactions (_zoomActions) are initialized. Then I set the model as data context for the layout root:
<Grid x:Name="LayoutRoot" 
  Background="Transparent" 
    DataContext="{StaticResource MapViewerModel}">
  <!-- ommitted stuff-->
</Grid>
and the application is off to go. There are other way of doing this: I watched Dennis Vroegop once writing code instantiating the model in the constructor in the ‘code behind file’ of the user control and then setting the data context by code - this enables passing the Dispatcher as a parameter which is necessary if you bind stuff coming from another thread. Dennis himself, by the way, is currently in the process of writing an excellent introduction to MVVM – the first two parts (in Dutch) you can find here and here.

Implementing the ‘swipe’

As I wrote earlier, in the Windows Phone 7 application it is possible to select the next or the previous map by ‘swiping’ over the map title. There are probably better ways to do this, but I did it as follows: first, I trap the ManipulationCompleted event and pass it to the model using the same trick as before:

<TextBlock x:Name="PageTitle"
  Text="{Binding CurrentTileSource.MapName}" 
  Margin="-3,-8,0,0"
  Style="{StaticResource PhoneTextTitle1Style">
   <i:Interaction.Triggers>
    <i:EventTrigger EventName="ManipulationCompleted" >
      <cmd:EventToCommand Command="{Binding SetNextMapCommandWp7}" 
        PassEventArgsToCommand="True"/>                            
    </i:EventTrigger>
  </i:Interaction.Triggers>
</TextBlock>
and then I go on processing it like this:
/// <summary>
/// Called by the Windows Phone client
/// when the user swipes over the map tile
/// </summary>
public ICommand SetNextMapCommandWp7
{
  get
  {
    return 
      new RelayCommand<ManipulationCompletedEventArgs>(SetNextMapWp7);
  }
}

/// <summary>
/// Determine to select previous or next map
/// </summary>
/// <param name="e"></param>
public void SetNextMapWp7(ManipulationCompletedEventArgs e)
{
  if (e.TotalManipulation.Translation.X > 0)
  {
    SetPreviousMap();
  }
  else
  {
    SetNextMap();
  }
}

/// <summary>
/// Move to the next map
/// </summary>
public void SetNextMap()
{
 var currentNode = _allTileSources.Find(CurrentTileSource);
 if (currentNode != null)
 {
  CurrentTileSource =
   currentNode.Next != null ? 
    currentNode.Next.Value : _allTileSources.First.Value;
 }
}

/// <summary>
/// Move to the previous map
/// </summary>
public void SetPreviousMap()
{
 var currentNode = _allTileSources.Find(CurrentTileSource);
 if (currentNode != null)
 {
  CurrentTileSource =
   currentNode.Previous != null ? 
     currentNode.Previous.Value : _allTileSources.Last.Value;
 }
}

/// <summary>
/// Command called by Silverlight 4 client
/// when user clicks on map title
/// </summary>
public ICommand SetNextMapCommand
{
  get
  {
    return new RelayCommand(SetNextMap);
  }
}

by checking the TotalManipulation.Translation.X property of the ManipulationCompletedEvent I can calculate if the user swiped from left to right or from right to left, thus showing the next or the previous map type. The types themselves, by the way, are stored in a LinkedList – that’s the first time I found use for that one – but that’s is not important for the way this works. Notice the last method – the simple SetNextCommand method – that is called by the Silverlight 4 client, where it traps a simple MouseLeftButtonDown event, which allows you only to select the next map – not the previous. ManipulationCompletedEvent seems not to be available in Silverlight 4. This is the only allowance I had to make to get the application to work from ‘plain’ Silverlight 4 as well.

Conclusion

This application is maybe somewhat unusual and still very limited - I don’t start any story board from the model showing fancy animations (which I would like to when the user swipes over the map title to select another map, but I have not found out yet how to do that). Still, I think it’s safe to say MVVM Light used in conjunction with attached dependency properties makes it possible to bind virtually anything, even a MultiScaleImage, to a model. In addition, it’s possible to reuse a lot – if not all – model code by linking source files form Windows Phone 7 libraries to Silverlight 4 libraries and vice versa which makes making applications that run both on Windows Phone 7 and Silverlight 4 (and in the near future, maybe even on Symbian) quite feasible. All in all, from an architectural standpoint, MVVM Light and attached dependency properties are a real winning team.

Credits, disclaimers and legal stuff

The algorithms for loading tiles from the Google servers and OSM are a slight adaptation from stuff to be found in the DeepEarth codebase on CodePlex, while the Bing algorithm actually comes from Microsoft itself. This code loads maps directly from Google Maps and Bing Maps servers bypassing any kind of licenses, TOSses and APIs, and is in this sense probably illegal. This is done for demonstration purposes only, to show how seamlessly the MultiScaleImage works together with multiple map servers. I do by no means encourage or endorse this code to be used other than in demo settings only, and will take no responsibility for any consequences if you choose to act otherwise. So beware. If you use the Google and Bing code in production settings, don’t come knocking on my door after the map owners have come around to kick in yours. :-P

I nicked the algorithm for dragging the map and calculating new positions somewhere from the internet, but for the life I cannot remember where, and since I heavily refactored it for use in dependency properties Google Bing cannot find it back either. If anyone recognizes his/her code and feels I shortchanged him/her in the credits, please let me know and I will correct ASAP.

2 comments:

nkstr said...

Hi Joost,

it's with great pleasure I read your impressive blog, thanks for sharing.
I'm trying to implement a WP-mapviewer with LOCAL content, as the app must be available offline. Any thoughts on that? I get the feeling the MultiScaleImage element will talk only to a web server, am I wrong?

Keep it up!

Joost van Schaik said...

@nkstr I know its at least possible to get the Bing Maps control to do that. I know a fellow who did it, but he won't share :(