diff --git a/src/Controls/src/Core/IndicatorView/IndicatorView.cs b/src/Controls/src/Core/IndicatorView/IndicatorView.cs
index 961d99bb11d3..363a05abcf62 100644
--- a/src/Controls/src/Core/IndicatorView/IndicatorView.cs
+++ b/src/Controls/src/Core/IndicatorView/IndicatorView.cs
@@ -60,10 +60,18 @@ public partial class IndicatorView : TemplatedView, ITemplatedIndicatorView
static readonly BindableProperty IndicatorLayoutProperty = BindableProperty.Create(nameof(IndicatorLayout), typeof(IBindableLayout), typeof(IndicatorView), null, propertyChanged: TemplateUtilities.OnContentChanged);
+ readonly WeakNotifyCollectionChangedProxy _collectionChangedProxy = new();
+ readonly NotifyCollectionChangedEventHandler _collectionChangedEventHandler;
+
///
/// Initializes a new instance of the class.
///
- public IndicatorView() { }
+ public IndicatorView()
+ {
+ _collectionChangedEventHandler = OnCollectionChanged;
+ }
+
+ ~IndicatorView() => _collectionChangedProxy.Unsubscribe();
///
/// Gets or sets the shape of the indicators.
@@ -237,11 +245,11 @@ static void UpdateIndicatorLayout(IndicatorView indicatorView, object newValue)
void ResetItemsSource(IEnumerable oldItemsSource)
{
- if (oldItemsSource is INotifyCollectionChanged oldCollection)
- oldCollection.CollectionChanged -= OnCollectionChanged;
+ if (oldItemsSource is INotifyCollectionChanged)
+ _collectionChangedProxy.Unsubscribe();
if (ItemsSource is INotifyCollectionChanged collection)
- collection.CollectionChanged += OnCollectionChanged;
+ _collectionChangedProxy.Subscribe(collection, _collectionChangedEventHandler);
OnCollectionChanged(ItemsSource, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
index dddd92d96824..8e12082917ab 100644
--- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt
@@ -17,6 +17,7 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnAttachedToWindow() -> void
override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnDetachedFromWindow() -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView.OnVisibilityChanged(Android.Views.View changedView, Android.Views.ViewStates visibility) -> void
override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnHiddenChanged(bool hidden) -> void
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
index 25c1b293624b..0cb5c7ce88b8 100644
--- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt
@@ -13,5 +13,7 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.NumberOfSections(UIKit.UICollectionView collectionView) -> nint
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
index 25c1b293624b..0cb5c7ce88b8 100644
--- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt
@@ -13,5 +13,7 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.NumberOfSections(UIKit.UICollectionView collectionView) -> nint
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
index 5dd193844cfa..2a202a66463f 100644
--- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt
@@ -3,5 +3,6 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
index e24d5a76d14f..0c3dcf905094 100644
--- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt
@@ -4,6 +4,7 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
override Microsoft.Maui.Controls.Handlers.Items.CarouselViewHandler.UpdateEmptyViewVisibility() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
index 5dd193844cfa..2a202a66463f 100644
--- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt
@@ -3,5 +3,6 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
index 5dd193844cfa..2a202a66463f 100644
--- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
+++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt
@@ -3,5 +3,6 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? property
~override Microsoft.Maui.Controls.RadioButton.OnPropertyChanged(string propertyName = null) -> void
override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void
override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void
+Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
diff --git a/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs b/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs
index f680aa089e91..611d2b6e4d15 100644
--- a/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs
+++ b/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs
@@ -1,4 +1,7 @@
+using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
using Xunit;
namespace Microsoft.Maui.Controls.Core.UnitTests
@@ -65,5 +68,23 @@ public void IndicatorLayout_ShouldBeRemovedWhenIndicatorTemplateIsNulled()
//Assert
Assert.Null(indicatorView.IndicatorLayout);
}
+
+ [Fact, Category(TestCategory.Memory)]
+ public async Task IndicatorViewItemsSourceDoesNotLeak()
+ {
+ // A long-lived collection - e.g. a ViewModel's items that is also bound to a
+ // CarouselView - must not keep every IndicatorView it was assigned to alive.
+ var sharedItemsSource = new ObservableCollection { "a", "b", "c" };
+
+ WeakReference weakIndicatorView;
+ {
+ var indicatorView = new IndicatorView();
+ indicatorView.ItemsSource = sharedItemsSource;
+ weakIndicatorView = new WeakReference(indicatorView);
+ }
+
+ Assert.False(await weakIndicatorView.WaitForCollect(), "IndicatorView should not be alive!");
+ GC.KeepAlive(sharedItemsSource);
+ }
}
}