Mapping in WPF, Silverlight and WP7

In this article I show a simple example of integrating Bing and Google mapping into in WPF, Silverlight, and Windows Phone applications. I was inspired to investigate how easy it is to get up and running with maps on these platforms when I saw how similar the Bing APIs are in each case. While Google does not provide specific mapping controls for Silverlight/WPF, it's still quite straightforward to integrate the browser version, once you know where to start.

Overview

WPF Silverlight WP7 Metro
Bing Bing Maps WPF Control Bing Maps Silverlight Control Bing Maps Silverlight Control for Windows Phone Bing Maps for Metro Style Apps (Beta)
Google WebBrowser +Maps JavaScript API
(or Maps API for Flash)
Maps JavaScript API +
OOB:WebBrowser
In Browser:HTML overlay (windowless)
WebBrowser + Maps JavaScript API WebView + JavaScript API?

Microsoft provide Bing mapping controls for Silverlight, Windows Phone 7, WPF, with a very similar API (and by very similar I mean in most cases there is just a different namespace). There is also an AJAX version, but there's no real reason to use that here.

Google provide a JavaScript API which can be used by hosting in a browser control, which works well in WPF, WP7 and Silverlight out-of-browser; in Silverlight in the browser, hosting HTML content is not possible, so I'll demonstrate overlaying HTML on top of the Silverlight app. For WPF (or WinForms) applications there's also the option of embedding the Google Maps Flash control, but I'll stick with the web version as it spans these different platforms.

In both cases there are "reasonable use" limits for free use of the service, which seem to me quite... reasonable. In the case of Google Maps there is a restriction around using the service for free if your app is not also free, and it's not clear to me exactly what this means, while Bing does not impose such a restriction, which might be relevant if you're publishing a WP7 app.

Data

As a data source I'll import data in the GPX format (an XML format for exchange of GPS data). I'll just pull out a sequence of locations from this, which we can then plot on the map. GPX files contain two types of locations, "track points" (part of a "track" which is meant to be logged GPS data), and "way points" (which are more meant to be part of a plotted course or significant locations), here we'll just play dumb and extract everything in a flattened format.

I'm just embedding a few GPX files in the app resources for this example. I've chosen a few running routes which you can see in the examples below, no prizes will be awarded for guessing which one I logged myself.

private void LoadTrack(string track)
{
    using (var streamReader = new StreamReader(StreamForTrack(track)))
    {
        XDocument doc = XDocument.Parse(streamReader.ReadToEnd());
        var ns = doc.Root.GetDefaultNamespace();

        Locations  = doc.Descendants()
            .Where(el => el.Name == ns + "wpt" || el.Name == ns + "trkpt")
            .Select(trkpt =>
                new Location
                {
                    Latitude = double.Parse(trkpt.Attribute("lat").Value),
                    Longitude = double.Parse(trkpt.Attribute("lon").Value)
                });
            .ToLocationCollection();
    };
}

/// Work around annoying LocationCollection - which contains no AddRange method or collection constructor
public static LocationCollection ToLocationCollection(this IEnumerable<Location> locations)
{
    var locs = new LocationCollection();
    foreach (var loc in locations)
    {
       locs.Add(loc);
    }
    return locs;
}

Lastly I'll expose the available tracks as a property AllTracks and the selected track as Track:

private string _track;
public string Track
{
    get { return _track;}
    set
    {
        if (value == _track) return;
        _track = value;
        LoadTrack(_track);
        OnPropertyChanged("Track");
    }
}

Bing - Silverlight

It's refreshingly simple to create a map to display our route. Setting the DataContext to the class described above, the following XAML displays a map of the route:

<!--Need to set CredentialsProvider=...-->
<Grid>
    <Grid.DataContext>
        <local:GpxPath />
    </Grid.DataContext>
    <bing:Map x:Name="Map" Mode="Aerial">
        <bing:MapPolyline Locations="{Binding Locations}"  Stroke="Red" StrokeThickness="1" />
    </bing:Map>
</Grid>

On top of this it's nice to actually show the relevant part of the map, so lets set the view based on the bounding rectangle of the given locations. I wanted to do this in XAML, but unfortunately this is a method on the Map class. First step, wrap this in an attached property:

public static class Mapping
{
    // ...
    public static readonly DependencyProperty ViewProperty =
        DependencyProperty.RegisterAttached("View", typeof(LocationRect), typeof(Mapping), new PropertyMetadata(OnViewPropertyChanged));

    private static void OnViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Map map = d as Map;
        var rect = e.NewValue as LocationRect;
        if (map != null && rect != null)
        {
            map.SetView(rect);
        }
    }
}

Then we can make a converter to get the bounding LocationRect of our Locations collection:

public class LocationsViewConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var locs = value as IEnumerable<Location>;
        if (locs == null || !locs.Any()) { return null; }
        return new LocationRect(locs.Max(l => l.Latitude), locs.Min(l => l.Longitude),  // N, W
                                locs.Min(l => l.Latitude), locs.Max(l => l.Longitude)); // S, E
    }
    // ...
}

It's quite nice to see pushpins for the route start/end, so lets add them. Just as MapPolyline is analogous to Polyline, MapItemsControl is to ItemsControl. So I can add pushpins by binding to the appropriate collection. Add a property and update it when loading a new track:

public IEnumerable<Location> PushPins { get; set; } // Really add property changed notification
// ...
    PushPins = new List<Location> { locations.First(), locations.Last() };

Here's the updated XAML with the view setting and push pins:

<bing:Map Mode="Aerial"
          local:Mapping.View="{Binding Locations, Converter={StaticResource LocationsViewConverter}}">
    <bing:MapLayer>
        <bing:MapPolyline Locations="{Binding Locations}"  Stroke="Red" StrokeThickness="1" />
    </bing:MapLayer>
    <bing:MapItemsControl ItemsSource="{Binding PushPins}">
        <bing:MapItemsControl.ItemTemplate>
            <DataTemplate>
                <bing:Pushpin Location="{Binding}" />
            </DataTemplate>
        </bing:MapItemsControl.ItemTemplate>
    </bing:MapItemsControl>
</bing:Map>

And the end result: Get Microsoft Silverlight

Bing - WPF

The WPF Bing control follows the Silverlight control API very closely, and other than the namespace there's little difference. Some options are different for the surrounding UI, but the main API is the same. It literally is as simple as using the same code files with:

#if SILVERLIGHT
using Microsoft.Maps.MapControl;
#else
using Microsoft.Maps.MapControl.WPF;
#endif

The XAML is very similar - see the project at the end of the post for other details.

Bing - WP7

Again the WP7 API is similar, although in this case there are some differences. An obvious one is the Location class is supplemented with the Geolocation class - for example LocationCollection is now an ObservableCollection<Geolocation>. In fact there are implicit conversions between Location and Geolocation, so in most cases we can again get away with the same C# code with some careful use of types - other than some conditional includes:

#if WINDOWS_PHONE
using Microsoft.Phone.Controls.Maps;
using Microsoft.Phone.Controls.Maps.Platform;
#elif SILVERLIGHT
using Microsoft.Maps.MapControl;
#else
using Microsoft.Maps.MapControl.WPF;
#endif

Of course the end result looks somewhat different:

And finally

The various projects with the above code can be downloaded here. If I was going to create the same thing again I'd keep references to Bing namespaces entirely out of the common code with some more aggressive use of converters, rather than dancing around the differences, but as an exploration of the APIs I found it interesting to see how little had to change.

I'm going to pause for there and show the Google Maps version in another installment. There there will be some more interesting issues to consider.

MORE BY NICHOLAS

blog comments powered by Disqus