Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Controls/Maps/src/HandlerImpl/Map.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ bool IMap.ClusterClicked(IReadOnlyList<IMapPin> pins, Location location)

void IMap.LongClicked(Location location) => MapLongClicked?.Invoke(this, new MapClickedEventArgs(location));

void IMap.ShowInfoWindow(IMapPin pin) => Handler?.Invoke(nameof(IMap.ShowInfoWindow), pin);

void IMap.HideInfoWindow(IMapPin pin) => Handler?.Invoke(nameof(IMap.HideInfoWindow), pin);

MapSpan? IMap.VisibleRegion
{
get
Expand Down
16 changes: 16 additions & 0 deletions src/Controls/Maps/src/Map.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,22 @@ void PinsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
throw new ArgumentException("Pin must have a Label to be added to a map");
}

if (e.NewItems is not null)
{
foreach (Pin pin in e.NewItems)
{
pin.Parent = this;
}
}

if (e.OldItems is not null)
{
foreach (Pin pin in e.OldItems)
{
pin.Parent = null;
}
}
Comment on lines +288 to +302
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PinsOnCollectionChanged sets pin.Parent for NewItems and clears it for OldItems, but ObservableCollection.Clear() raises a Reset action with no OldItems. That means map.Pins.Clear() (and other reset scenarios) will leave the removed pins' Parent pointing at the Map, which can cause incorrect behavior and keep the Map alive (leak) if pins are retained elsewhere. Consider handling NotifyCollectionChangedAction.Reset (e.g., by using a custom collection that raises a Remove event with the removed items, similar to GestureElement.GestureRecognizerCollection).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it looks like a good point. I've seen some devs reporting issues with memory leaks when it comes to map, so it would be good to make sure we avoid them

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a valid concern but it's a pre-existing issue in Map.cs, not introduced by this PR. The Reset action not clearing Parent was already the behavior before this change. A separate issue/PR should address this collection management improvement.


Handler?.UpdateValue(nameof(IMap.Pins));
}

Expand Down
24 changes: 24 additions & 0 deletions src/Controls/Maps/src/Pin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,30 @@ public bool SendInfoWindowClick()
return args.HideInfoWindow;
}

/// <summary>
/// Shows the info window (callout) for this pin on the map.
/// </summary>
/// <remarks>The pin must be added to a <see cref="Map"/> for this method to have an effect.</remarks>
public void ShowInfoWindow()
{
if (Parent is Map map)
{
map.Handler?.Invoke(nameof(IMap.ShowInfoWindow), this);
}
}

/// <summary>
/// Hides the info window (callout) for this pin on the map.
/// </summary>
/// <remarks>The pin must be added to a <see cref="Map"/> for this method to have an effect.</remarks>
public void HideInfoWindow()
{
if (Parent is Map map)
{
map.Handler?.Invoke(nameof(IMap.HideInfoWindow), this);
}
}

bool Equals(Pin other)
{
return string.Equals(Label, other.Label, StringComparison.Ordinal) &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/Maps/src/PublicAPI/net/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ Microsoft.Maui.Controls.Maps.MapElement.ZIndex.get -> int
Microsoft.Maui.Controls.Maps.MapElement.ZIndex.set -> void
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.get -> string!
Microsoft.Maui.Controls.Maps.Pin.ClusteringIdentifier.set -> void
Microsoft.Maui.Controls.Maps.Pin.HideInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Pin.ImageSource.get -> Microsoft.Maui.Controls.ImageSource?
Microsoft.Maui.Controls.Maps.Pin.ImageSource.set -> void
Microsoft.Maui.Controls.Maps.Pin.ShowInfoWindow() -> void
Microsoft.Maui.Controls.Maps.Polygon.PolygonClicked -> System.EventHandler?
Microsoft.Maui.Controls.Maps.Polyline.PolylineClicked -> System.EventHandler?
const Microsoft.Maui.Controls.Maps.Pin.DefaultClusteringIdentifier = "maui_default_cluster" -> string!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:maps="clr-namespace:Microsoft.Maui.Controls.Maps;assembly=Microsoft.Maui.Controls.Maps"
x:Class="Maui.Controls.Sample.Pages.MapsGalleries.InfoWindowGallery"
Title="Info Window">
<Grid RowDefinitions="Auto,*">
<HorizontalStackLayout Spacing="10" Padding="10">
<Button Text="Show Seattle" AutomationId="ShowSeattle" Clicked="OnShowSeattle" />
<Button Text="Show NYC" AutomationId="ShowNYC" Clicked="OnShowNYC" />
<Button Text="Hide All" AutomationId="HideAll" Clicked="OnHideAll" />
</HorizontalStackLayout>
<maps:Map x:Name="map" Grid.Row="1" />
</Grid>
</ContentPage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Devices.Sensors;
using Microsoft.Maui.Maps;

namespace Maui.Controls.Sample.Pages.MapsGalleries;

public partial class InfoWindowGallery : ContentPage
{
Pin _seattlePin;
Pin _nycPin;

public InfoWindowGallery()
{
InitializeComponent();

_seattlePin = new Pin
{
Label = "Seattle",
Address = "Washington, USA",
Location = new Microsoft.Maui.Devices.Sensors.Location(47.6062, -122.3321)
};

_nycPin = new Pin
{
Label = "New York City",
Address = "New York, USA",
Location = new Microsoft.Maui.Devices.Sensors.Location(40.7128, -74.0060)
};

map.Pins.Add(_seattlePin);
map.Pins.Add(_nycPin);
map.Pins.Add(new Pin
{
Label = "Los Angeles",
Address = "California, USA",
Location = new Microsoft.Maui.Devices.Sensors.Location(34.0522, -118.2437)
});

map.MoveToRegion(MapSpan.FromCenterAndRadius(
new Microsoft.Maui.Devices.Sensors.Location(39.8, -98.5), Distance.FromMiles(1500)));
}

void OnShowSeattle(object? sender, EventArgs e)
{
_seattlePin.ShowInfoWindow();
}

void OnShowNYC(object? sender, EventArgs e)
{
_nycPin.ShowInfoWindow();
}

void OnHideAll(object? sender, EventArgs e)
{
_seattlePin.HideInfoWindow();
_nycPin.HideInfoWindow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public MapsGallery()
GalleryBuilder.NavButton("Element Visibility & ZIndex", () => new MapElementVisibilityGallery(), Navigation),
GalleryBuilder.NavButton("MapElement Click Events", () => new MapElementClickGallery(), Navigation),
GalleryBuilder.NavButton("Map Long Click", () => new MapLongClickGallery(), Navigation),
GalleryBuilder.NavButton("Info Window", () => new InfoWindowGallery(), Navigation),
}
}
};
Expand Down
56 changes: 56 additions & 0 deletions src/Controls/tests/Core.UnitTests/MapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,62 @@ public void PolylineClickedEventSenderIsPolyline()
Assert.Same(polyline, sender);
}

