Map with ItemsSource and ItemTemplate (#4269) fixes #1708
authorAndrei Nitescu <nitescua@yahoo.com>
Thu, 10 Jan 2019 17:52:11 +0000 (19:52 +0200)
committerRui Marinho <me@ruimarinho.net>
Thu, 10 Jan 2019 17:52:11 +0000 (17:52 +0000)
Xamarin.Forms.Controls/CoreGallery.cs
Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml [new file with mode: 0644]
Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml.cs [new file with mode: 0644]
Xamarin.Forms.Controls/Xamarin.Forms.Controls.csproj
Xamarin.Forms.Core.UnitTests/MapTests.cs
Xamarin.Forms.Maps/Map.cs
Xamarin.Forms.Maps/Pin.cs

index 0d8f322..daf969d 100644 (file)
@@ -359,6 +359,7 @@ namespace Xamarin.Forms.Controls
                                new GalleryPageFactory(() => new ListRefresh(), "ListView.PullToRefresh"),
                                new GalleryPageFactory(() => new ListViewDemoPage(), "ListView Demo Gallery - Legacy"),
                                new GalleryPageFactory(() => new MapGallery(), "Map Gallery - Legacy"),
+                               new GalleryPageFactory(() => new MapWithItemsSourceGallery(), "Map With ItemsSource Gallery - Legacy"),
                                new GalleryPageFactory(() => new MinimumSizeGallery(), "MinimumSize Gallery - Legacy"),
                                new GalleryPageFactory(() => new MultiGallery(), "Multi Gallery - Legacy"),
                                new GalleryPageFactory(() => new NavigationMenuGallery(), "NavigationMenu Gallery - Legacy"),
diff --git a/Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml b/Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml
new file mode 100644 (file)
index 0000000..e48b7a9
--- /dev/null
@@ -0,0 +1,35 @@
+<?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:map="clr-namespace:Xamarin.Forms.Maps;assembly=Xamarin.Forms.Maps"
+                        x:Class="Xamarin.Forms.Controls.GalleryPages.MapWithItemsSourceGallery">
+       <Grid>
+               <Grid.RowDefinitions>
+                       <RowDefinition />
+                       <RowDefinition Height="Auto" />
+               </Grid.RowDefinitions>
+               <map:Map ItemsSource="{Binding Places}"
+                                x:Name="_map">
+                       <map:Map.ItemTemplate>
+                               <DataTemplate>
+                                       <map:Pin Position="{Binding Position}"
+                                                        Address="{Binding Address}"
+                                                        Label="{Binding Description}" />
+                               </DataTemplate>
+                       </map:Map.ItemTemplate>
+               </map:Map>
+               <StackLayout Orientation="Horizontal"
+                                        Grid.Row="1">
+                       <Button Text="Add"
+                                       Command="{Binding AddPlaceCommand}" />
+                       <Button Text="Remove"
+                                       Command="{Binding RemovePlaceCommand}" />
+                       <Button Text="Clear"
+                                       Command="{Binding ClearPlacesCommand}" />
+                       <Button Text="Update"
+                                       Command="{Binding UpdatePlacesCommand}" />
+                       <Button Text="Replace"
+                                       Command="{Binding ReplacePlaceCommand}" />
+               </StackLayout>
+       </Grid>
+</ContentPage>
\ No newline at end of file
diff --git a/Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml.cs b/Xamarin.Forms.Controls/GalleryPages/MapWithItemsSourceGallery.xaml.cs
new file mode 100644 (file)
index 0000000..dd96aa3
--- /dev/null
@@ -0,0 +1,155 @@
+using System;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows.Input;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.Maps;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Controls.GalleryPages
+{
+       [XamlCompilation(XamlCompilationOptions.Compile)]
+       public partial class MapWithItemsSourceGallery : ContentPage
+       {
+               static readonly Position startPosition = new Position(39.8283459, -98.5794797);
+
+               public MapWithItemsSourceGallery()
+               {
+                       InitializeComponent();
+                       BindingContext = new ViewModel();
+                       _map.MoveToRegion(MapSpan.FromCenterAndRadius(startPosition, Distance.FromMiles(1200)));
+               }
+
+               [Preserve(AllMembers = true)]
+               class ViewModel
+               {
+                       int _pinCreatedCount = 0;
+
+                       readonly ObservableCollection<Place> _places;
+
+                       public IEnumerable Places => _places;
+
+                       public ICommand AddPlaceCommand { get; }
+                       public ICommand RemovePlaceCommand { get; }
+                       public ICommand ClearPlacesCommand { get; }
+                       public ICommand UpdatePlacesCommand { get; }
+                       public ICommand ReplacePlaceCommand { get; }
+
+                       public ViewModel()
+                       {
+                               _places = new ObservableCollection<Place>() {
+                                       new Place("New York, USA", "The City That Never Sleeps", new Position(40.67, -73.94)),
+                                       new Place("Los Angeles, USA", "City of Angels", new Position(34.11, -118.41)),
+                                       new Place("San Francisco, USA", "Bay City ", new Position(37.77, -122.45))
+                               };
+
+                               AddPlaceCommand = new Command(AddPlace);
+                               RemovePlaceCommand = new Command(RemovePlace);
+                               ClearPlacesCommand = new Command(() => _places.Clear());
+                               UpdatePlacesCommand = new Command(UpdatePlaces);
+                               ReplacePlaceCommand = new Command(ReplacePlace);
+                       }
+
+                       void AddPlace()
+                       {
+                               _places.Add(NewPlace());
+                       }
+
+                       void RemovePlace()
+                       {
+                               if (_places.Any())
+                               {
+                                       _places.Remove(_places.First());
+                               }
+                       }
+
+                       void UpdatePlaces()
+                       {
+                               if (!_places.Any())
+                               {
+                                       return;
+                               }
+
+                               double lastLatitude = _places.Last().Position.Latitude;
+
+                               foreach (Place place in Places)
+                               {
+                                       place.Position = new Position(lastLatitude, place.Position.Longitude);
+                               }
+                       }
+
+                       void ReplacePlace()
+                       {
+                               if (!_places.Any())
+                               {
+                                       return;
+                               }
+
+                               _places[_places.Count - 1] = NewPlace();
+                       }
+
+                       static class RandomPosition
+                       {
+                               static Random Random = new Random(Environment.TickCount);
+
+                               public static Position Next()
+                               {
+                                       return new Position(
+                                               latitude: Random.NextDouble() * 180 - 90,
+                                               longitude: Random.NextDouble() * 360 - 180);
+                               }
+
+                               public static Position Next(Position position, double latitudeRange, double longitudeRange)
+                               {
+                                       return new Position(
+                                               latitude: position.Latitude + (Random.NextDouble() * 2 - 1) * latitudeRange,
+                                               longitude: position.Longitude + (Random.NextDouble() * 2 - 1) * longitudeRange);
+                               }
+                       }
+
+                       Place NewPlace()
+                       {
+                               ++_pinCreatedCount;
+
+                               return new Place(
+                                       $"Pin {_pinCreatedCount}",
+                                       $"Desc {_pinCreatedCount}",
+                                       RandomPosition.Next(startPosition, 8, 19));
+                       }
+               }
+
+               [Preserve(AllMembers = true)]
+               class Place : INotifyPropertyChanged
+               {
+                       Position _position;
+
+                       public event PropertyChangedEventHandler PropertyChanged;
+
+                       public string Address { get; }
+
+                       public string Description { get; }
+
+                       public Position Position
+                       {
+                               get => _position;
+                               set
+                               {
+                                       if (!_position.Equals(value))
+                                       {
+                                               _position = value;
+                                               PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Position)));
+                                       }
+                               }
+                       }
+
+                       public Place(string address, string description, Position position)
+                       {
+                               Address = address;
+                               Description = description;
+                               Position = position;
+                       }
+               }
+       }
+}
\ No newline at end of file
index ce3765f..8826b36 100644 (file)
     <EmbeddedResource Include="GalleryPages\crimson.jpg" />
     <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
     <PackageReference Include="Xam.Plugin.DeviceInfo" Version="3.0.2" />
