From 98ff9ef8ae46149a65bbc92cfe883baff358bf27 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Wed, 20 May 2026 12:03:43 +0530 Subject: [PATCH 1/4] Fixed-35481 : SwipeView leaks when SwipeItems are reused or replaced --- src/Controls/src/Core/SwipeView/SwipeItems.cs | 59 ++++++++++++ src/Controls/src/Core/SwipeView/SwipeView.cs | 54 +++++------ .../tests/Core.UnitTests/SwipeViewTests.cs | 91 +++++++++++++++++++ 3 files changed, 171 insertions(+), 33 deletions(-) diff --git a/src/Controls/src/Core/SwipeView/SwipeItems.cs b/src/Controls/src/Core/SwipeView/SwipeItems.cs index ca5153a074f5..e4a64417b71d 100644 --- a/src/Controls/src/Core/SwipeView/SwipeItems.cs +++ b/src/Controls/src/Core/SwipeView/SwipeItems.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Microsoft.Extensions.Logging; @@ -16,6 +17,45 @@ public class SwipeItems : Element, IList, INotifyCollectionChanged { readonly ObservableCollection _swipeItems; + /// + /// Notifies the owning (resolved via ) + /// that something in this collection has changed so the handler can refresh the platform UI. + /// + /// + /// Using (rather than a captured-closure subscription on the + /// SwipeView side) avoids leaking the owning SwipeView when a SwipeItems instance is + /// cached/shared across multiple SwipeView instances. When SwipeItems is reassigned to a + /// new owner, AddLogicalChild reassigns so notifications + /// always go to the current owner. + /// + void NotifyOwner() + { + if (Parent is not SwipeView swipeView) + { + return; + } + + if (this == swipeView.LeftItems) + { + swipeView.Handler?.UpdateValue(nameof(SwipeView.LeftItems)); + } + + if (this == swipeView.RightItems) + { + swipeView.Handler?.UpdateValue(nameof(SwipeView.RightItems)); + } + + if (this == swipeView.TopItems) + { + swipeView.Handler?.UpdateValue(nameof(SwipeView.TopItems)); + } + + if (this == swipeView.BottomItems) + { + swipeView.Handler?.UpdateValue(nameof(SwipeView.BottomItems)); + } + } + /// /// Initializes a new instance of the class with the specified swipe items. /// @@ -31,6 +71,22 @@ public SwipeItems(IEnumerable swipeItems) _swipeItems = new ObservableCollection(swipeItems) ?? throw new ArgumentNullException(nameof(swipeItems)); _swipeItems.CollectionChanged += OnSwipeItemsChanged; + + // Self-subscribe to PropertyChanged so we can notify the owning SwipeView when + // Mode / SwipeBehaviorOnInvoked change. Using a self-subscription (rather than an + // OnPropertyChanged override) keeps the public API surface unchanged. The handler's + // Target is this same SwipeItems instance, so it cannot keep the object alive + // beyond its natural lifetime. + PropertyChanged += OnSelfPropertyChanged; + } + + void OnSelfPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == ModeProperty.PropertyName || + e.PropertyName == SwipeBehaviorOnInvokedProperty.PropertyName) + { + NotifyOwner(); + } } /// @@ -157,6 +213,9 @@ void OnSwipeItemsChanged(object sender, NotifyCollectionChangedEventArgs notifyC } CollectionChanged?.Invoke(this, notifyCollectionChangedEventArgs); + + // Notify the owning SwipeView so its handler can update the platform UI. + NotifyOwner(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Controls/src/Core/SwipeView/SwipeView.cs b/src/Controls/src/Core/SwipeView/SwipeView.cs index 18a27764e189..2277b9a9e9f3 100644 --- a/src/Controls/src/Core/SwipeView/SwipeView.cs +++ b/src/Controls/src/Core/SwipeView/SwipeView.cs @@ -163,44 +163,21 @@ static void OnSwipeItemsChanged(BindableObject bindable, object oldValue, object if (oldValue is SwipeItems oldItems) { - oldItems.CollectionChanged -= SwipeItemsCollectionChanged; - oldItems.PropertyChanged -= SwipeItemsPropertyChanged; swipeView.RemoveLogicalChild(oldItems); } if (newValue is SwipeItems newItems) { - newItems.CollectionChanged += SwipeItemsCollectionChanged; - newItems.PropertyChanged += SwipeItemsPropertyChanged; + // AddLogicalChild reassigns newItems.Parent to this SwipeView. When SwipeItems is + // cached/shared across multiple SwipeView instances, subsequent assignments + // reparent it away from the previous owner, so previous SwipeViews are not held + // alive through this collection. SwipeItems itself self-subscribes to its own + // CollectionChanged / PropertyChanged events and uses Parent (this SwipeView) to + // notify the handler — we intentionally do NOT add closure-based subscriptions + // here, since those captured `swipeView` and prevented GC when SwipeItems was + // shared across SwipeViews (issue #35481). swipeView.AddLogicalChild(newItems); } - - void SwipeItemsPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (sender is SwipeItems swipeItems) - SendChange(swipeItems); - } - - void SwipeItemsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) - { - if (sender is SwipeItems swipeItems) - SendChange(swipeItems); - } - - void SendChange(SwipeItems swipeItems) - { - if (swipeItems == swipeView.LeftItems) - swipeView?.Handler?.UpdateValue(nameof(LeftItems)); - - if (swipeItems == swipeView.RightItems) - swipeView?.Handler?.UpdateValue(nameof(RightItems)); - - if (swipeItems == swipeView.TopItems) - swipeView?.Handler?.UpdateValue(nameof(TopItems)); - - if (swipeItems == swipeView.BottomItems) - swipeView?.Handler?.UpdateValue(nameof(BottomItems)); - } } public event EventHandler SwipeStarted; @@ -295,13 +272,24 @@ bool ISwipeView.IsOpen protected override void OnChildAdded(Element child) { base.OnChildAdded(child); - child.PropertyChanged += OnPropertyChanged; + + // Skip SwipeItems children: they are logical children for visual-tree purposes only; + // subscribing to their PropertyChanged would create a strong reference from a + // potentially cached/long-lived SwipeItems back to this SwipeView. + if (child is not SwipeItems) + { + child.PropertyChanged += OnPropertyChanged; + } } protected override void OnChildRemoved(Element child, int oldLogicalIndex) { base.OnChildRemoved(child, oldLogicalIndex); - child.PropertyChanged -= OnPropertyChanged; + + if (child is not SwipeItems) + { + child.PropertyChanged -= OnPropertyChanged; + } } void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) diff --git a/src/Controls/tests/Core.UnitTests/SwipeViewTests.cs b/src/Controls/tests/Core.UnitTests/SwipeViewTests.cs index 19cf3979fcdf..9fc73a2acfe3 100644 --- a/src/Controls/tests/Core.UnitTests/SwipeViewTests.cs +++ b/src/Controls/tests/Core.UnitTests/SwipeViewTests.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform; using Xunit; @@ -566,5 +568,94 @@ public void SwipeViewRediscoversScrollParentWhenTemplateRootIsReparented() var scrollView2 = new ScrollView { Content = contentView }; Assert.Equal(scrollView2, GetPrivateField(swipeView, "_scrollParent")); } + + // Regression test for https://github.com/dotnet/maui/issues/35481 + // A SwipeItems instance that is cached (e.g. in a static dictionary) and shared + // across multiple SwipeViews must not keep prior SwipeView instances alive. + [Fact] + public void CachedSwipeItemsDoesNotKeepSwipeViewAlive() + { + // Simulate the repro: one long-lived SwipeItems cache shared across many SwipeViews. + var cachedSwipeItems = new SwipeItems { new SwipeItem { Text = "Delete" } }; + + // Allocate the SwipeViews in a non-inlined helper so the locals do not remain + // rooted on the test method's stack frame in Debug builds. + var swipeViewRefs = CreateSwipeViewsSharingCachedItems(cachedSwipeItems, 20); + + // Mutate the cached SwipeItems after the SwipeViews are no longer referenced. + // Before the fix, the CollectionChanged/PropertyChanged subscriptions on + // SwipeItems held a strong reference back to every SwipeView that had ever + // used this cache, so this mutation would keep them all alive. + cachedSwipeItems.Add(new SwipeItem { Text = "Archive" }); + + ForceFullGC(); + + GC.KeepAlive(cachedSwipeItems); + + Assert.All(swipeViewRefs, r => Assert.False(r.IsAlive, + "SwipeView was kept alive by the cached SwipeItems — issue #35481 regression.")); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + static List CreateSwipeViewsSharingCachedItems(SwipeItems shared, int count) + { + var refs = new List(count); + for (int i = 0; i < count; i++) + { + var sv = new SwipeView { RightItems = shared }; + // Simulate a CollectionView recycling the row: the SwipeView reassigns its + // RightItems before going out of scope, releasing the Parent back-reference + // from the cached SwipeItems. Before the fix, the closure-based + // CollectionChanged/PropertyChanged subscriptions on the cached SwipeItems + // still held this SwipeView alive even after this reassignment. + sv.RightItems = new SwipeItems(); + refs.Add(new WeakReference(sv)); + } + return refs; + } + + // Regression test for https://github.com/dotnet/maui/issues/35481 + // Replacing RightItems with a new SwipeItems instance must release any back-reference + // from the previous (cached) SwipeItems to the SwipeView. Without the fix, the cached + // SwipeItems' CollectionChanged/PropertyChanged delegates kept the SwipeView alive + // even after it was logically unhooked. + [Fact] + public void ReplacingCachedSwipeItemsReleasesPreviousOwnerReference() + { + // Cache: held by external code (the user's static dictionary in the repro). + var cachedRightItems = new SwipeItems { new SwipeItem { Text = "Done" } }; + + var swipeViewRef = CreateSwipeViewAssignThenReplace(cachedRightItems); + + // Mutate the cached SwipeItems after the SwipeView is unhooked, just like the repro. + cachedRightItems.Add(new SwipeItem { Text = "Archive" }); + + ForceFullGC(); + + GC.KeepAlive(cachedRightItems); + + Assert.False(swipeViewRef.IsAlive, + "Replaced SwipeView was kept alive by its previously-assigned cached SwipeItems — issue #35481 regression."); + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + static WeakReference CreateSwipeViewAssignThenReplace(SwipeItems cachedRightItems) + { + var sv = new SwipeView { RightItems = cachedRightItems }; + // Replace the assignment — the previous (cached) SwipeItems is now logically + // unhooked but kept alive by the caller. It must not retain the SwipeView. + sv.RightItems = new SwipeItems { new SwipeItem { Text = "Replaced" } }; + return new WeakReference(sv); + } + + static void ForceFullGC() + { + for (int i = 0; i < 5; i++) + { + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true); + GC.WaitForPendingFinalizers(); + GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true, compacting: true); + } + } } } From 82d1b34cc739a23a2a1e12ea0445b4293d0525af Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Wed, 20 May 2026 12:59:20 +0530 Subject: [PATCH 2/4] Update SwipeItems.cs --- .../net-android/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 1 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 1 + .../net-tizen/PublicAPI.Unshipped.txt | 1 + .../net-windows/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + .../netstandard/PublicAPI.Unshipped.txt | 1 + src/Controls/src/Core/SwipeView/SwipeItems.cs | 20 +++++++++---------- 8 files changed, 16 insertions(+), 11 deletions(-) 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 d7e793dd6c1c..ac7b86c3be1e 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH ~override Microsoft.Maui.Controls.Handlers.Items.RecyclerViewScrollListener.OnScrollStateChanged(AndroidX.RecyclerView.Widget.RecyclerView recyclerView, int newState) -> void ~override Microsoft.Maui.Controls.Handlers.Items.SelectableItemsViewAdapter.IsSelectionEnabled(Android.Views.ViewGroup parent, int viewType) -> bool override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.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 d30e929de861..e49d162cf312 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 d30e929de861..e49d162cf312 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 c88246042d15..9c0d236e2414 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -3,3 +3,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/SwipeView/SwipeItems.cs b/src/Controls/src/Core/SwipeView/SwipeItems.cs index e4a64417b71d..71ecbe0d4915 100644 --- a/src/Controls/src/Core/SwipeView/SwipeItems.cs +++ b/src/Controls/src/Core/SwipeView/SwipeItems.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; using Microsoft.Extensions.Logging; @@ -71,19 +70,18 @@ public SwipeItems(IEnumerable swipeItems) _swipeItems = new ObservableCollection(swipeItems) ?? throw new ArgumentNullException(nameof(swipeItems)); _swipeItems.CollectionChanged += OnSwipeItemsChanged; - - // Self-subscribe to PropertyChanged so we can notify the owning SwipeView when - // Mode / SwipeBehaviorOnInvoked change. Using a self-subscription (rather than an - // OnPropertyChanged override) keeps the public API surface unchanged. The handler's - // Target is this same SwipeItems instance, so it cannot keep the object alive - // beyond its natural lifetime. - PropertyChanged += OnSelfPropertyChanged; } - void OnSelfPropertyChanged(object sender, PropertyChangedEventArgs e) + // Override OnPropertyChanged so we can notify the owning SwipeView when Mode / + // SwipeBehaviorOnInvoked change. The notification goes through Parent (a weak + // reference managed by AddLogicalChild), so the owning SwipeView is never rooted + // by this SwipeItems instance (issue #35481). + protected override void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) { - if (e.PropertyName == ModeProperty.PropertyName || - e.PropertyName == SwipeBehaviorOnInvokedProperty.PropertyName) + base.OnPropertyChanged(propertyName); + + if (propertyName == ModeProperty.PropertyName || + propertyName == SwipeBehaviorOnInvokedProperty.PropertyName) { NotifyOwner(); } From bead39dbfc34cb047af96be5289e949ea3aaa514 Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Wed, 20 May 2026 15:42:07 +0530 Subject: [PATCH 3/4] Update PropertyChanged. --- .../net-android/PublicAPI.Unshipped.txt | 1 - .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 1 - .../net-maccatalyst/PublicAPI.Unshipped.txt | 1 - .../net-tizen/PublicAPI.Unshipped.txt | 1 - .../net-windows/PublicAPI.Unshipped.txt | 1 - .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 - .../netstandard/PublicAPI.Unshipped.txt | 1 - src/Controls/src/Core/SwipeView/SwipeItems.cs | 23 +++++++++---------- 8 files changed, 11 insertions(+), 19 deletions(-) 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 ac7b86c3be1e..d7e793dd6c1c 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -11,4 +11,3 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH ~override Microsoft.Maui.Controls.Handlers.Items.RecyclerViewScrollListener.OnScrollStateChanged(AndroidX.RecyclerView.Widget.RecyclerView recyclerView, int newState) -> void ~override Microsoft.Maui.Controls.Handlers.Items.SelectableItemsViewAdapter.IsSelectionEnabled(Android.Views.ViewGroup parent, int viewType) -> bool override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void -~override Microsoft.Maui.Controls.SwipeItems.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 e49d162cf312..d30e929de861 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -11,4 +11,3 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 e49d162cf312..d30e929de861 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -11,4 +11,3 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 5e48695eac5b..d6335d8b78ac 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -2,4 +2,3 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 9c0d236e2414..c88246042d15 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -3,4 +3,3 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index 5e48695eac5b..d6335d8b78ac 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -2,4 +2,3 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 5e48695eac5b..d6335d8b78ac 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -2,4 +2,3 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void -~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/SwipeView/SwipeItems.cs b/src/Controls/src/Core/SwipeView/SwipeItems.cs index 71ecbe0d4915..1d5c18118abd 100644 --- a/src/Controls/src/Core/SwipeView/SwipeItems.cs +++ b/src/Controls/src/Core/SwipeView/SwipeItems.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Microsoft.Extensions.Logging; @@ -70,21 +71,19 @@ public SwipeItems(IEnumerable swipeItems) _swipeItems = new ObservableCollection(swipeItems) ?? throw new ArgumentNullException(nameof(swipeItems)); _swipeItems.CollectionChanged += OnSwipeItemsChanged; + + // Self-subscribe to PropertyChanged so we can notify the owning SwipeView when + // any of this SwipeItems' properties change. The handler's Target is this same + // SwipeItems instance, so the subscription cannot keep the SwipeItems alive + // beyond its natural lifetime, and it does not root the owning SwipeView + // either (the SwipeView is located on demand via Element.Parent, which is a + // WeakReference internally — see issue #35481). + PropertyChanged += OnSelfPropertyChanged; } - // Override OnPropertyChanged so we can notify the owning SwipeView when Mode / - // SwipeBehaviorOnInvoked change. The notification goes through Parent (a weak - // reference managed by AddLogicalChild), so the owning SwipeView is never rooted - // by this SwipeItems instance (issue #35481). - protected override void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) + void OnSelfPropertyChanged(object sender, PropertyChangedEventArgs e) { - base.OnPropertyChanged(propertyName); - - if (propertyName == ModeProperty.PropertyName || - propertyName == SwipeBehaviorOnInvokedProperty.PropertyName) - { - NotifyOwner(); - } + NotifyOwner(); } /// From 3b6e1e005b616127a53dabdc124f2a67ccd3b01c Mon Sep 17 00:00:00 2001 From: Vignesh-SF3580 <102575140+Vignesh-SF3580@users.noreply.github.com> Date: Thu, 21 May 2026 11:52:28 +0530 Subject: [PATCH 4/4] Addressed concerns. --- .../net-android/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 1 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 1 + .../net-tizen/PublicAPI.Unshipped.txt | 1 + .../net-windows/PublicAPI.Unshipped.txt | 1 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + .../netstandard/PublicAPI.Unshipped.txt | 1 + src/Controls/src/Core/SwipeView/SwipeItems.cs | 33 +++++++++---------- 8 files changed, 23 insertions(+), 17 deletions(-) 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 d7e793dd6c1c..ac7b86c3be1e 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.OnH ~override Microsoft.Maui.Controls.Handlers.Items.RecyclerViewScrollListener.OnScrollStateChanged(AndroidX.RecyclerView.Widget.RecyclerView recyclerView, int newState) -> void ~override Microsoft.Maui.Controls.Handlers.Items.SelectableItemsViewAdapter.IsSelectionEnabled(Android.Views.ViewGroup parent, int viewType) -> bool override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.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 d30e929de861..e49d162cf312 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 d30e929de861..e49d162cf312 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -11,3 +11,4 @@ override Microsoft.Maui.Controls.Platform.Compatibility.ShellItemRenderer.ViewDi ~override Microsoft.Maui.Controls.Platform.Compatibility.ShellSectionRenderer.DidMoveToParentViewController(UIKit.UIViewController parent) -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.Handlers.Items2.StructuredItemsViewController2.UpdateFlowDirection() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-tizen/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> 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 c88246042d15..9c0d236e2414 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -3,3 +3,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt index d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt index d6335d8b78ac..5e48695eac5b 100644 --- a/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -2,3 +2,4 @@ override Microsoft.Maui.Controls.Shapes.Shape.OnPropertyChanged(string? propertyName = null) -> void override Microsoft.Maui.Controls.GraphicsView.OnBindingContextChanged() -> void override Microsoft.Maui.Controls.TitleBar.OnBindingContextChanged() -> void +~override Microsoft.Maui.Controls.SwipeItems.OnPropertyChanged(string propertyName = null) -> void diff --git a/src/Controls/src/Core/SwipeView/SwipeItems.cs b/src/Controls/src/Core/SwipeView/SwipeItems.cs index 1d5c18118abd..066ad57b1d2d 100644 --- a/src/Controls/src/Core/SwipeView/SwipeItems.cs +++ b/src/Controls/src/Core/SwipeView/SwipeItems.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; using Microsoft.Extensions.Logging; @@ -39,18 +38,15 @@ void NotifyOwner() { swipeView.Handler?.UpdateValue(nameof(SwipeView.LeftItems)); } - - if (this == swipeView.RightItems) + else if (this == swipeView.RightItems) { swipeView.Handler?.UpdateValue(nameof(SwipeView.RightItems)); } - - if (this == swipeView.TopItems) + else if (this == swipeView.TopItems) { swipeView.Handler?.UpdateValue(nameof(SwipeView.TopItems)); } - - if (this == swipeView.BottomItems) + else if (this == swipeView.BottomItems) { swipeView.Handler?.UpdateValue(nameof(SwipeView.BottomItems)); } @@ -71,19 +67,22 @@ public SwipeItems(IEnumerable swipeItems) _swipeItems = new ObservableCollection(swipeItems) ?? throw new ArgumentNullException(nameof(swipeItems)); _swipeItems.CollectionChanged += OnSwipeItemsChanged; - - // Self-subscribe to PropertyChanged so we can notify the owning SwipeView when - // any of this SwipeItems' properties change. The handler's Target is this same - // SwipeItems instance, so the subscription cannot keep the SwipeItems alive - // beyond its natural lifetime, and it does not root the owning SwipeView - // either (the SwipeView is located on demand via Element.Parent, which is a - // WeakReference internally — see issue #35481). - PropertyChanged += OnSelfPropertyChanged; } - void OnSelfPropertyChanged(object sender, PropertyChangedEventArgs e) + // Override OnPropertyChanged so we can notify the owning SwipeView when Mode / + // SwipeBehaviorOnInvoked change. Filtering by property name avoids a Handler.UpdateValue + // storm on every base-Element property change (Parent, BindingContext, Style, etc.). + // The notification goes through Parent (a weak reference managed by AddLogicalChild), + // so the owning SwipeView is never rooted by this SwipeItems instance (issue #35481). + protected override void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) { - NotifyOwner(); + base.OnPropertyChanged(propertyName); + + if (propertyName == ModeProperty.PropertyName || + propertyName == SwipeBehaviorOnInvokedProperty.PropertyName) + { + NotifyOwner(); + } } ///