Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -154,6 +154,13 @@ protected override void Dispose(bool disposing)
tabbed.PagesChanged -= OnPagesChanged;
}

if (_currentBarBackground is GradientBrush currentGradientBrush)
{
currentGradientBrush.Parent = null;
currentGradientBrush.InvalidateGradientBrushRequested -= OnBarBackgroundChanged;
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
}
_currentBarBackground = null;

FinishedCustomizingViewControllers -= HandleFinishedCustomizingViewControllers;
}

Expand Down
7 changes: 7 additions & 0 deletions src/Controls/src/Core/Platform/Android/TabbedPageManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ public virtual void SetElement(TabbedPage tabbedPage)

_viewPager.LayoutChange -= OnLayoutChanged;
_viewPager.Adapter = null;

if (_currentBarBackground is GradientBrush currentGradientBrush)
{
currentGradientBrush.Parent = null;
currentGradientBrush.InvalidateGradientBrushRequested -= OnBarBackgroundChanged;
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
}
_currentBarBackground = null;
Comment thread
SubhikshaSf4851 marked this conversation as resolved.
}

Element = tabbedPage;
Expand Down
113 changes: 113 additions & 0 deletions src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -503,5 +504,117 @@ TabbedPage CreateBasicTabbedPage(bool bottomTabs = false, bool isSmoothScrollEna
Controls.PlatformConfiguration.AndroidSpecific.TabbedPage.SetIsSmoothScrollEnabled(tabs, isSmoothScrollEnabled);
return tabs;
}

// https://github.com/dotnet/maui/issues/35469
// When a TabbedPage.BarBackground is set from a shared GradientBrush (e.g. via an app-resource Style),
// removing the TabbedPage from the window must not leave a live subscription on the shared brush.
// The renderer/manager subscribes to GradientBrush.InvalidateGradientBrushRequested; if it fails to
// unsubscribe on disconnect the brush keeps the renderer alive, preventing GC.
[Fact(DisplayName = "TabbedPage renderer/manager does not leak shared GradientBrush subscriber on disconnect")]
public async Task TabbedPageDoesNotLeakGradientBrushSubscriberOnDisconnect()
{
SetupBuilder();

// Simulate a shared app-resource brush — this object lives beyond the TabbedPage.
var sharedBrush = new LinearGradientBrush(
new GradientStopCollection
{
new GradientStop(Colors.Teal, 0f),
new GradientStop(Colors.CornflowerBlue, 1f)
},
new Graphics.Point(0, 0),
new Graphics.Point(1, 1));

var tabbedPageRef = new WeakReference(null);
var handlerRef = new WeakReference(null);

var navPage = new NavigationPage(new ContentPage { Title = "Home" });

await CreateHandlerAndAddToWindow(new Window(navPage), async () =>
{
var tabbedPage = new TabbedPage
{
Title = "Tabbed",
BarBackground = sharedBrush,
};
tabbedPage.Children.Add(new ContentPage { Title = "Tab 1" });
tabbedPage.Children.Add(new ContentPage { Title = "Tab 2" });

// Push so the TabbedPage handler is created and subscribes to the brush.
await navPage.Navigation.PushModalAsync(tabbedPage);
await OnLoadedAsync(tabbedPage.Children[0]);

tabbedPageRef.Target = tabbedPage;
handlerRef.Target = tabbedPage.Handler;

// Pop — this should cause the renderer/manager to disconnect and unsubscribe.
await navPage.Navigation.PopModalAsync();
});

// After full GC the renderer/manager should be collected.
await AssertionExtensions.WaitForGC(handlerRef);

// The shared brush must have zero subscribers to InvalidateGradientBrushRequested.
// If the renderer/manager leaked, it would still be subscribed here.
var brushInvocationList = GetGradientBrushInvocationList(sharedBrush);
Assert.Empty(brushInvocationList);
}

// https://github.com/dotnet/maui/issues/35469
// Variant: BarBackground applied via a Style (the exact scenario from the bug report).
[Fact(DisplayName = "TabbedPage renderer/manager does not leak shared GradientBrush subscriber when BarBackground is set via Style")]
public async Task TabbedPageDoesNotLeakGradientBrushSubscriberWhenSetViaStyle()
{
SetupBuilder();

var sharedBrush = new LinearGradientBrush(
new GradientStopCollection
{
new GradientStop(Colors.DarkGreen, 0f),
new GradientStop(Colors.SteelBlue, 1f)
},
new Graphics.Point(0, 0),
new Graphics.Point(1, 1));

var barBackgroundStyle = new Style(typeof(TabbedPage))
{
Setters = { new Setter { Property = TabbedPage.BarBackgroundProperty, Value = sharedBrush } }
};

var handlerRef = new WeakReference(null);

var navPage = new NavigationPage(new ContentPage { Title = "Home" });

await CreateHandlerAndAddToWindow(new Window(navPage), async () =>
{
var tabbedPage = new TabbedPage { Style = barBackgroundStyle };
tabbedPage.Children.Add(new ContentPage { Title = "Tab 1" });
tabbedPage.Children.Add(new ContentPage { Title = "Tab 2" });

await navPage.Navigation.PushModalAsync(tabbedPage);
await OnLoadedAsync(tabbedPage.Children[0]);

handlerRef.Target = tabbedPage.Handler;

await navPage.Navigation.PopModalAsync();
});

await AssertionExtensions.WaitForGC(handlerRef);

var brushInvocationList = GetGradientBrushInvocationList(sharedBrush);
Assert.Empty(brushInvocationList);
}

static System.Delegate[] GetGradientBrushInvocationList(GradientBrush brush)
{
var field = typeof(GradientBrush).GetField(
"InvalidateGradientBrushRequested",
BindingFlags.Instance | BindingFlags.NonPublic);

if (field is null)
return [];

return (field.GetValue(brush) as MulticastDelegate)?.GetInvocationList() ?? [];
}
}
}
Loading