+    <Compile Update="GalleryPages\BindableLayoutGalleryPage.xaml.cs">
+      <DependentUpon>BindableLayoutGalleryPage.xaml</DependentUpon>
+    </Compile>
+    <Compile Update="GalleryPages\VisualStateManagerGalleries\OnPlatformExample.xaml.cs">
+      <DependentUpon>OnPlatformExample.xaml</DependentUpon>
+    </Compile>
+    <EmbeddedResource Update="GalleryPages\BindableLayoutGalleryPage.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\MapWithItemsSourceGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\TitleView.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\ButtonDisabledStatesGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\DatePickerDisabledStatesGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\PickerDisabledStatesGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\SearchBarDisabledStatesGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\TimePickerDisabledStatesGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\VisualStateManagerGalleries\ValidationExample.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\XamlPage.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="ControlGalleryPages\BehaviorsAndTriggers.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\StyleXamlGallery.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\XamlNativeViews.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\MacTwitterDemo.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="HanselForms\MyAbout.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="HanselForms\BlogPage.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="HanselForms\TwitterPage.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="CompressedLayout.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="ControlGalleryPages\LayoutAddPerformance.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
+    <EmbeddedResource Update="GalleryPages\ControlTemplateXamlPage.xaml">
+      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+    </EmbeddedResource>
   </ItemGroup>
   <Target Name="CreateControllGalleryConfig" BeforeTargets="Build">
     <CreateItem Include="blank.config">