[Fact]
public void ShowInfoWindowDoesNotThrowWhenPinHasNoParent()
{
var pin = new Pin
{
Label = "Test",
Location = new Location(0, 0)
};

// Should not throw even without a parent map
pin.ShowInfoWindow();
}

[Fact]
public void HideInfoWindowDoesNotThrowWhenPinHasNoParent()
{
var pin = new Pin
{
Label = "Test",
Location = new Location(0, 0)
};

// Should not throw even without a parent map
pin.HideInfoWindow();
}

[Fact]
public void ShowInfoWindowOnPinAddedToMap()
{
var map = new Map();
var pin = new Pin
{
Label = "Test",
Location = new Location(47.6, -122.3)
};
map.Pins.Add(pin);

// Pin has a parent now; without a handler it's a no-op but should not throw
pin.ShowInfoWindow();
}

[Fact]
public void HideInfoWindowOnPinAddedToMap()
{
var map = new Map();
var pin = new Pin
{
Label = "Test",
Location = new Location(47.6, -122.3)
};
map.Pins.Add(pin);

// Pin has a parent now; without a handler it's a no-op but should not throw
pin.HideInfoWindow();
}

[Fact]
public void MapLongClickedEventFires()
{
Expand Down
12 changes: 12 additions & 0 deletions src/Core/maps/src/Core/IMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,17 @@ public interface IMap : IView
/// Moves the map so that it displays the specified <see cref="MapSpan"/> region.
/// </summary>
void MoveToRegion(MapSpan region);

/// <summary>
/// Shows the info window for the specified pin.
/// </summary>
/// <param name="pin">The pin whose info window should be shown.</param>
void ShowInfoWindow(IMapPin pin);

/// <summary>
/// Hides the info window for the specified pin.
/// </summary>
/// <param name="pin">The pin whose info window should be hidden.</param>
void HideInfoWindow(IMapPin pin);
}
}
26 changes: 26 additions & 0 deletions src/Core/maps/src/Handlers/Map/MapHandler.Android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,32 @@ static ABitmap ScaleBitmap(ABitmap source, int targetWidth, int targetHeight)
return targetPin;
}

Marker? GetMarkerForPin(IMapPin pin)
{
if (_markers == null || pin.MarkerId is not string pinMarkerId)
return null;

for (int i = 0; i < _markers.Count; i++)
{
if (_markers[i].Id == pinMarkerId)
return _markers[i];
}

return null;
}

void ShowInfoWindow(IMapPin pin)
{
var marker = GetMarkerForPin(pin);
marker?.ShowInfoWindow();
}

void HideInfoWindow(IMapPin pin)
{
var marker = GetMarkerForPin(pin);
marker?.HideInfoWindow();
}

void ClearMapElements()
{
if (_polylines != null)
Expand Down
4 changes: 4 additions & 0 deletions src/Core/maps/src/Handlers/Map/MapHandler.Standard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,9 @@ public partial class MapHandler : ViewHandler<IMap, object>
public static void MapElements(IMapHandler handler, IMap map) => throw new NotImplementedException();

public void UpdateMapElement(IMapElement element) => throw new NotImplementedException();

void ShowInfoWindow(IMapPin pin) { }

void HideInfoWindow(IMapPin pin) { }
}
}
4 changes: 4 additions & 0 deletions src/Core/maps/src/Handlers/Map/MapHandler.Tizen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ public partial class MapHandler : ViewHandler<IMap, NView>
public static void MapElements(IMapHandler handler, IMap map) => throw new NotImplementedException();

public void UpdateMapElement(IMapElement element) => throw new NotImplementedException();

void ShowInfoWindow(IMapPin pin) { }

void HideInfoWindow(IMapPin pin) { }
}
}
4 changes: 4 additions & 0 deletions src/Core/maps/src/Handlers/Map/MapHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,9 @@ public partial class MapHandler : ViewHandler<IMap, FrameworkElement>
public static void MapElements(IMapHandler handler, IMap map) => throw new NotImplementedException();

public void UpdateMapElement(IMapElement element) => throw new NotImplementedException();

void ShowInfoWindow(IMapPin pin) { }

void HideInfoWindow(IMapPin pin) { }
}
}
Loading
Loading