Repository navigation
GradientBrush.GradientStops.Clear() leaks brushes when stops are shared #36743
Copy link
Copy link
Closed
Labels
area-drawingShapes, Borders, Shadows, Graphics, BoxView, custom drawingShapes, Borders, Shadows, Graphics, BoxView, custom drawingpartner/syncfusionIssues / PR's with Syncfusion collaborationIssues / PR's with Syncfusion collaborationplatform/androidplatform/iosplatform/macosmacOS / Mac CatalystmacOS / Mac Catalystplatform/windowss/triagedIssue has been reviewedIssue has been revieweds/verifiedVerified / Reproducible Issue ready for Engineering TriageVerified / Reproducible Issue ready for Engineering Triaget/bugSomething isn't workingSomething isn't working
Milestone
Description
Description
GradientBrush subscribes to PropertyChanged on each GradientStop. When Clear() is called, ObservableCollection fires a Reset notification with e.OldItems == null. The old handler only unsubscribed from e.OldItems, so shared stops were never unsubscribed — keeping brushes alive indefinitely.
Steps to Reproduce
1.Add the below code in MainPage.xaml and MainPage.xaml.cs
MainPage.xaml
<VerticalStackLayout Padding="20"
Spacing="12">
<Label Text="GradientBrush.GradientStops.Clear() Leak Repro"
FontSize="18"
FontAttributes="Bold"/>
<Label Text="A shared GradientStop keeps the GradientBrush alive after Clear() because the CollectionChanged Reset action leaves e.OldItems null, so the PropertyChanged subscription is never removed."
FontSize="13"
TextColor="Gray"/>
<Button Text="1. Create brushes using shared stops"
Clicked="OnCreateClicked"
AutomationId="CreateButton"/>
<Button Text="2. Clear() GradientStops and release"
Clicked="OnClearAndReleaseClicked"
AutomationId="ClearButton"/>
<Button Text="3. Force GC and check leaks"
Clicked="OnCheckLeaksClicked"
AutomationId="CheckButton"/>
<Label x:Name="StatusLabel"
AutomationId="StatusLabel"
FontSize="15"
TextColor="DarkBlue"/>
<BoxView x:Name="PreviewBox"
HeightRequest="60"
CornerRadius="8"/>
</VerticalStackLayout>
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
// Shared GradientStops kept alive externally — simulates a shared resource
// (e.g. a style resource or static geometry reused across pages).
readonly GradientStop _sharedStop1 = new GradientStop(Colors.DeepSkyBlue, 0f);
readonly GradientStop _sharedStop2 = new GradientStop(Colors.MediumVioletRed, 1f);
const int BrushCount = 50;
// Weak references so we can observe whether the brushes and boxes are collected.
List<WeakReference> _brushRefs = new();
List<WeakReference> _boxRefs = new();
// Strong references released on step 2.
List<LinearGradientBrush> _brushes = new();
List<BoxView> _boxes = new();
public MainPage()
{
InitializeComponent();
UpdateStatus("Step 1: Press \"Create\" to allocate brushes.");
}
void OnCreateClicked(object sender, EventArgs e)
{
_brushes.Clear();
_boxes.Clear();
_brushRefs.Clear();
_boxRefs.Clear();
for (int i = 0; i < BrushCount; i++)
{
var brush = new LinearGradientBrush
{
StartPoint = new Point(0, 0),
EndPoint = new Point(1, 1),
};
// Add the shared stops — subscribes to their PropertyChanged.
brush.GradientStops.Add(_sharedStop1);
brush.GradientStops.Add(_sharedStop2);
var box = new BoxView { HeightRequest = 4 };
box.Background = brush;
_brushes.Add(brush);
_boxes.Add(box);
_brushRefs.Add(new WeakReference(brush));
_boxRefs.Add(new WeakReference(box));
}
// Show one of the brushes on screen.
PreviewBox.Background = _brushes[0];
UpdateStatus($"Created {BrushCount} brushes with shared stops. Now press \"Clear\".");
}
void OnClearAndReleaseClicked(object sender, EventArgs e)
{
if (_brushes.Count == 0)
{
UpdateStatus("Press \"Create\" first.");
return;
}
foreach (var brush in _brushes)
{
// BUG: Clear() fires CollectionChanged with Action=Reset.
// e.OldItems is null so OnGradientStopCollectionChanged never
// unsubscribes the shared stops, leaving them holding the brush alive.
brush.GradientStops.Clear();
}
// Release all strong references.
_brushes.Clear();
_boxes.Clear();
PreviewBox.Background = null;
UpdateStatus($"Cleared GradientStops and released {BrushCount} brushes. Now press \"Check Leaks\".");
}
void OnCheckLeaksClicked(object sender, EventArgs e)
{
if (_brushRefs.Count == 0)
{
UpdateStatus("Press \"Create\" and then \"Clear\" first.");
return;
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
int liveBrushes = _brushRefs.Count(r => r.IsAlive);
int liveBoxes = _boxRefs.Count(r => r.IsAlive);
string verdict = liveBrushes > 0
? $"LEAK CONFIRMED: {liveBrushes}/{BrushCount} brushes still alive (shared stops hold them via PropertyChanged). Expected: 0."
: $"No leak: 0/{BrushCount} brushes alive after GC.";
StatusLabel.TextColor = liveBrushes > 0 ? Colors.Red : Colors.Green;
UpdateStatus(verdict);
}
void UpdateStatus(string message)
{
StatusLabel.Text = message;
}
}
2.Press "Create brushes using shared stops" — creates 50 brushes sharing the same 2 GradientStop instances.
3.Press "Clear() GradientStops and release" — clears each brush's stops and drops all strong refs.
4.Press "Force GC and check leaks".
Link to public reproduction project repository
No response
Version with bug
10.0.80
Is this a regression from previous behavior?
No, this is something new
Last version that worked well
Unknown/Other
Affected platforms
iOS, Android, Windows, macOS
Affected platform versions
No response
Did you find any workaround?
No response
Relevant log output
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area-drawingShapes, Borders, Shadows, Graphics, BoxView, custom drawingShapes, Borders, Shadows, Graphics, BoxView, custom drawingpartner/syncfusionIssues / PR's with Syncfusion collaborationIssues / PR's with Syncfusion collaborationplatform/androidplatform/iosplatform/macosmacOS / Mac CatalystmacOS / Mac Catalystplatform/windowss/triagedIssue has been reviewedIssue has been revieweds/verifiedVerified / Reproducible Issue ready for Engineering TriageVerified / Reproducible Issue ready for Engineering Triaget/bugSomething isn't workingSomething isn't working
Activity
MihuBot commentedon Jul 23, 2026
I'm a bot. Here is a possible related and/or duplicate issue (I may be wrong):
Ahamed-Ali commentedon Jul 23, 2026
This issue has been validated in Visual Studio Code 1.123.0 with .NET MAUI versions 10.0.0 ,10.0.80 and 10.0.90. It was reproducible on iOS, Android, Windows and MacOS platform.
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
12 remaining items
Publish reproduction evidence for #36743 on android
Publish reproduction evidence for #36743 on android
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …
Fix GradientBrush.GradientStops.Clear() leaks brushes when stops are …