index 0d6888d..251e944 100644 (file)
@@ -1,5 +1,8 @@
-using System;
 using NUnit.Framework;
+using System;
+using System.Collections;
+using System.Collections.ObjectModel;
+using System.Linq;
 using Xamarin.Forms.Maps;
 
 namespace Xamarin.Forms.Core.UnitTests
@@ -143,5 +146,308 @@ namespace Xamarin.Forms.Core.UnitTests
 
                        Assert.False (signaled);
                }
+
+               [Test]
+               public void TracksEmpty()
+               {
+                       var map = new Map();
+
+                       var itemsSource = new ObservableCollection<int>();
+                       map.ItemsSource = itemsSource;
+                       map.ItemTemplate = new DataTemplate();
+
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksAdd()
+               {
+                       var itemsSource = new ObservableCollection<int>();
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource.Add(1);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksInsert()
+               {
+                       var itemsSource = new ObservableCollection<int>();
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource.Insert(0, 1);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksRemove()
+               {
+                       var itemsSource = new ObservableCollection<int>() { 0, 1 };
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource.RemoveAt(0);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+
+                       itemsSource.Remove(1);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksReplace()
+               {
+                       var itemsSource = new ObservableCollection<int>() { 0, 1, 2 };
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource[0] = 3;
+                       itemsSource[1] = 4;
+                       itemsSource[2] = 5;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void ItemMove()
+               {
+                       var itemsSource = new ObservableCollection<int>() { 0, 1 };
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource.Move(0, 1);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+
+                       itemsSource.Move(1, 0);
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksClear()
+               {
+                       var itemsSource = new ObservableCollection<int>() { 0, 1 };
+
+                       var map = new Map()
+                       {
+                               ItemsSource = itemsSource,
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       itemsSource.Clear();
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksNull()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+                       map.ItemsSource = itemsSource;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+
+                       itemsSource = null;
+                       map.ItemsSource = itemsSource;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void TracksItemTemplate()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 3));
+                       map.ItemsSource = itemsSource;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+                       foreach (Pin pin in map.Pins)
+                       {
+                               Assert.IsTrue(pin.Address == "Address");
+                       }
+
+                       map.ItemTemplate = GetItemTemplate("Address 2");
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+                       foreach(Pin pin in map.Pins)
+                       {
+                               Assert.IsTrue(pin.Address == "Address 2");
+                       }
+               }
+
+               [Test]
+               public void ItemsSourceTakePrecendenceOverPins()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       map.Pins.Add(new Pin() { Label = "Label" });
+                       map.Pins.Add(new Pin() { Label = "Label" });
+
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+                       map.ItemsSource = itemsSource;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               [Test]
+               public void ElementIsGarbageCollectedAfterItsRemoved()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       // Create a view-model and bind the map to it
+                       map.SetBinding(Map.ItemsSourceProperty, new Binding(nameof(MockViewModel.Items)));
+                       map.BindingContext = new MockViewModel(new ObservableCollection<int>(Enumerable.Range(0, 10)));
+
+                       // Set ItemsSource
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+                       itemsSource = null;
+
+                       // Remove map from container
+                       var pageRoot = new Grid();
+                       pageRoot.Children.Add(map);
+                       var page = new ContentPage() { Content = pageRoot };
+
+                       var weakReference = new WeakReference(map);
+                       pageRoot.Children.Remove(map);
+                       map = null;
+
+                       GC.Collect();
+                       GC.WaitForPendingFinalizers();
+
+                       Assert.IsFalse(weakReference.IsAlive);
+               }
+
+               [Test]
+               public void ThrowsExceptionOnUsingDataTemplateSelectorForItemTemplate()
+               {
+                       var map = new Map();
+
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+                       map.ItemsSource = itemsSource;
+
+                       Assert.Throws(typeof(NotSupportedException), () => map.ItemTemplate = GetDataTemplateSelector());
+               }
+
+               [Test]
+               public void DontTrackAfterItemsSourceChanged()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       var itemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+                       map.ItemsSource = itemsSource;
+                       map.ItemsSource = new ObservableCollection<int>(Enumerable.Range(0, 10));
+
+                       itemsSource.Add(11);
+                       Assert.IsTrue(itemsSource.Count() == 11);
+               }
+
+               [Test]
+               public void WorksWithNullItems()
+               {
+                       var map = new Map()
+                       {
+                               ItemTemplate = GetItemTemplate()
+                       };
+
+                       var itemsSource = new ObservableCollection<int?>(Enumerable.Range(0, 10).Cast<int?>());
+                       itemsSource.Add(null);
+                       map.ItemsSource = itemsSource;
+                       Assert.IsTrue(IsMapWithItemsSource(itemsSource, map));
+               }
+
+               // Checks if for every item in the items source there's a corresponding pin
+               static bool IsMapWithItemsSource(IEnumerable itemsSource, Map map)
+               {
+                       if (itemsSource == null)
+                       {
+                               return true;
+                       }
+
+                       if (map.ItemTemplate == null)
+                       {
+                               // If ItemsSource is set but ItemTemplate is not, there should not be any Pins
+                               return !map.Pins.Any();
+                       }
+
+                       int i = 0;
+                       foreach (object item in itemsSource)
+                       {
+                               // Pins collection order are not tracked, so just make sure a Pin for item exists
+                               if (!map.Pins.Any(p => Equals(item, p.BindingContext)))
+                               {
+                                       return false;
+                               }
+
+                               ++i;
+                       }
+
+                       return map.Pins.Count == i;
+               }
+
+               static DataTemplate GetItemTemplate(string address = null)
+               {
+                       return new DataTemplate(() => new Pin()
+                       {
+                               Address = address ?? "Address",
+                               Label = "Label",
+                               Position = new Position()
+                       });
+               }
+
+               static DataTemplateSelector GetDataTemplateSelector()
+               {
+                       return new TestDataTemplateSelector();
+               }
+
+               class TestDataTemplateSelector : DataTemplateSelector
+               {
+                       readonly DataTemplate dt = GetItemTemplate();
+
+                       protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
+                       {
+                               return dt;
+                       }
+               }
+
+               class MockViewModel
+               {
+                       public IEnumerable Items { get; }
+                       public MockViewModel(IEnumerable itemsSource)
+                       {
+                               Items = itemsSource;
+                       }
+               }
        }
 }
index c717950..f881c19 100644 (file)
@@ -18,6 +18,12 @@ namespace Xamarin.Forms.Maps
 
                public static readonly BindableProperty HasZoomEnabledProperty = BindableProperty.Create("HasZoomEnabled", typeof(bool), typeof(Map), true);
 
+               public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(nameof(IEnumerable), typeof(IEnumerable), typeof(Map), default(IEnumerable),
+                       propertyChanged: (b, o, n) => ((Map)b).OnItemsSourcePropertyChanged((IEnumerable)o, (IEnumerable)n));
+
+               public static readonly BindableProperty ItemTemplateProperty = BindableProperty.Create(nameof(ItemTemplate), typeof(DataTemplate), typeof(Map), default(DataTemplate),
+                       propertyChanged: (b, o, n) => ((Map)b).OnItemTemplatePropertyChanged((DataTemplate)o, (DataTemplate)n));
+
                readonly ObservableCollection<Pin> _pins = new ObservableCollection<Pin>();
                MapSpan _visibleRegion;
 
@@ -64,6 +70,18 @@ namespace Xamarin.Forms.Maps
                        get { return _pins; }
                }
 
