diff --git a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs index a4888f30e..3716b1b87 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs @@ -108,6 +108,22 @@ namespace Microsoft.Maui.Controls.Handlers.Items public override void CellDisplayingEnded(UICollectionView collectionView, UICollectionViewCell cell, NSIndexPath indexPath) { + if (cell is TemplatedCell templatedCell && + (templatedCell.PlatformHandler?.VirtualView as View)?.BindingContext is object bindingContext) + { + // We want to unbind a cell that is no longer present in the items source. Unfortunately + // it's too expensive to check directly, so let's check that the current binding context + // matches the item at a given position. + + var itemsSource = ViewController?.ItemsSource; + if (itemsSource is null || + !itemsSource.IsIndexPathValid(indexPath) || + !Equals(itemsSource[indexPath], bindingContext)) + { + templatedCell.Unbind(); + } + } + if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal) { var actualWidth = collectionView.ContentSize.Width - collectionView.Bounds.Size.Width; diff --git a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs index 2b6e48119..f4945af4e 100644 --- a/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs +++ b/src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs @@ -58,6 +58,15 @@ namespace Microsoft.Maui.Controls.Handlers.Items ConstrainedDimension = default; } + internal void Unbind() + { + if (PlatformHandler?.VirtualView is View view) + { + view.MeasureInvalidated -= MeasureInvalidated; + view.BindingContext = null; + } + } + public override UICollectionViewLayoutAttributes PreferredLayoutAttributesFittingAttributes( UICollectionViewLayoutAttributes layoutAttributes) { @@ -116,6 +125,12 @@ namespace Microsoft.Maui.Controls.Handlers.Items _size = rectangle.Size; } + public override void PrepareForReuse() + { + Unbind(); + base.PrepareForReuse(); + } + public void Bind(DataTemplate template, object bindingContext, ItemsView itemsView) { var oldElement = PlatformHandler?.VirtualView as View; @@ -157,18 +172,10 @@ namespace Microsoft.Maui.Controls.Handlers.Items // Same template if (oldElement != null) { - if (oldElement.BindingContext == null || !(oldElement.BindingContext.Equals(bindingContext))) - { - // If the data is different, update it + oldElement.BindingContext = bindingContext; + oldElement.MeasureInvalidated += MeasureInvalidated; - // Unhook the MeasureInvalidated handler, otherwise it'll fire for every invalidation during the - // BindingContext change - oldElement.MeasureInvalidated -= MeasureInvalidated; - oldElement.BindingContext = bindingContext; - oldElement.MeasureInvalidated += MeasureInvalidated; - - UpdateCellSize(); - } + UpdateCellSize(); } } 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 f6e67f3c4..11676eeed 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -106,6 +106,7 @@ Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> b Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void +override Microsoft.Maui.Controls.Handlers.Items.TemplatedCell.PrepareForReuse() -> void override Microsoft.Maui.Controls.Handlers.Compatibility.PhoneFlyoutPageRenderer.ViewWillLayoutSubviews() -> void override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> 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 ba4790c4e..b46d72235 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -105,6 +105,7 @@ Microsoft.Maui.Controls.Region.Equals(Microsoft.Maui.Controls.Region other) -> b Microsoft.Maui.Controls.Shapes.Matrix.Equals(Microsoft.Maui.Controls.Shapes.Matrix other) -> bool Microsoft.Maui.Controls.Shapes.Shape.~Shape() -> void Microsoft.Maui.Controls.VisualElement.~VisualElement() -> void +override Microsoft.Maui.Controls.Handlers.Items.TemplatedCell.PrepareForReuse() -> void override Microsoft.Maui.Controls.Handlers.Compatibility.PhoneFlyoutPageRenderer.ViewWillLayoutSubviews() -> void override Microsoft.Maui.Controls.LayoutOptions.GetHashCode() -> int override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> void diff --git a/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.cs b/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.cs index 63829158b..1789a67ae 100644 --- a/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/CollectionView/CollectionViewTests.cs @@ -397,5 +397,68 @@ namespace Microsoft.Maui.DeviceTests timeout -= interval; } } + + [Fact] + public async Task ClearingItemsSourceClearsBindingContext() + { + SetupBuilder(); + + IReadOnlyList logicalChildren = null; + var collectionView = new CollectionView + { + ItemTemplate = new DataTemplate(() => new Label() { HeightRequest = 30, WidthRequest = 200 }), + WidthRequest = 200, + HeightRequest = 200, + }; + + await CreateHandlerAndAddToWindow(collectionView, async handler => + { + var data = new ObservableCollection() + { + new MyRecord("Item 1"), + new MyRecord("Item 2"), + new MyRecord("Item 3"), + }; + collectionView.ItemsSource = data; + await Task.Delay(100); + + logicalChildren = collectionView.LogicalChildrenInternal; + Assert.NotNull(logicalChildren); + Assert.True(logicalChildren.Count == 3); + + // Clear collection + var savedItems = data.ToArray(); + data.Clear(); + + await Task.Delay(100); + + // Check that all logical children have no binding context + foreach (var logicalChild in logicalChildren) + { + Assert.Null(logicalChild.BindingContext); + } + + // Re-add the old children + foreach (var savedItem in savedItems) + { + data.Add(savedItem); + } + + await Task.Delay(100); + + // Check that the right number of logical children have binding context again + int boundChildren = 0; + foreach (var logicalChild in logicalChildren) + { + if (logicalChild.BindingContext is not null) + { + boundChildren++; + } + } + Assert.Equal(3, boundChildren); + }); + } + + record MyRecord(string Name); } }