[iOS] Clear BindingContext when cell is queued for reuse (#14619)
* Clear BindingContext when cell is queued for reuse This avoid holding references to objects that were already removed. * Auto-format source code * Update src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt to list PrepareForReuse override. * Empty commit * Apply code review feedback * Update public API for maccatalyst * Add test that shows the fix is not fully working yet * [iOS] Unbind TemplatedCell when it goes out of display and the bound item is no longer present in items source. Extend the test to check rebinding after clearing the source. * Relax the test to make it pass on Android. * Update src/Controls/src/Core/Handlers/Items/iOS/ItemsViewDelegator.cs Co-authored-by: Rui Marinho <me@ruimarinho.net> * Use Equals to compare item list elements * Auto-format source code * PR feedback --------- Co-authored-by: GitHub Actions Autoformatter <autoformat@example.com> Co-authored-by: Rui Marinho <me@ruimarinho.net>
This commit is contained in:
Родитель
b43f2650b9
Коммит
170a7a9832
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -397,5 +397,68 @@ namespace Microsoft.Maui.DeviceTests
|
|||
timeout -= interval;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClearingItemsSourceClearsBindingContext()
|
||||
{
|
||||
SetupBuilder();
|
||||
|
||||
IReadOnlyList<Element> logicalChildren = null;
|
||||
var collectionView = new CollectionView
|
||||
{
|
||||
ItemTemplate = new DataTemplate(() => new Label() { HeightRequest = 30, WidthRequest = 200 }),
|
||||
WidthRequest = 200,
|
||||
HeightRequest = 200,
|
||||
};
|
||||
|
||||
await CreateHandlerAndAddToWindow<CollectionViewHandler>(collectionView, async handler =>
|
||||
{
|
||||
var data = new ObservableCollection<MyRecord>()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче