Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
11dbd6b
[leak-fix] Fix GradientBrush.GradientStops memory leak (Fixes #36363)
github-actions[bot] Jul 11, 2026
16141ad
Fix GradientBrush weak stop subscriptions
Copilot Jul 11, 2026
10a8974
Add GradientBrush clear-and-reuse coverage
Copilot Jul 12, 2026
2561d37
Expand GradientBrush sharing coverage
Copilot Jul 13, 2026
4c7fc90
Cover shared GradientBrush sibling collection
Copilot Jul 13, 2026
d78e983
Test duplicate GradientStop subscriptions
Copilot Jul 13, 2026
ad3922b
Invalidate GradientBrush when stops are replaced
Copilot Jul 13, 2026
46a1183
Handle null GradientStops binding context changes
Copilot Jul 13, 2026
7a22116
Document GradientStops replacement invalidation
Copilot Jul 13, 2026
9fa3204
Handle null gradient stops during context propagation
Copilot Jul 13, 2026
d96f8a1
Preserve shared gradient stop parent ownership
Copilot Jul 14, 2026
deb0edd
Cover reentrant GradientBrush invalidations
Copilot Jul 14, 2026
d51f4a0
Use reference identity for GradientStop ownership
Copilot Jul 14, 2026
7f48963
Fix reentrant GradientStop removal
Copilot Jul 14, 2026
4e08b15
Avoid stale GradientStop parent warnings
Copilot Jul 14, 2026
dc8c36e
Harden GradientBrush subscription cleanup
Copilot Jul 14, 2026
5cbbea4
Harden GradientStop parent cleanup
Copilot Jul 14, 2026
d7b3b9e
Clear inherited context for collected gradient stops
Copilot Jul 14, 2026
b09c64a
Drain collected parent context on parent access
Copilot Jul 14, 2026
1c45b37
Handle failed inherited context dispatch
Copilot Jul 14, 2026
68492ab
Dispatch pending binding context cleanup
Copilot Jul 14, 2026
dc3bad7
Fix inherited cleanup for bound BindingContext
Copilot Jul 15, 2026
fee53dd
Make inherited binding context cleanup finalizer-safe
Copilot Jul 15, 2026
dfd20b2
Prevent stale BindingContext during pending cleanup
Copilot Jul 15, 2026
1db995c
Deduplicate inherited context cleanup dispatch
Copilot Jul 15, 2026
e865300
Fix late-bound cleanup dispatcher
Copilot Jul 15, 2026
e313e45
Capture dispatcher during parent assignment
Copilot Jul 15, 2026
79818b1
Fix dispatcher race and stale bindings
Copilot Jul 15, 2026
e91b117
Guard finalizer dispatcher resolution
Copilot Jul 15, 2026
6685ddf
Handle disposed cleanup dispatcher
Copilot Jul 15, 2026
049ec12
Handle disposed dispatcher on normal cleanup
Copilot Jul 15, 2026
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
362 changes: 341 additions & 21 deletions src/Controls/src/Core/BindableObject.cs

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/Controls/src/Core/BindingBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,15 @@ public object FallbackValue

internal bool AllowChaining { get; set; }

internal object Context { get; set; }
object _context;

internal object Context
{
get => _context is BindableObject.InheritedBindingContextReference inheritedContext
? inheritedContext.Target
: _context;
set => _context = value;
}

internal bool IsApplied { get; private set; }

Expand Down
63 changes: 43 additions & 20 deletions src/Controls/src/Core/DispatcherExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,65 @@ namespace Microsoft.Maui.Controls
internal static class DispatcherExtensions
{
public static IDispatcher FindDispatcher(this BindableObject? bindableObject)
{
if (bindableObject.TryFindDispatcher(includeParents: true) is IDispatcher dispatcher)
return dispatcher;

if (bindableObject is not Application &&
Application.Current?.Dispatcher is IDispatcher appDispatcher)
{
return appDispatcher;
}

throw new InvalidOperationException("BindableObject was not instantiated on a thread with a dispatcher nor does the current application have a dispatcher.");
}

internal static IDispatcher? TryFindDispatcher(
this BindableObject? bindableObject,
bool includeParents)
{
// try find the dispatcher in the current hierarchy
// Exclude Application because we don't want to jump
// directly to the Application IDispatcher at this point
if (bindableObject is not Application &&
bindableObject is Element element &&
element.FindMauiContext() is IMauiContext context &&
context.Services.GetService<IDispatcher>() is IDispatcher handlerDispatcher)
return handlerDispatcher;
bindableObject is Element element)
{
var context = includeParents
? element.FindMauiContext()
: (element as Maui.IElement)?.Handler?.MauiContext;

if (context?.Services.GetService<IDispatcher>() is IDispatcher handlerDispatcher)
return handlerDispatcher;
}

// maybe this thread has a dispatcher
if (Dispatcher.GetForCurrentThread() is IDispatcher globalDispatcher)
return globalDispatcher;

// If BO is of type Application then return the Dispatcher from ApplicationDispatcher
if (bindableObject is Application app &&
app.FindMauiContext() is IMauiContext appMauiContext)
{
if (appMauiContext.Services.GetOptionalApplicationDispatcher() is IDispatcher appDispatcherServiceDispatcher)
return appDispatcherServiceDispatcher;
TryFindApplicationDispatcher(app) is IDispatcher appDispatcher)
return appDispatcher;

// If BO is of type Application then check for its Dispatcher
if (appMauiContext.Services.GetService<IDispatcher>() is IDispatcher appHandlerDispatcher)
return appHandlerDispatcher;
// Try the static app's registered dispatcher without calling its Dispatcher
// property, which may throw. The public FindDispatcher path preserves that
// fallback after this non-throwing lookup returns null.
if (bindableObject is not Application && Application.Current is Application currentApp)
{
if (TryFindApplicationDispatcher(currentApp) is IDispatcher currentAppDispatcherService)
return currentAppDispatcherService;
}

// try looking on the static app
// We don't include Application because Application.Dispatcher will call
// `FindDispatcher` if it's _dispatcher property isn't initialized so this
// could cause a Stack Overflow Exception
if (bindableObject is not Application &&
Application.Current?.Dispatcher is IDispatcher appDispatcher)
return appDispatcher;
return null;
}

// no dispatchers found at all
throw new InvalidOperationException("BindableObject was not instantiated on a thread with a dispatcher nor does the current application have a dispatcher.");
static IDispatcher? TryFindApplicationDispatcher(Application app)
{
if (app.FindMauiContext() is not IMauiContext appMauiContext)
return null;

return appMauiContext.Services.GetOptionalApplicationDispatcher()
?? appMauiContext.Services.GetService<IDispatcher>();
}

public static void DispatchIfRequired(this IDispatcher? dispatcher, Action action)
Expand Down
102 changes: 82 additions & 20 deletions src/Controls/src/Core/Element/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Hosting;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Dispatching;

namespace Microsoft.Maui.Controls
{
Expand Down Expand Up @@ -333,32 +335,80 @@ internal Element ParentOverride
}

WeakReference<Element> _realParent;
Element TryGetRealParent(bool logWarningIfParentHasBeenCollected = true)
Element TryGetRealParent(
bool logWarningIfParentHasBeenCollected = true,
bool clearInheritedContextIfDispatchNotRequired = true)
{
var realParent = _realParent;
if (realParent is null)
while (true)
{
return null;
}
if (realParent.TryGetTarget(out var parent))
{
return parent;
}
else
{
// Clear the weak reference since the target has been garbage collected
// This prevents repeated checks and warnings on subsequent accesses
_realParent = null;
var realParent = Volatile.Read(ref _realParent);
if (realParent is null)
{
if (clearInheritedContextIfDispatchNotRequired)
{
DispatchInheritedBindingContextCleanup(clearIfDispatchNotRequired: true);
if (Volatile.Read(ref _realParent) is not null)
continue;
}

return null;
}

if (realParent.TryGetTarget(out var parent))
return parent;

if (!ClearRealParentAndInheritedContextIfCollected(
realParent,
clearInheritedContextIfDispatchNotRequired))
continue;

if (logWarningIfParentHasBeenCollected)
{
Application.Current?
.FindMauiContext()?
.CreateLogger<Element>()?
.LogWarning($"The RealParent on {this} has been Garbage Collected. This should never happen. Please log a bug: https://github.com/dotnet/maui");
logWarningIfParentHasBeenCollected = false;
}
}
}

return null;
internal void ClearRealParentAndInheritedContextIfCollected()
{
var realParent = Volatile.Read(ref _realParent);
if (realParent is not null)
ClearRealParentAndInheritedContextIfCollected(
realParent,
clearInheritedContextIfDispatchNotRequired: false);
}

bool ClearRealParentAndInheritedContextIfCollected(
WeakReference<Element> realParent,
bool clearInheritedContextIfDispatchNotRequired)
{
if (realParent.TryGetTarget(out _))
return false;

// Only clear the reference we observed; a new parent may be assigned concurrently.
if (!ReferenceEquals(Interlocked.CompareExchange(ref _realParent, null, realParent), realParent))
return false;

if (Volatile.Read(ref _realParent) is not null)
return true;

var inheritedContext = MarkInheritedBindingContextForCleanup();
if (inheritedContext is null)
return true;

if (Volatile.Read(ref _realParent) is not null)
{
CancelInheritedBindingContextCleanup(inheritedContext);
return true;
}

DispatchInheritedBindingContextCleanup(clearInheritedContextIfDispatchNotRequired);
Comment thread
kubaflo marked this conversation as resolved.

return true;
}

/// <summary>For internal use by .NET MAUI.</summary>
Expand All @@ -369,9 +419,9 @@ public Element RealParent
private set
{
if (value is null)
_realParent = null;
Volatile.Write(ref _realParent, null);
else
_realParent = new WeakReference<Element>(value);
Volatile.Write(ref _realParent, new WeakReference<Element>(value));
}
}

Expand All @@ -395,13 +445,22 @@ public Element Parent

void SetParent(Element value)
{
Element realParent = TryGetRealParent(false);
Element realParent = TryGetRealParent(
logWarningIfParentHasBeenCollected: false,
clearInheritedContextIfDispatchNotRequired: value is null);

if (realParent == value)
{
return;
}

if (!HasDispatcher && value is not null)
{
var parentDispatcher = value.GetDispatcherIfAvailable()
?? value.TryFindDispatcher(includeParents: true);
SetDispatcherIfUnset(parentDispatcher);
}

OnPropertyChanging(nameof(Parent));

if (_parentOverride == null)
Expand All @@ -420,12 +479,12 @@ void SetParent(Element value)
}

RealParent = value;
if (RealParent != null)
if (value != null)
{
var resources = GetParentResourcesForParentSet();
if (resources != null)
OnParentResourcesChanged(resources);
((IElementDefinition)RealParent).AddResourcesChangedListener(OnParentResourcesChanged);
((IElementDefinition)value).AddResourcesChangedListener(OnParentResourcesChanged);
}

object context = value?.BindingContext;
Expand Down Expand Up @@ -1086,6 +1145,9 @@ void SetHandler(IElementHandler newHandler)
OnHandlerChangingCore(new HandlerChangingEventArgs(_previousHandler, newHandler));

_handler = newHandler;
if (!HasDispatcher &&
_handler?.MauiContext?.Services.GetService<IDispatcher>() is IDispatcher handlerDispatcher)
SetDispatcherIfUnset(handlerDispatcher);

// Only call disconnect if the previous handler is still connected to this virtual view.
// If a handler is being reused for a different VirtualView then the virtual
Expand Down
Loading
Loading