+               public IEnumerable ItemsSource
+               {
+                       get { return (IEnumerable)GetValue(ItemsSourceProperty); }
+                       set { SetValue(ItemsSourceProperty, value); }
+               }
+
+               public DataTemplate ItemTemplate
+               {
+                       get { return (DataTemplate)GetValue(ItemTemplateProperty); }
+                       set { SetValue(ItemTemplateProperty, value); }
+               }
+
                [EditorBrowsable(EditorBrowsableState.Never)]
                public void SetVisibleRegion(MapSpan value) => VisibleRegion = value;
                public MapSpan VisibleRegion
@@ -107,5 +125,101 @@ namespace Xamarin.Forms.Maps
                        if (e.NewItems != null && e.NewItems.Cast<Pin>().Any(pin => pin.Label == null))
                                throw new ArgumentException("Pin must have a Label to be added to a map");
                }
+
+               void OnItemsSourcePropertyChanged(IEnumerable oldItemsSource, IEnumerable newItemsSource)
+               {
+                       if (oldItemsSource is INotifyCollectionChanged ncc)
+                       {
+                               ncc.CollectionChanged -= OnItemsSourceCollectionChanged;
+                       }
+
+                       if (newItemsSource is INotifyCollectionChanged ncc1)
+                       {
+                               ncc1.CollectionChanged += OnItemsSourceCollectionChanged;
+                       }
+
+                       _pins.Clear();
+                       CreatePinItems();
+               }
+
+               void OnItemTemplatePropertyChanged(DataTemplate oldItemTemplate, DataTemplate newItemTemplate)
+               {
+                       if (newItemTemplate is DataTemplateSelector)
+                       {
+                               throw new NotSupportedException($"You are using an instance of {nameof(DataTemplateSelector)} to set the {nameof(Map)}.{ItemTemplateProperty.PropertyName} property. Use an instance of a {nameof(DataTemplate)} property instead to set an item template.");
+                       }
+
+                       _pins.Clear();
+                       CreatePinItems();
+               }
+
+               void OnItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+               {
+                       switch (e.Action)
+                       {
+                               case NotifyCollectionChangedAction.Add:
+                                       if (e.NewStartingIndex == -1)
+                                               goto case NotifyCollectionChangedAction.Reset;
+                                       foreach (object item in e.NewItems)
+                                               CreatePin(item);
+                                       break;
+                               case NotifyCollectionChangedAction.Move:
+                                       if (e.OldStartingIndex == -1 || e.NewStartingIndex == -1)
+                                               goto case NotifyCollectionChangedAction.Reset;
+                                       // Not tracking order
+                                       break;
+                               case NotifyCollectionChangedAction.Remove:
+                                       if (e.OldStartingIndex == -1)
+                                               goto case NotifyCollectionChangedAction.Reset;
+                                       foreach (object item in e.OldItems)
+                                               RemovePin(item);
+                                       break;
+                               case NotifyCollectionChangedAction.Replace:
+                                       if (e.OldStartingIndex == -1)
+                                               goto case NotifyCollectionChangedAction.Reset;
+                                       foreach (object item in e.OldItems)
+                                               RemovePin(item);
+                                       foreach (object item in e.NewItems)
+                                               CreatePin(item);
+                                       break;
+                               case NotifyCollectionChangedAction.Reset:
+                                       _pins.Clear();
+                                       break;
+                       }
+               }
+
+               void CreatePinItems()
+               {
+                       if (ItemsSource == null || ItemTemplate == null)
+                       {
+                               return;
+                       }
+
+                       foreach (object item in ItemsSource)
+                       {
+                               CreatePin(item);
+                       }
+               }
+
+               void CreatePin(object newItem)
+               {
+                       if (ItemTemplate == null)
+                       {
+                               return;
+                       }
+
+                       var pin = (Pin)ItemTemplate.CreateContent();
+                       pin.BindingContext = newItem;
+                       _pins.Add(pin);
+               }
+
+               void RemovePin(object itemToRemove)
+               {
+                       Pin pinToRemove = _pins.FirstOrDefault(pin => pin.BindingContext?.Equals(itemToRemove) == true);
+                       if (pinToRemove != null)
+                       {
+                               _pins.Remove(pinToRemove);
+                       }
+               }
        }
 }
\ No newline at end of file
index 43f7b75..ca959ca 100644 (file)
@@ -3,7 +3,7 @@ using System.ComponentModel;
 
 namespace Xamarin.Forms.Maps
 {
-       public class Pin : BindableObject
+       public class Pin : Element
        {
                public static readonly BindableProperty TypeProperty = BindableProperty.Create("Type", typeof(PinType), typeof(Pin), default(PinType));