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); + } } }