Universal Apps are a pretty hot topic in the world of Windows app development. Universal apps allow you to build an app for Windows 8.1 and Windows Phone 8.1 while, at the same time allowing you to share code, user controls, styles, strings, and other assets between the two projects in Visual Studio. This saves time having to develop code for each platform. As great as this sounds there are a few caveats to be aware of. The main caveat is that not all controls available in Windows Store apps are available to Windows Phone apps and vice-versa. Also, at the time of writing this blog the Universal app template in Visual Studio is not available for Visual Basic apps. You can find some useful documentation on creating universal apps on MSDN here.
Recently on the Bing Maps forums, there have been a lot of questions around maps and universal apps. The primary concern being that many users want to create a XAML control in the shared part of the universal app and have it include a map. The main road block that developers are running into is that the Bing Maps SDK for Windows Store apps uses the namespace Bing.Maps, while the maps in the Windows Phone SDK use the namespace Windows.UI.Xaml.Controls.Maps. To make things a bit more complicated the map control in the Bing Maps SDK for Windows Store apps is called Map while in the Windows Phone SDK it’s called MapControl. This makes it pretty much impossible to reference these map controls in a single shared XAML control. But not all is lost, there is a way to get around this limitation. In this blog post we are going to take a look at how to make use of maps inside the shared project of a universal app.
If the mapping functionality you are adding to your app is fairly basic, you may want to simply launch the built-in maps app on the device rather than creating a custom mapping solution. If this is the case, take a look at our blog post outlining how to launch the maps app from a universal app.
Getting Started
To get started, open Visual Studio and create a new Universal App project in C#. Select the Blank App template, call the application SharedMapSample, and then press OK.
When the solution loads you will see three projects. The first two will be Windows and Windows Phone projects. The third project is a shared project used by the first two projects.
To keep things simple we are going to share the MainPage control. To do this drag and drop the MainPage.xaml and MainPage.xaml.cs files from either of the first two projects into the shared project. Next delete the references to these files in the first two projects. By doing this both apps will use the same MainPage control. You solution should now look like this:
Next, add a reference to the Bing Maps SDK to the Windows Store app. Right click on the References folder in the Windows Store app and press Add Reference. Select Windows → Extensions, and then select Bing Maps for C#, C++ and Visual Basic. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps . While you are here, also add a reference to the Microsoft Visual C++ Runtime Package, as this is required by the Bing Maps SDK when developing using C# or Visual Basic. Press OK.
In Solution Explorer, set the Active solution platform of the Windows Store app in Visual Studio by right clicking on the Solution folder and selecting Properties. Select Configuration Properties → Configuration. Find the Windows Store project and under the Platform column, set the target platform to x86, and press OK.
Using Conditional Compilation Symbols
One really powerful feature we have in the shared project of a universal app is being able to access libraries that are referenced by the Windows Store and Windows Phone app. If both projects reference libraries that have the same named class in the same namespace, you will be able to use this in the shared app without having to do anything special. However, if the libraries are different, as is the case when using the Bing Maps SDK in the Windows Store app and the built in maps in the Windows Phone SDK, then the classes will not work in both projects. To get around this we can use conditional compilation symbols in our code to separate and handle the differences between the libraries. Conditional compilation symbols are “if” statements that are interpreted by the compiler in Visual Studios at compile time. These symbols allow us to specify which blocks of code to compile based on which symbols are defined in the project. One of the most common conditional compilation symbols used is DEBUG, which allows us to specify a block of code that will be compiled when Visual Studios is in debug mode. Here is an example of how these symbols can be used:
#if DEBUG //Code block to use when in debug mode. #else //Code block to use when not in debug mode. #endif
In universal apps there are two conditional compilation symbols already defined for us to make things easy, WINDOWS_APP and WINDOWS_PHONE_APP. If we wanted we could define additional compilation symbols in the Build section of the project properties, but WINDOWS_APP and WINDOWS_PHONE APP are what’s needed for this app.
Before we jump into creating the map functionality, we are going to create some extensions to help make things easier. In the Bing Maps for Windows Store SDK coordinates are represented using a class called Location. In the Windows Phone SDK coordinates are represented using the BasicGeoposition and Geopoint classes and are in the Windows.Devices.Geolocation namespace which is available to both Windows and Windows Phone. As such, it makes sense to create a few extensions to easily convert between these classes in our Windows app. There are a couple of ways to do this, we could add the extensions to the Windows app, or we could add it to the shared project and use conditional compilation symbols. Since we may want to add other extensions to the app in the future, we will add an extension class to the shared project. To do this, right click on the shared project and select Add → New Item and create a new class file called Extensions.cs. Update this file with the following code:
using Windows.Devices.Geolocation; using System.Collections.Generic; #if WINDOWS_APP using Bing.Maps; #endif namespace SharedMapSample { public static class Extensions { #if WINDOWS_APP public static LocationCollection ToLocationCollection(this IList<BasicGeoposition> pointList) { var locs = new LocationCollection(); foreach (var p in pointList) { locs.Add(p.ToLocation()); } return locs; } public static Geopoint ToGeopoint(this Location location) { return new Geopoint(new BasicGeoposition() { Latitude = location.Latitude, Longitude = location.Longitude }); } public static Location ToLocation(this Geopoint location) { return new Location(location.Position.Latitude, location.Position.Longitude); } public static Location ToLocation(this BasicGeoposition location) { return new Location(location.Latitude, location.Longitude); } #elif WINDOWS_PHONE_APP //Add any required Windows Phone Extensions #endif } }
Wrapping the Map Controls
Conditional compilation symbols are great for working around platform specific dependencies in blocks of code, but this doesn’t address our main issue of using the map control in XAML. Since the map controls for Windows and Windows Phone have different names and namespaces, we simply can’t use them in a shared XAML file. To get around this, we can create a XAML control that wraps both map controls. If we create a XAML control class that inherits from a common control that is available Windows and Windows Phone, such as the Grid control, we can then use conditional compilation symbols in our code to add the respective map control as a child of our XAML control. To do this, create a new class in the shared project called MapView. When this class is created we will use conditional compilation symbols to create an instance of the appropriate map control and add it as a child of our class. Update the MapView.cs file with the following code.
using System.ComponentModel; using Windows.UI.Xaml.Controls; using Windows.Devices.Geolocation; using System.Collections.Generic; using Windows.UI; #if WINDOWS_PHONE_APP using Windows.UI.Xaml.Controls.Maps; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; using Windows.UI.Xaml; #elif WINDOWS_APP using Bing.Maps; #endif namespace SharedMapSample { public class MapView : Grid { #if WINDOWS_APP private Map _map; #elif WINDOWS_PHONE_APP private MapControl _map; #endif public MapView() { #if WINDOWS_APP _map = new Map(); #elif WINDOWS_PHONE_APP _map = new MapControl(); #endif this.Children.Add(_map); } } }
To make use of this class, open the MainPage.xaml file and update it with the following XAML.
<Page x:Class="SharedMapSample.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SharedMapSample" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> <Grid> <local:MapView x:Name="MyMap"/> </Grid> </Page>
If you run either app a full screen map should load up. This is a good starting point, but we want to do more than just load a map. We need to be able do things like set the center and zoom level of the map, turn on the traffic layer. There are a lot of little differences between the two map controls when it comes to doing these things. We can wrap these functionalities and use conditional compilation symbols to help us out. While we are at it we will make the MapView class inherit from the INotifyPropertyChanged class so we can make some of the properties bindable via XAML. We will also create a method for setting the map view as well. To do all this update the MapView class with the following code.
using System.ComponentModel; using Windows.UI.Xaml.Controls; using Windows.Devices.Geolocation; using System.Collections.Generic; using Windows.UI; #if WINDOWS_PHONE_APP using Windows.UI.Xaml.Controls.Maps; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; using Windows.UI.Xaml; #elif WINDOWS_APP using Bing.Maps; #endif namespace SharedMapSample { public class MapView : Grid, INotifyPropertyChanged { #if WINDOWS_APP private Map _map; #elif WINDOWS_PHONE_APP private MapControl _map; #endif public MapView() { #if WINDOWS_APP _map = new Map(); #elif WINDOWS_PHONE_APP _map = new MapControl(); #endif this.Children.Add(_map); } public double Zoom { get { return _map.ZoomLevel; } set { _map.ZoomLevel = value; OnPropertyChanged("Zoom"); } } public Geopoint Center { get { #if WINDOWS_APP return _map.Center.ToGeopoint(); #elif WINDOWS_PHONE_APP return _map.Center; #endif } set { #if WINDOWS_APP _map.Center = value.ToLocation(); #elif WINDOWS_PHONE_APP _map.Center = value; #endif OnPropertyChanged("Center"); } } public string Credentials { get { #if WINDOWS_APP return _map.Credentials; #elif WINDOWS_PHONE_APP return string.Empty; #endif } set { #if WINDOWS_APP if (!string.IsNullOrEmpty(value)) { _map.Credentials = value; } #endif OnPropertyChanged("Credentials"); } } public string MapServiceToken { get { #if WINDOWS_APP return string.Empty; #elif WINDOWS_PHONE_APP return _map.MapServiceToken; #endif } set { #if WINDOWS_PHONE_APP if (!string.IsNullOrEmpty(value)) { _map.MapServiceToken = value; } #endif OnPropertyChanged("MapServiceToken"); } } public bool ShowTraffic { get { #if WINDOWS_APP return _map.ShowTraffic; #elif WINDOWS_PHONE_APP return _map.TrafficFlowVisible; #endif } set { #if WINDOWS_APP _map.ShowTraffic = value; #elif WINDOWS_PHONE_APP _map.TrafficFlowVisible = value; #endif OnPropertyChanged("ShowTraffic"); } } public void SetView(BasicGeoposition center, double zoom) { #if WINDOWS_APP _map.SetView(center.ToLocation(), zoom); OnPropertyChanged("Center"); OnPropertyChanged("Zoom"); #elif WINDOWS_PHONE_APP _map.Center = new Geopoint(center); _map.ZoomLevel = zoom; #endif } public event PropertyChangedEventHandler PropertyChanged; internal void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); } } } }
We can now set the zoom level and show or hide the traffic layer of the map right in XAML like so.
<local:MapView x:Name="MyMap" Zoom="10" ShowTraffic="True"/>
Adding Pushpins and Shapes to the Map
A map without content can be pretty boring. There are several different types of content that can be added to the map, the most common are pushpins, polylines, and polygons. In the Bing Maps Windows Store SDK there is a Pushpin class which we can use to create our pushpins, but for the maps in the Windows Phone SDK we have to create a UIElement to use as a pushpin. To keep things simple, we will create a method for adding pushpins to the map that will take in a location and some text. The text will be overlaid on top of the pushpin. This is useful for numbering pushpins so that you can visually link them to content that may be displayed outside of the map. Add the following method to the MapView class.
In the MapView class add a private MapLayer property called _pinLayer and add it to the map in the constructor of the MapView class like so.
#if WINDOWS_APP private Map _map; private MapLayer _pinLayer; #elif WINDOWS_PHONE_APP private MapControl _map; #endif public MapView() { #if WINDOWS_APP _map = new Map(); _pinLayer = new MapLayer(); _map.Children.Add(_pinLayer); #elif WINDOWS_PHONE_APP _map = new MapControl(); #endif this.Children.Add(_map); }
Next add the following methods to the MapView class for pushpins to the map.
public void AddPushpin(BasicGeoposition location, string text) { #if WINDOWS_APP var pin = new Pushpin() { Text = text }; MapLayer.SetPosition(pin, location.ToLocation()); _pinLayer.Children.Add(pin); #elif WINDOWS_PHONE_APP var pin = new Grid() { Width = 24, Height = 24, Margin = new Windows.UI.Xaml.Thickness(-12) }; pin.Children.Add(new Ellipse() { Fill = new SolidColorBrush(Colors.DodgerBlue), Stroke = new SolidColorBrush(Colors.White), StrokeThickness = 3, Width = 24, Height = 24 }); pin.Children.Add(new TextBlock() { Text = text, FontSize = 12, Foreground = new SolidColorBrush(Colors.White), HorizontalAlignment = Windows.UI.Xaml.HorizontalAlignment.Center, VerticalAlignment = Windows.UI.Xaml.VerticalAlignment.Center }); MapControl.SetLocation(pin, new Geopoint(location)); _map.Children.Add(pin); #endif }
Polylines and Polygons are very similar in both map controls. However, there are some slight differences to be aware of. The Bing Maps SDK uses the Color class to define the Fill and Stroke properties of shapes, whereas the maps in Windows Phone SDK use brushes. Also, the Bing Maps SDK does not provide a stroke property for polygons. One of the biggest differences when it comes to polylines and polygons in the maps is how they are added to the map. In Bing Maps we have to add a MapShapeLayer to the ShapeLayers property of the map and then add our shapes to the new MapShapeLayer. For the maps in the Windows Phone SDK we add our shapes to the MapElements property of the map. In the MapView class add a private MapShapeLayer property called _shapeLayer and add it to the map in the constructor of the MapView class like so.
#if WINDOWS_APP private Map _map; private MapShapeLayer _shapeLayer; private MapLayer _pinLayer; #elif WINDOWS_PHONE_APP private MapControl _map; #endif public MapView() { #if WINDOWS_APP _map = new Map(); _shapeLayer = new MapShapeLayer(); _pinLayer = new MapLayer(); _map.ShapeLayers.Add(_shapeLayer); _map.Children.Add(_pinLayer); #elif WINDOWS_PHONE_APP _map = new MapControl(); #endif this.Children.Add(_map); }
Next add the following methods to the MapViewclass for adding and polylines and polygons to the map.
public void AddPolyline(List<BasicGeoposition> locations, Color strokeColor, double strokeThickness) { #if WINDOWS_APP var line = new MapPolyline() { Locations = locations.ToLocationCollection(), Color = strokeColor, Width = strokeThickness }; _shapeLayer.Shapes.Add(line); #elif WINDOWS_PHONE_APP var line = new MapPolyline() { Path = new Geopath(locations), StrokeColor = strokeColor, StrokeThickness = strokeThickness }; _map.MapElements.Add(line); #endif } public void AddPolygon(List<BasicGeoposition> locations, Color fillColor, Color strokeColor, double strokeThickness) { #if WINDOWS_APP var line = new MapPolygon() { Locations = locations.ToLocationCollection(), FillColor = fillColor }; _shapeLayer.Shapes.Add(line); #elif WINDOWS_PHONE_APP var line = new MapPolygon() { Path = new Geopath(locations), FillColor = fillColor, StrokeColor = strokeColor, StrokeThickness = strokeThickness }; _map.MapElements.Add(line); #endif }
To help us out we will also create a simple method for clearing all the pushpins and shapes on the map. Add the following method to the MapView class.
public void ClearMap() { #if WINDOWS_APP _shapeLayer.Shapes.Clear(); _pinLayer.Children.Clear(); #elif WINDOWS_PHONE_APP _map.MapElements.Clear(); _map.Children.Clear(); #endif }
At this point our MapView class has lots of useful functionality, now it’s just a matter of implementing it. In our app we will create an AppBar that has a bunch of buttons for testing out the different functionalities we created. Open the MainPage.xaml file and update it with the following XAML.
<Page x:Class="SharedMapSample.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:SharedMapSample" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> <Page.BottomAppBar> <CommandBar> <CommandBar.SecondaryCommands> <AppBarButton Label="Go to London" Click="GoToLondonBtn_Clicked"> <AppBarButton.Icon> <FontIcon Glyph=""/> </AppBarButton.Icon> </AppBarButton> <AppBarToggleButton Label="Toggle Traffic" Click="ToggleTrafficBtn_Clicked"> <AppBarToggleButton.Icon> <FontIcon Glyph=""/> </AppBarToggleButton.Icon> </AppBarToggleButton> <AppBarButton Label="Add Pushpins" Click="AddPushpinsBtn_Clicked"> <AppBarButton.Icon> <FontIcon Glyph=""/> </AppBarButton.Icon> </AppBarButton> <AppBarButton Label="Add Polyline" Click="AddPolylineBtn_Clicked"> <AppBarButton.Icon> <FontIcon Glyph="〽"/> </AppBarButton.Icon> </AppBarButton> <AppBarButton Label="Add Polygon" Click="AddPolygonBtn_Clicked"> <AppBarButton.Icon> <FontIcon Glyph="⬟"/> </AppBarButton.Icon> </AppBarButton> <AppBarButton Label="Clear Map" Click="ClearMapBtn_Clicked"> <AppBarButton.Icon> <FontIcon Glyph=""/> </AppBarButton.Icon> </AppBarButton> </CommandBar.SecondaryCommands> </CommandBar> </Page.BottomAppBar> <Grid> <local:MapView x:Name="MyMap" Credentials="YOUR_KEY" MapServiceToken="YOUR TOKEN" /> </Grid> </Page>
Next we need to add the event handlers for all the buttons in the app bar. Open up the MainPage.xaml.cs file and update it with the following code:
using System; using System.Collections.Generic; using Windows.Devices.Geolocation; using Windows.UI; using Windows.UI.Xaml.Controls; namespace SharedMapSample { public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } private void GoToLondonBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { MyMap.SetView(new BasicGeoposition() { Latitude = 51.5, Longitude = -0.05 }, 11); } private void ToggleTrafficBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { var toggle = sender as AppBarToggleButton; MyMap.ShowTraffic = toggle.IsChecked.HasValue && toggle.IsChecked.Value; } private void AddPushpinsBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { var locs = GetSamplePoints(); for (int i = 0; i < locs.Count; i++) { MyMap.AddPushpin(locs[i], (i + 1).ToString()); } } private void AddPolylineBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { var locs = GetSamplePoints(); MyMap.AddPolyline(locs, GetRandomColor(), 5); } private void AddPolygonBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { var locs = GetSamplePoints(); MyMap.AddPolygon(locs, GetRandomColor(), GetRandomColor(), 2); } private void ClearMapBtn_Clicked(object sender, Windows.UI.Xaml.RoutedEventArgs e) { MyMap.ClearMap(); } private List<BasicGeoposition> GetSamplePoints() { var center = MyMap.Center.Position; var rand = new Random(); center.Latitude += rand.NextDouble() * 0.05 - 0.025; center.Longitude += rand.NextDouble() * 0.05 - 0.025; var locs = new List<BasicGeoposition>(); locs.Add(new BasicGeoposition() { Latitude = center.Latitude - 0.05, Longitude = center.Longitude - 0.05 }); locs.Add(new BasicGeoposition() { Latitude = center.Latitude - 0.05, Longitude = center.Longitude + 0.05 }); locs.Add(new BasicGeoposition() { Latitude = center.Latitude + 0.05, Longitude = center.Longitude + 0.05 }); locs.Add(new BasicGeoposition() { Latitude = center.Latitude + 0.05, Longitude = center.Longitude - 0.05 }); return locs; } private Color GetRandomColor() { var rand = new Random(); byte[] bytes = new byte[3]; rand.NextBytes(bytes); return Color.FromArgb(150, bytes[0], bytes[1], bytes[2]); } } }
At this point our application is complete. If you run the Windows app and press the buttons to “Go to London”, show traffic and add a polygon, you will end up with a map that looks something like this.
If you run the Windows Phone app and press the same buttons you will end up with a map that looks something like this.
Wrapping Things Up
In this blog post we have seen how we can make use of maps in the shared project of a universal app. The full source code for this blog with some extended functionalities can be found on the MSDN Code Sample Gallery here. In this blog we have taken a look at one approach on how to use maps in universal apps. For a similar but somewhat different approach take a look at this video on Channel 9.
If you want to take things further, you may want to make the map view as reusable as possible. One way to go about this is to create two class libraries: one for Windows and the other for Windows Phone. Add the MapView class to one of the libraries, then from the other project add it as a linked file. This will give you one file to maintain between the two projects that has the same class name and namespace. If you then reference these projects from the Windows and Windows Phone projects in the Universal app respectively you will be able to use the MapView class inside the shared project. We could even go further and create a wrapper for the Bing Maps WPF control as well if we wanted to have even more cross platform support. In fact this has been done in the Microsoft Maps Spatial Toolbox library on CodePlex. This library exposes a bunch of functionalities such as heat maps, pushpin clustering and support for importing various spatial data formats. This library can be used in Windows 8.1, Windows Phone 8.x and WPF apps.
- Ricky Brundritt, , EMEA Bing Maps TSP