Generic databinding for MapIcons in Universal Windows Apps
Preface
Ever since I introduced databinding for the UWP map control (and it’s previous incarnations for Windows Phone 8.x and Windows 8.x) I have been asked to write ‘real’ data binding for map shapes. I have patiently tried to explain that the very nature of the map control makes this impossible as the map shapes are not templated controls drawn by the XAML renderer, but drawn by the map control itself – and that is what makes it so fast. So I encouraged people to write their own MapShapeDrawer child classes that converted a view model into a map shape. How hard can it be, I thought. Judging by the number of requests I got, apparently it is hard, indeed, or too inflexible. So I decided to take a shot at creating a more or less generic class for converting view models to MapIcons – the most commonly used scenario. Thus GenericMapIconDrawer was created.
If the previous paragraphs made no sense at all to you, because this is the first time you ever have encountered my map binding library, I encourage you to read this article first.
Demo
I have once again updated the demo application that goes with the WpWinNl project. If you first hit Show Area, then hit “Flags” a number of time, you will see the map slowly getting covered with round flags of Belgium, Germany, Italy, Netherlands, Sweden and the UK. Don’t ask me why I choose this particular group of countries – I just did. If you then hit the “Pirate!” button, one of the nation’s flags will turn into Jolly Rogers, the descriptive label will change in “Arrrr!” and the icon will seem to jump upward a little. If you press the “Pirate!” button again, the current Jolly Roger flags will disappear, and another nation is selected to turn into pirates. Unless it selects a nation that has already turned pirate, then nothing will happen. If you press the “Pirate!” button long enough, all nations will turn into pirates and then disappear. Crime does not pay, in the end. At least in this demo. See video below.
The purpose of this – admittedly – rather peculiar demo, which I created when I was entirely sober indeed, is to show that by merely changing properties things change on the map. So even when it is not strictly data binding, it sure acts like data binding is happening. By the way, on my Surface Pro 4 you hardly see the flickering – it seems like my trusted old 2011 i7 970 machine that I used for recording this video is finally showing it’s age.
How the demo works (aka how you should use the new library feature)
There is actually way more code to the demo than the actual changes to the WpWinNl.Maps package comprise. First of all, the base class for my geometry providing view models has been changed so that it's name property is an actual view model property raising a NotifyPropertyChanged, using the standard MVVMLight syntax
public class GeometryProvider : ViewModelBase { private string _name; public string Name { get { return _name; } set { Set(() => Name, ref _name, value); } } }
Then there is the FlagList class, a child class of GeometryProvider, that provides a list of randomly located flags from a randomly selected nation within a rectangle defined by two Geopoints
using System; using System.Collections.Generic; using Windows.Devices.Geolocation; using Windows.Foundation; namespace WpWinNl.MapBindingDemo.Models { public class FlagList : GeometryProvider { public static readonly string[] Countries = { "Belgium", "Germany", "Italy", "Netherlands", "Sweden", "UK" }; private Uri _iconUri; public Uri Icon { get { return _iconUri; } set { Set(() => Icon, ref _iconUri, value); } } private BasicGeoposition _point; public BasicGeoposition Point { get { return _point; } set { Set(() => Point, ref _point, value); } } private Point _anchorPoint = new Point(0.5, 0.5); public Point AnchorPoint { get { return _anchorPoint; } set { Set(() => AnchorPoint, ref _anchorPoint, value); } } private bool _isVisible = true; public bool IsVisible { get { return _isVisible; } set { Set(() => IsVisible, ref _isVisible, value); } } public static IEnumerable<FlagList> GetRandomFlags( Geopoint point1, Geopoint point2, int nrOfPoints) { var flags = new List<FlagList>(); var points = PointList.GetRandomPoints(point1, point2, nrOfPoints); var r = new Random(DateTime.Now.Millisecond * 2); foreach (var point in points) { var flagIdx = (int)Math.Round(r.NextDouble() * 5); flags.Add(new FlagList { Name = Countries[flagIdx], Icon = new Uri($"ms-appx:///Assets/{Countries[flagIdx]}.png"), Point = point.Point }); } return flags; } } }
Notice here, as well, that all properties are raising INotifyPropertyChanged, that IsVisible is true by default and that we have a default icon anchor point of 0.5, 0.5 – that is, the center of the icon falls on the location specified by "Point".
On the MapBindingViewModel there is a new public property ObservableCollection<FlagList> Flags that only gets loaded up with initial data in the LoadFlags method
public void LoadFlags() { Flags.AddRange(FlagList.GetRandomFlags(new Geopoint(_area.NorthwestCorner), new Geopoint(_area.SoutheastCorner), 50)); }Which is called when you press the "Flags" button. It add 50 icons every time you press it. Then there is the method that changes a random flag into pirate flags
public void ChangeToPirate() { foreach (var flag in Flags.Where(p => p.Name == "Arrr!").ToList()) { flag.IsVisible = false; } var r = new Random(DateTime.Now.Millisecond * 2); var flagIdx = (int)Math.Round(r.NextDouble() * 5); var flagName = FlagList.Countries[flagIdx];) foreach (var flag in Flags.Where(p => p.Name == flagName).ToList()) { flag.Name = "Arrr!"; flag.Icon = new Uri("ms-appx:///Assets/JollyRoger.png"); flag.AnchorPoint = new Point(0.5, 1); } }
Any existing pirate flags are made invisible first, then new ones are created by setting the Name, the Icon and the AnchorPoint property. Thus the label changes, the icon, and the icon on the map seems to jump up about half it’s size as it’s anchor point is now center/bottom in stead of center/center (boy does that terminology bring back memories of my very first job)
In XAML, things are more or less still the same, with some additions:
<maps:MapControl x:Name="MyMap" Grid.Row="0" ZoomLevel="{x:Bind ViewModel.ZoomLevel, Mode=OneWay}" Center="{x:Bind ViewModel.Center, Mode=OneWay}"> <interactivity:Interaction.Behaviors> <mapbinding:MapShapeDrawBehavior LayerName="Flags" ItemsSource="{x:Bind ViewModel.Flags, Converter={StaticResource MapObjectsListConverter}}" PathPropertyName="Point"> <mapbinding:MapShapeDrawBehavior.EventToHandlerMappers> <mapbinding:EventToHandlerMapper EventName="MapElementClick" CommandName="SelectCommand" /> </mapbinding:MapShapeDrawBehavior.EventToHandlerMappers> <mapbinding:MapShapeDrawBehavior.ShapeDrawer> <mapbinding:GenericMapIconDrawer ImageUriPropertyName="Icon" AnchorPropertyName="AnchorPoint" TitlePropertyName="Name" IsVisiblePropertyName="IsVisible" CollisionBehaviorDesired="RemainVisible"/> </mapbinding:MapShapeDrawBehavior.ShapeDrawer> </mapbinding:MapShapeDrawBehavior> </interactivity:Interaction.Behaviors> </maps:MapControl
As drawer we have the new GenericMapIconDrawer that has a boatload of new properties, basically instructing the GenericMapIconDrawer from which view model properties to get the values that should be applied to the MapIcon it is creating. So, technically, this is still not data binding – the properties are pulled from the view model using reflection. Which means that you really should test this using .NET Native tooling to see if those properties are not pulled out by the compiler – or else suffer the pain I suffered when I tried to publish my app.
How the code works
The GenericMapIconDrawer is surprisingly simple. It's basic setup is like this: a few properties and a CreateShape method that actually creates the Icon from the viewmodel:
using Windows.Devices.Geolocation; using Windows.Foundation; using System; using System.Reflection; using Windows.Storage.Streams; using Windows.UI.Xaml.Controls.Maps; namespace WpWinNl.Maps { public class GenericMapIconDrawer : MapShapeDrawer { protected object ViewModel; protected MapIcon Icon; public string TitlePropertyName { get; set; } public string AnchorPropertyName { get; set; } public string ImageUriPropertyName { get; set; } public string IsVisiblePropertyName { get; set; } public MapElementCollisionBehavior CollisionBehaviorDesired { get; set; } public override MapElement CreateShape(object viewModel, BasicGeoposition pos) { ViewModel = viewModel; Icon = new MapIcon { Location = new Geopoint(pos), CollisionBehaviorDesired = CollisionBehaviorDesired, ZIndex = ZIndex }; SetPropertyValuesFromViewModel(); return Icon; } } }
The SetPropertyValuesFromViewModel is pull the additional four properties from the view model (the position is already being taken care of by the MapShapeDrawBehavior itself)
private void SetPropertyValuesFromViewModel() { string title = null; if (TryGetPropertyValue(ViewModel, TitlePropertyName, ref title)) { Icon.Title = title; } Point anchorPoint; if (TryGetPropertyValue(ViewModel, AnchorPropertyName, ref anchorPoint)) { Icon.NormalizedAnchorPoint = anchorPoint; } Uri imageUri = null; if (TryGetPropertyValue(ViewModel, ImageUriPropertyName, ref imageUri)) { Icon.Image = RandomAccessStreamReference.CreateFromUri(imageUri); } bool isVisble = true; if (TryGetPropertyValue(ViewModel, IsVisiblePropertyName, ref isVisble)) { Icon.Visible = isVisble; } }
And because I am a lazy b*****d I wrote a little helper method do to the repetitive heavy lifting for that
private static bool TryGetPropertyValue<T>(object obj, string propertyName, ref T outValue) { if (!string.IsNullOrWhiteSpace(propertyName)) { var prop = obj.GetType().GetRuntimeProperty(propertyName); var result = prop?.GetValue(obj); if (result is T) { outValue = (T) prop.GetValue(obj); return true; } } return false; }
Note, however, that only the position, label text, icon uri, anchor point and visibility are pulled from the view model. Z-index and collisionbehavior are not. Deep down in the MapShapeDrawBehavior, in the CreateShape method, there is another change that I want to draw your attention to:
var evt = viewModel.GetType().GetRuntimeEvent("PropertyChanged"); if (evt != null) { var observable = Observable.FromEventPattern<PropertyChangedEventArgs>( viewModel, "PropertyChanged") .Subscribe(se => { if (!LegacyMode || se.EventArgs.PropertyName == PathPropertyName) { ReplaceShape(se.Sender); } }); TrackObservable(viewModel, observable); }
Previously, the shape would only be replaced if the geometry changed. Now, unless the new property LegacyMode is set to true, this will happen at every PropertyChanged event. If you look carefully at the video, you will actually see the Jolly Rogers flickering, which is correct – since three properties are changed (Name, Icon and AnchorPoint) each flag is redrawn three times. This is quite inefficient, but unfortunately the way it works. You cannot change an Icon, only replace it. So for every property change it actually gets replaced indeed, and to that extent I also had to make a little change to ReplaceShape itself.
private void ReplaceShape(object viewModel) { var shape = AssociatedObject.MapElements.FirstOrDefault(p => p.ReadData() == viewModel); if (shape != null) { var shapeLocation = AssociatedObject.MapElements.IndexOf(shape); if (shapeLocation != -1) { var newShape = CreateShape(viewModel); if (newShape != null) { // Previous code // AssociatedObject.MapElements[shapeLocation] = CreateShape(viewModel); AssociatedObject.MapElements.RemoveAt(shapeLocation); AssociatedObject.MapElements.Insert(shapeLocation, newShape); } } } else { AddNewShape(viewModel); } }
So this experiment did not only bring new (or at least easier to use) functionality – it also instilled a bug fix. Of course, you can work around the repeated drawing/flickering by making a view model that does not fire PropertyChanged on every property change, but handle this manually when you are done. But that kind of performance tweaking is outside of the scope of this article.
Concluding remarks
Data binding shapes in the classical way still is not possible, so I had to resort to something that acts like it. I hope this makes using this package for mapping a bit easier. Be advised that for massive changes to large datasets this may not be the most efficient way to get things done, but for your average project it makes things way easier.
It’s now downloadable from NuGet as version 3.0.6, and you can find the sources of the demo app here.