Disclaimer: this is not a 101 article. It requires understanding of the basic idea about MVVM, data binding, the MVVMLight messenger, and the use of behavior in Windows 8 XAML.
Updated for RTM Bing Maps Control October 3 2012
Introduction
Let me get this straight: I don’t want you to wean off Windows Phone development – far from it. It’s value proposition is great, and it will become much greater still. This article is yet another way to show you how to carry over code and architecture principles between Microsoft’s great tile based operating systems. It’s all about re-using skills and code. C# and XAML code that is.
Those who have attended my talks about this subject during this year, have seen the application to the right popping up. Basically it generates shapes (be it points, lines or polygons) by data binding from ‘business objects’ - using MVVMLight view models. If you tap any of those shapes, a “SelectCommand” on the bound view model will be fired, and the view model will put itself on the MVVMLight Messenger. Some other view model will listen for those messages, and pop up the info window. The app shows gas stations (points), roadblocks (lines) and buildings (shapes). Don’t go look for the gas stations, they are not there and the fuel prices a way bit behind the (expensive) times, the roadblock are all but one fictional as well. Only the buildings are real – location wise that is. Sources of the original Windows Phone (7) application can be found here:
Windows Phone map binding recap
A quick recap: if you want to data bind to a Bing Maps control in Windows Phone, you will go about like this – first, you would define a data template for a layer:
<DataTemplate x:Key="RoadBlockViewModelTemplate"> <Microsoft_Phone_Controls_Maps:MapPolyline Locations="{Binding Geometry}" Stroke="#FF71FF00" StrokeThickness="5"> <i:Interaction.Triggers> <i:EventTrigger EventName="Tap"> <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SelectCommand}" /> </i:EventTrigger> </i:Interaction.Triggers> </Microsoft_Phone_Controls_Maps:MapPolyline> </DataTemplate>This would be able to make a line geometry from a viewmodel containing a “Geometry” attribute containing a LocationCollection object, and an ICommand “SelectCommand” that is executed when the user taps the line. Second, you would make a Bing Maps control, and define a layer like this.
<Microsoft_Phone_Controls_Maps:Map x:Name="map" CredentialsProvider="Your-credentials-here"> <Microsoft_Phone_Controls_Maps:MapLayer x:Name="MapLayer_RoadBlocks"> <Microsoft_Phone_Controls_Maps:MapItemsControl ItemsSource="{Binding RoadBlocks}" ItemTemplate="{StaticResource RoadBlockViewModelTemplate}"/> </Microsoft_Phone_Controls_Maps:MapLayer> </Microsoft_Phone_Controls_Maps:Map>;
The roadblock view model would look like this:
using System.Windows.Input; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using GalaSoft.MvvmLight.Messaging; using Microsoft.Phone.Controls.Maps; using MvvmMaps.Logic.Models.GeoObjects; using MvvmMaps.Logic.Models.Geometries; using Wp7nl.Utilities; namespace MvvmMaps.Logic.ViewModels { public class RoadBlockViewModel : ViewModelBase { public RoadBlock Model { get; set; } // Some code omitted
[DoNotSerialize] public LocationCollection Geometry { get { var modelGeom = Model.Location as LineGeometry; return modelGeom.GetLocationCollection(); } set { var modelGeom = Model.Location as LineGeometry; modelGeom.SetLocationCollection(value); } } [DoNotSerialize] public ICommand SelectCommand { get { return new RelayCommand( () => Messenger.Default.Send(this), () => true); } } } }
Basically it comes down to a Geometry view model property that converts a business object geometry to and from something the Bing Maps control understands – a LocationCollection. This is a named collection that contains objects of type GeoCoordinate – your basic Lat/Lon container. As said above, when you tap select the MVVMLight Messenger just sets off the selected viewmodel, and ‘something’ should capture that message and handle it.
Going to Windows 8 – challenges
Now let’s re-use our skills on Windows 8. Simply put – in its current state, the data binding support for the Bing Maps SDK for Windows Store apps is pretty easy to describe with one word – non-existent. Some developers immediately go into the ‘blame-and-flame-the-Microsoft-dev-team’ mode when they encounter things like this. I think ‘CodePlex library’. I always see things like this as an intellectual challenge, a chance to contribute to the community, and fortunately there are more people thinking that way. My very smart fellow Dutch developer community member Dave Smits has created BindableRTMaps, which is very useful for binding point objects – but its shape support is a bit limited. Being a GIS professional and an MVVMLight junkie, I want the control to be able to generate geographical elements directly from view models, just like I was able to do in Windows Phone. I solved the data binding issue for shapes using a Behavior based upon my WinRtBehaviors library. Not quite surprising for those who know me.
The result can be seen below:
In the center, next to the river, the Vicrea offices, where I work. Say cheese fellows! ;-) As you can see, the shapes are beautifully re-projected when Birds’ Eye View is selected. This is the stuff that makes a GIS buff tick.
Researching the control I quickly found the following:
- There’s no data binding support at all (as stated)
- The shapes the control draws cannot be templated. They are apparently just projections of something native (as is the map itself).
- There are only two kinds of shapes: MapPolygon and MapPolyline, both descending from MapShape, which in turn descends from DependencyObject – which is very fortunate, as I hope will become clear over the course of this article.
So I had the challenge to create something that can be put into XAML to still give the designer an amount of control how things appear, without having to resort to code.
Introducing MapShapeDrawBehavior
The behavior I created is called MapShapeDrawBehavior (I’ve never been one for original catchy names) and can be used like this:
<Maps:Map Credentials="Your-credentials-here"> <WinRtBehaviors:Interaction.Behaviors> <MapBinding:MapShapeDrawBehavior LayerName="Roadblocks" ItemsSource="{Binding RoadBlocks, Mode=TwoWay}" TapCommand="SelectCommand" PathPropertyName="Geometry" > <MapBinding:MapShapeDrawBehavior.ShapeDrawer> <MapBinding:MapPolylineDrawer Color="Green" Width="10"/> </MapBinding:MapShapeDrawBehavior.ShapeDrawer> </MapBinding:MapShapeDrawBehavior> </WinRtBehaviors:Interaction.Behaviors> </Maps:Map>
For every category of objects there’s a layer – which translates to one behavior per list of objects, in this case the road blocks (the green line on the app sceenshot above). Then you need to define three things per layer:
- What command in the item view model (in this case, a RoadBlockViewModel) must be fired when a MapShape’s only event – Tap – is called.
- Which property in the item view model contains the Path – this is the terminology for a MapShape’s collection of points. This is, once again, of type LocationCollection. Only that’s no longer a collection of GeoCoordinate but of Location.
- Finally, you need to define a drawer. A drawer is a concept I sucked from my own thumb – it determines how a collection of points is supposed to be transformed to something on the map. It’s my way to make something that’s not templatable more or less configurable.
I created three drawers out of the box: MapPolylineDrawer, MapPolygonDrawer, and MapStarDrawer. The last one draws a configurable star shaped polygon around a point – since map shapes cannot be points by themselves. A drawer needs only to implement one method:
public override MapShape CreateShape(object viewModel, LocationCollection path)
The basic drawers don’t do anything with the view model: they just take the settings from XAML. But if you want for instance your shapes having different colors based upon some view model property – say you want to color urban areas based upon their crime rate (what we GIS buffs call a thematic map) – you can write a little custom drawer.
If you just want to use the behavior you are done with reading. You can download the demo solution with code (which, incidentally, shows off a lot of more things than just binding to a map) and start playing around with it. Be aware of the following issues/caveats:
- You will need to install the Bing Maps SDK for Windows Store apps first
- When I moved the solution from my Big Black Box to my slate I had to delete and redo all references to Bing.Maps and “Bing Maps for C#, C++, or Visual Basic” (this was using the Beta, I don’t know if that still applies to the RTM version)
- The control apparently contains native code, so you cannot build it for Any CPU.
- The designer only works when you build for x86 (this still the case in RTM)
For the technically interested I will continue with some gory details.
The inner guts
The behavior itself is actually pretty big, so I won’t repeat all code verbatim; I will concentrate on the interesting parts.
First of all, I already mentioned the fact MapShape descends from DependencyObject. That spells ‘Ahoy, Attached Dependency Property ahead!’ to me. So I created two of those, one holding the name of the layer (I use those to find out which shapes belong to a single layer) and one in which I store the view model from which the shape was created:
using Windows.UI.Xaml; namespace Win8nl.MapBinding { public static class MapElementProperties { public static readonly DependencyProperty ViewModelProperty = DependencyProperty.RegisterAttached("ViewModel", typeof(object), typeof(MapElementProperties), new PropertyMetadata(default(object))); // Called when Property is retrieved public static object GetViewModel(DependencyObject obj) { return obj.GetValue(ViewModelProperty) as object; } // Called when Property is set public static void SetViewModel( DependencyObject obj, object value) { obj.SetValue(ViewModelProperty, value); } public static readonly DependencyProperty LayerNameProperty = DependencyProperty.RegisterAttached("LayerName", typeof(string), typeof(MapElementProperties), new PropertyMetadata(default(string))); // Called when Property is retrieved public static string GetLayerName(DependencyObject obj) { return obj.GetValue(LayerNameProperty) as string; } // Called when Property is set public static void SetLayerName( DependencyObject obj, string value) { obj.SetValue(LayerNameProperty, value); } } }
The core of the MapShapeDrawBehavior self consists out of just five little methods, and the VERY core method is CreateShape. The behavior iterates over the object list databound to ItemsSource, and calls CreateShape for every view model:
/// <summary> /// Creates a new shape /// </summary> /// <param name="viewModel"></param> /// <returns></returns> private MapShape CreateShape(object viewModel) { var path = GetPathValue(viewModel); if (path != null && path.Any()) { var newShape = CreateDrawable(viewModel, path); newShape.Tapped += ShapeTapped; MapElementProperties.SetViewModel(newShape, viewModel); MapElementProperties.SetLayerName(newShape, LayerName); // Listen to property changed event of geometry property to check // if the shape needs tobe redrawed var evt = viewModel.GetType().GetRuntimeEvent("PropertyChanged"); if (evt != null) { Observable .FromEventPattern<PropertyChangedEventArgs>(viewModel, "PropertyChanged") .Subscribe(se => { if (se.EventArgs.PropertyName == PathPropertyName) { ReplaceShape(se.Sender); } }); } return newShape; } return null; }
- First, it reads the view model property that holds the geometry (or at least, it tries that)
- It creates the actual shape
- It attaches an event listener to the “Tapped” event
- It puts the view model and the layer name in attached dependency properties for said shape
- Finally it attaches a property changed listener so that when the property that’s holding the view model’s geometry changes, the ReplaceShape method is called (which replaces the shape on the map – duh)
GetPathValue is a simple method that retrieves the view model’s geometry using reflection. Nothing special there:
private LocationCollection GetPathValue(object viewModel) { if (viewModel != null) { var dcType = viewModel.GetType(); var methodInfo = dcType.GetRuntimeMethod("get_" + PathPropertyName, new Type[0]); if (methodInfo != null) { return methodInfo.Invoke(viewModel, null) as LocationCollection; } } return null; }
CreateDrawable – well that’s VERY simple. Get the drawer and let it decide how the shape will look
protected virtual MapShape CreateDrawable(object viewModel, LocationCollection path ) { var newShape = ShapeDrawer.CreateShape(viewModel, path); return newShape; }
And finally ShapeTapped and FireViewModelCommand:
private void ShapeTapped(object sender, TappedRoutedEventArgs e) { var shape = sender as MapShape; if( shape != null ) { var viewModel = MapElementProperties.GetViewModel(shape); if( viewModel != null ) { FireViewmodelCommand(viewModel, TapCommand); } } } private void FireViewmodelCommand(object viewModel, string commandName) { if (viewModel != null && !string.IsNullOrWhiteSpace(commandName)) { var dcType = viewModel.GetType(); var commandGetter = dcType.GetRuntimeMethod("get_" + commandName, new Type[0]); if (commandGetter != null) { var command = commandGetter.Invoke(viewModel, null) as ICommand; if (command != null) { command.Execute(viewModel); } } } }
ShapeTapped checks if it the object sending the event is actually a shape, then tries to retrieve a view model from the attached dependency property, and calls FireViewModelCommand on it. Which basically is directly ripped from my earlier EventToCommandBehavior. And then the circle is round again – user taps, view model command is called (just as Laurent Bugnion’s EventToCommand trigger did for Windows Phone) and the view model takes it further just like before.
There’s more to this behavior, but mostly it’s just reacting to events that occur when the ObservableCollection ItemsSource changes.
Some concluding remarks
Of course this behavior was geared to make the code I already had as much reusable as possible, but I think the way WinRT XAML apps and Windows Phone apps can be put together are remarkably similar – provided you make good use of MVVM and keep your code as clean as possible. So what did I have to do to move over my business and view model code to get this working? Well not very much, actually.
- A tiny thing in my model library because I was so clever to use a BackgroundWorker somewhere in my models – which is not supported in WinRt
- The Gas station view model was changed to do the conversion form business object geometry to Bing Maps’ Location in stead of the converter I originally - because my solution does not support converters.
- I had to change some name spaces and data types. Mainly GeoCoordinate was now called Location.
- Oh yeah – in stead of "clr-namespace" I had to use "using" for declaring namespaces in XAML. I used ReSharper toalt-enter trough the errors and add the namespaces almost automatically.
And that was about it. Of course, the code in it was quite trivial, but still. On the XAML side things were a bit more complicated:
- Converters and Attached Dependency Properties were carried over with minimal changes.
- I had to trash my geometry templates and had to write the behavior to emulate the templates – in a way, which admittedly was no small feat. But that’s a hole that only needs to be plugged once, and can now act as a base for possible better solutions.
- I had to do some fiddling around with the DataTemplateSelector – that works a wee bit different, and will be subject of a future blog post.
- ‘Tombstoning’ works a bit differently, but quite analogous. Been there, done that, wrote the blogpost.
- The App Bar on Windows 8 has a lot more possibilities. And – thank Saint Sinofsky and his minions – it supports data binding out of the box. Moving from Windows Phone app bars to Windows 8 app bars is quite easy. Provided you used the BindableApplicationbar and MVVM of course ;-)
- I kinda 1:1 copied the data window (the popup with alphanumeric data that appears when you tap a shape) – that worked remarkably well, but you might want to do something about the styling for a real-world application. The data window is a wee bit small now and does fit in styling wise ;-)
This is still a work in progress, but I think for basic shape data binding this is already very usable. The Bing Maps control is very fast, courtesy of native code, no doubt. I hope this will help people.
Once again, for those who don’t feel scrolling all the way up: the source code. Also updated for RTM. Enjoy!
2 comments:
I will give it a try!
Though I already know, that I don't like the idea of having a UI dependency (type "Location") in my ViewModels.
Anyway - great work!! Thanks for sharing!
cheers from Austria,
Thomas
@Thomas, Josh Blake once called a view model "a converter on steroids". And I totally agree. An important point of a view model is to translate stuff from the business objects into something the GUI understands, and vice versa.
Post a Comment