Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH
override Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView<TItemsView, TAdapter, TItemsViewSource>.OnSizeChanged(int w, int h, int oldw, int oldh) -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2<
Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
override Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView.LayoutSubviews() -> void
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2<
Microsoft.Maui.Controls.IndicatorView.~IndicatorView() -> void
~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void
Microsoft.Maui.Controls.Label.~Label() -> void
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
override Microsoft.Maui.Controls.Handlers.Items.MauiCollectionView.LayoutSubviews() -> void
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.TraitCollectionDidChange(UIKit.UITraitCollection previousTraitCollection) -> void
~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.ViewWillTransitionToSize(CoreGraphics.CGSize toSize, UIKit.IUIViewControllerTransitionCoordinator coordinator) -> void
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ 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
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ 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
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ 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
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ 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
Microsoft.Maui.Controls.SwipeView.~SwipeView() -> void
98 changes: 81 additions & 17 deletions src/Controls/src/Core/SwipeView/SwipeView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ public IPlatformElementConfiguration<T, SwipeView> On<T>() where T : IConfigPlat
View? _scrollParent;
Element? _templateParent;
SwipeDirection? _swipeDirection;
WeakScrollParentProxy? _scrolledProxy;

~SwipeView()
Comment thread
kubaflo marked this conversation as resolved.
{
_scrolledProxy?.Unsubscribe();
}

ISwipeItems ISwipeView.LeftItems => new HandlerSwipeItems(LeftItems);

Expand Down Expand Up @@ -323,20 +329,7 @@ private protected override void OnParentChangedCore()

void UnsubscribeFromParentScrolledEvents()
{
if (_scrollParent is ScrollView scrollView)
{
scrollView.Scrolled -= OnParentScrolled;
}
#pragma warning disable CS0618 // Type or member is obsolete
else if (_scrollParent is ListView listView)
{
listView.Scrolled -= OnParentScrolled;
}
#pragma warning restore CS0618 // Type or member is obsolete
else if (_scrollParent is CollectionView collectionView)
{
collectionView.Scrolled -= OnParentScrolled;
}
_scrolledProxy?.Unsubscribe();
_scrollParent = null;
}

Expand All @@ -356,7 +349,7 @@ bool SubscribeToNearestScrollParent(Element startElement)

if (_scrollParent is ScrollView scrollView)
{
scrollView.Scrolled += OnParentScrolled;
EnsureScrolledProxy().Subscribe(this, scrollView);
return true;
}

Expand All @@ -365,7 +358,7 @@ bool SubscribeToNearestScrollParent(Element startElement)

if (_scrollParent is ListView listView)
{
listView.Scrolled += OnParentScrolled;
EnsureScrolledProxy().Subscribe(this, listView);
return true;
}
#pragma warning restore CS0618 // Type or member is obsolete
Expand All @@ -374,13 +367,15 @@ bool SubscribeToNearestScrollParent(Element startElement)

if (_scrollParent is Microsoft.Maui.Controls.CollectionView collectionView)
{
collectionView.Scrolled += OnParentScrolled;
EnsureScrolledProxy().Subscribe(this, collectionView);
return true;
}

return false;
}

WeakScrollParentProxy EnsureScrolledProxy() => _scrolledProxy ??= new WeakScrollParentProxy();

void UpdateMargin()
{
if (this is not ISwipeView swipeView)
Expand Down Expand Up @@ -408,6 +403,75 @@ void OnParentScrolled(object? sender, ItemsViewScrolledEventArgs e)
((ISwipeView)this).RequestClose(new SwipeViewCloseRequest(true));
}

// Subscribes to an ancestor scroll container's Scrolled event via a WeakReference back to the
// SwipeView, so a long-lived scroll parent does not root a SwipeView that has been detached from
// the visual tree without its direct Parent changing (see issue #36481).
sealed class WeakScrollParentProxy
{
WeakReference<SwipeView>? _swipeView;
View? _source;

public void Subscribe(SwipeView swipeView, View source)
{
Unsubscribe();

_swipeView = new WeakReference<SwipeView>(swipeView);
_source = source;

switch (source)
{
case ScrollView scrollView:
scrollView.Scrolled += OnScrolled;
break;
#pragma warning disable CS0618 // Type or member is obsolete
case ListView listView:
listView.Scrolled += OnScrolled;
break;
#pragma warning restore CS0618 // Type or member is obsolete
case CollectionView collectionView:
collectionView.Scrolled += OnItemsViewScrolled;
break;
}
}

public void Unsubscribe()
{
switch (_source)
{
case ScrollView scrollView:
scrollView.Scrolled -= OnScrolled;
break;
#pragma warning disable CS0618 // Type or member is obsolete
case ListView listView:
listView.Scrolled -= OnScrolled;
break;
#pragma warning restore CS0618 // Type or member is obsolete
case CollectionView collectionView:
collectionView.Scrolled -= OnItemsViewScrolled;
break;
}

_source = null;
_swipeView = null;
}

void OnScrolled(object? sender, ScrolledEventArgs e)
{
if (_swipeView is not null && _swipeView.TryGetTarget(out var swipeView))
swipeView.OnParentScrolled(sender, e);
else
Unsubscribe();
}

void OnItemsViewScrolled(object? sender, ItemsViewScrolledEventArgs e)
{
if (_swipeView is not null && _swipeView.TryGetTarget(out var swipeView))
swipeView.OnParentScrolled(sender, e);
else
Unsubscribe();
}
}

void ISwipeView.SwipeStarted(SwipeViewSwipeStarted swipeStarted)
{
_swipeDirection = swipeStarted.SwipeDirection;
Expand Down
45 changes: 45 additions & 0 deletions src/Controls/tests/Core.UnitTests/SwipeViewMemoryLeakTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Maui.Controls.Core.UnitTests
{
public class SwipeViewMemoryLeakTests : BaseTestFixture
{
/// <summary>
/// Verifies that a <see cref="SwipeView"/> placed beneath a long-lived <see cref="ScrollView"/>
/// does not leak after it is detached from the scroll container by removing an intermediate
/// ancestor (which leaves the SwipeView's direct parent unchanged). Reproduces issue #36481:
/// the ancestor <c>Scrolled</c> subscription was a plain (non-weak) delegate, so the long-lived
/// ScrollView permanently rooted the detached SwipeView and its subtree.
/// </summary>
[Fact, Category(TestCategory.Memory)]
public async Task SwipeViewDoesNotLeakWhenAncestorScrollViewOutlivesIt()
{
// The ScrollView is the long-lived root that outlives the SwipeView.
var scroll = new ScrollView();

WeakReference CreateSwipeViewReference()
{
var inner = new VerticalStackLayout();
scroll.Content = inner;

var swipe = new SwipeView { Content = new Label() };
inner.Children.Add(swipe); // subscribes to scroll.Scrolled

// Detach the intermediate ancestor: swipe.Parent stays 'inner', so the
// direct-parent-change teardown never runs.
scroll.Content = null;

return new WeakReference(swipe);
}

var reference = CreateSwipeViewReference();

Assert.False(await reference.WaitForCollect(), "SwipeView should not be alive!");

// Keep the long-lived ScrollView alive for the duration of the test.
GC.KeepAlive(scroll);
}
}
}
Loading