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 |
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
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;
}
});
};
});
}
}
<?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;
}
}
}
}
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.