Friday, February 28, 2020

Displaying maps with Xamarin Forms

This blog explains how to display a map and add a pin and a route using Xamarin Forms. It is based on https://xamarinhelp.com/xamarin-forms-maps/ but adds binding for routes. It only covers Android because Apple charges too much for a developer license.

We will end up with a map, a pin, and a short route like this. I will hard code the coordinates because this isn't a blog about Geolocator.

The final result
Start a new Xamarin Forms project in Visual Studio (I used 2019). Call it XamarinMap.


Chose the blank template and de-select iOS.


The first thing to do is download the Xamarin.Forms.Maps package using NuGet.
Select Tools -> NuGet package manager -> Manage NuGet packages for solution.
Select Browse and look for Xamarin.Forms.Maps. Select and install for the whole project.


At this point I ran into a problem because the package is version 4.5x and my Xamarin.Forms package is version 4.0x so the install failed. If you see errors during the install you need to browse for Xamarin.Forms first and install version 4.5. Then you can install Xamarin.Forms.Maps.

Once you are successful your solution will look like this.


The map class we just installed is not usable with MVVM because the important properties are not Dependency Properties so they cannot be bound. We're going to fix that now.

Add a new class in the portable project and call it BindableMap. We will add dependency properties for the map center, pins, and map elements (route lines).
  • Map center is a position called MapPosition
  • The pins are a collection of Pin called MapPins
  • The route(s) is a collection of MapElement called MapElements
Replace the contents of BindableMap.cs with this.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Xamarin.Forms;
using Xamarin.Forms.Maps;

namespace XamarinMap
{
    public class BindableMap : Xamarin.Forms.Maps.Map
    {
        public static readonly BindableProperty MapPinsProperty = BindableProperty.Create(
                 nameof(MapPins),
                 typeof(ObservableCollection<Pin>),
                 typeof(BindableMap),
                 new ObservableCollection<Pin>(),
                 propertyChanged: (b, o, n) =>
                 {
                     var bindable = (BindableMap)b;
                     bindable.Pins.Clear();

                     var collection = (ObservableCollection<Pin>)n;
                     foreach (var item in collection)
                         bindable.Pins.Add(item);
                     collection.CollectionChanged += (sender, e) =>
                     {
                         Device.BeginInvokeOnMainThread(() =>
                         {
                             switch (e.Action)
                             {
                                 case NotifyCollectionChangedAction.Add:
                                 case NotifyCollectionChangedAction.Replace:
                                 case NotifyCollectionChangedAction.Remove:
                                     if (e.OldItems != null)
                                         foreach (var item in e.OldItems)
                                             bindable.Pins.Remove((Pin)item);
                                     if (e.NewItems != null)
                                         foreach (var item in e.NewItems)
                                             bindable.Pins.Add((Pin)item);
                                     break;
                                 case NotifyCollectionChangedAction.Reset:
                                     bindable.Pins.Clear();
                                     break;
                             }
                         });
                     };
                 });
        public IList<Pin> MapPins { get; set; }

        public static readonly BindableProperty MapPositionProperty = BindableProperty.Create(
                 nameof(MapPosition),
                 typeof(Position),
                 typeof(BindableMap),
                 new Position(0, 0),
                 propertyChanged: (b, o, n) =>
                 {
                     ((BindableMap)b).MoveToRegion(MapSpan.FromCenterAndRadius(
                          (Position)n,
                          Distance.FromMiles(1)));
                 });

        public Position MapPosition { get; set; }

        public static readonly BindableProperty MapElementsProperty = BindableProperty.Create(
            nameof(MapElements),
            typeof(ObservableCollection<MapElement>),
            typeof(BindableMap),
            new ObservableCollection<MapElement>(),
                             propertyChanged: (b, o, n) =>
                             {
                                 var bindable = (BindableMap)b;
                                 bindable.MapElements.Clear();

                                 var collection = (ObservableCollection<MapElement>)n;
                                 foreach (var item in collection)
                                     bindable.MapElements.Add(item);
                                 collection.CollectionChanged += (sender, e) =>
                                 {
                                     Device.BeginInvokeOnMainThread(() =>
                                     {
                                         switch (e.Action)
                                         {
                                             case NotifyCollectionChangedAction.Add:
                                             case NotifyCollectionChangedAction.Replace:
                                             case NotifyCollectionChangedAction.Remove:
                                                 if (e.OldItems != null)
                                                     foreach (var item in e.OldItems)
                                                         bindable.MapElements.Remove((MapElement)item);
                                                 if (e.NewItems != null)
                                                     foreach (var item in e.NewItems)
                                                         bindable.MapElements.Add((MapElement)item);
                                                 break;
                                             case NotifyCollectionChangedAction.Reset:
                                                 bindable.Pins.Clear();
                                                 break;
                                         }
                                     });
                                 };
                             });
    }
}

Now we can write the XAML in MainWindow.xaml. You can use this technique to make any of the map properties bindable so they can be accessed via MVVM.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:local="clr-namespace:Map"
             xmlns:maps="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
             BindingContext="{StaticResource ViewModel}"
             mc:Ignorable="d"
             x:Class="Map.MainPage">
    <ContentPage.Resources>
        <ResourceDictionary>
            <local:ViewModel x:Key="ViewModel"/>
        </ResourceDictionary>
    </ContentPage.Resources>
    <local:BindableMap MapType="Street" MapPosition="{Binding MyPosition}" MapPins="{Binding PinCollection}" MapElements="{Binding Route}"/>
</ContentPage>

We now need to create the view model. Add a folder called ViewModels and add a class in that folder called ViewModel. We will define public properties called MyPosition, PinCollection, and Route.

using System.Collections.ObjectModel;
using Xamarin.Forms.Maps;

namespace XamarinMap
{
    class ViewModel
    {
        public Position MyPosition
        {
            get { return new Position(40.74, -73.98); }
        }

        public ObservableCollection<Pin> PinCollection
        {
            get { return new ObservableCollection<Pin> { new Pin() { Position = MyPosition, Type = PinType.Generic, Label = "You are here" } }; }
        }

        public ObservableCollection<MapElement> Route
        {
            get
            {
                ObservableCollection<MapElement> r = new ObservableCollection<MapElement>();
                Polyline pl = new Xamarin.Forms.Maps.Polyline() {StrokeColor = Xamarin.Forms.Color.Purple, StrokeWidth = 8};
                pl.Geopath.Add(MyPosition);
                pl.Geopath.Add(new Position(40.7442, -73.99));
                r.Add(pl);
                return r;
            }
        }
    }
}

Two more tweaks are needed in the Android platform project. 

Open MainActivity.cs and initialize FormsMaps after the Xamarin.Forms.Forms.Init line.

using Android.App;
using Android.Content.PM;
using Android.Runtime;
using Android.OS;
using Xamarin;

namespace XamarinMap.Droid
{
    [Activity(Label = "XamarinMap", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;
            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            FormsMaps.Init(this, savedInstanceState);
            LoadApplication(new App());
        }
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

}

Now open AndroidManifest.xml in the Properties node of the Android project. Add this meta-data tag in the application tag, substituting your own API key. If you don't have one you can get one for free by following these instructions.

<application android:label="XamarinMap.Android">
  <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" />
</application>

Here's the final solution explorer. I've highlighted all the files we added or changed.