diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs
index 0e7d7d65c219..8590a2d2d10b 100644
--- a/src/Controls/src/Core/BindableObject.cs
+++ b/src/Controls/src/Core/BindableObject.cs
@@ -7,6 +7,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Threading;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Dispatching;
@@ -26,8 +27,18 @@ public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceH
/// Gets the dispatcher that was available when this bindable object was created,
/// otherwise tries to find the nearest available dispatcher (probably the window's/app's).
///
- public IDispatcher Dispatcher =>
- _dispatcher ??= this.FindDispatcher();
+ public IDispatcher Dispatcher
+ {
+ get
+ {
+ var dispatcher = GetDispatcherIfAvailable();
+ if (dispatcher is not null)
+ return dispatcher;
+
+ SetDispatcherIfUnset(this.FindDispatcher());
+ return GetDispatcherIfAvailable();
+ }
+ }
///
/// Initializes a new instance of the class.
@@ -38,12 +49,55 @@ public BindableObject()
_dispatcher = Dispatching.Dispatcher.GetForCurrentThread();
}
+ internal void SetDispatcherIfUnset(IDispatcher dispatcher)
+ {
+ if (dispatcher is not null)
+ Interlocked.CompareExchange(ref _dispatcher, dispatcher, null);
+ }
+
+ internal IDispatcher GetDispatcherIfAvailable() =>
+ Volatile.Read(ref _dispatcher);
+
+ internal bool HasDispatcher =>
+ GetDispatcherIfAvailable() is not null;
+
internal ushort _triggerCount = 0;
internal Dictionary _triggerSpecificity = new();
readonly Dictionary _properties = new(4);
bool _applying;
+ WeakReference _inheritedBindingContext;
WeakReference _inheritedContext;
+ internal sealed class InheritedBindingContextReference : WeakReference
+ {
+ internal InheritedBindingContextReference(object target)
+ : base(target)
+ {
+ }
+ }
+
+ sealed class PendingInheritedBindingContextCleanup : WeakReference
+ {
+ int _dispatchScheduled;
+
+ internal PendingInheritedBindingContextCleanup(WeakReference inheritedContext, bool isBindingContextBinding)
+ : base(null)
+ {
+ InheritedContext = inheritedContext;
+ IsBindingContextBinding = isBindingContextBinding;
+ }
+
+ internal WeakReference InheritedContext { get; }
+
+ internal bool IsBindingContextBinding { get; }
+
+ internal bool TryScheduleDispatch() =>
+ Interlocked.CompareExchange(ref _dispatchScheduled, 1, 0) == 0;
+
+ internal void ResetScheduledDispatch() =>
+ Interlocked.Exchange(ref _dispatchScheduled, 0);
+ }
+
/// Bindable property for .
public static readonly BindableProperty BindingContextProperty =
BindableProperty.Create(nameof(BindingContext), typeof(object), typeof(BindableObject), default(object),
@@ -55,7 +109,25 @@ public BindableObject()
///
public object BindingContext
{
- get => _inheritedContext?.Target ?? GetValue(BindingContextProperty);
+ get
+ {
+ var inheritedContext = Volatile.Read(ref _inheritedContext);
+ var inheritedBindingContext = Volatile.Read(ref _inheritedBindingContext);
+ if (inheritedContext is PendingInheritedBindingContextCleanup
+ || inheritedBindingContext is PendingInheritedBindingContextCleanup)
+ {
+ DispatchInheritedBindingContextCleanup(clearIfDispatchNotRequired: true);
+ inheritedContext = Volatile.Read(ref _inheritedContext);
+ inheritedBindingContext = Volatile.Read(ref _inheritedBindingContext);
+ if (inheritedContext is PendingInheritedBindingContextCleanup
+ || inheritedBindingContext is PendingInheritedBindingContextCleanup)
+ {
+ return null;
+ }
+ }
+
+ return inheritedContext?.Target ?? GetValue(BindingContextProperty);
+ }
set => SetValue(BindingContextProperty, value);
}
@@ -344,7 +416,7 @@ internal void SetBinding(BindableProperty targetProperty, BindingBase binding, S
targetProperty.BindingChanging?.Invoke(this, oldBinding, binding);
- binding.Apply(BindingContext, this, targetProperty, false, specificity);
+ binding.Apply(GetBindingContextForBindingApplication(targetProperty), this, targetProperty, false, specificity);
}
///
@@ -355,32 +427,189 @@ internal void SetBinding(BindableProperty targetProperty, BindingBase binding, S
/// For internal use only. This API can be changed or removed without notice at any time.
[EditorBrowsable(EditorBrowsableState.Never)]
public static void SetInheritedBindingContext(BindableObject bindable, object value)
+ {
+ SetInheritedBindingContextCore(bindable, value, force: false);
+ }
+
+ internal static void SetInheritedBindingContextForBinding(BindableObject bindable, object value)
+ {
+ SetInheritedBindingContextCore(bindable, value, force: true);
+ }
+
+ static void SetInheritedBindingContextCore(BindableObject bindable, object value, bool force)
{
// I wonder if we couldn't treat BindingContext with specificities
BindablePropertyContext bpContext = bindable.GetContext(BindingContextProperty);
- if (bpContext != null && bpContext.Values.GetSpecificity() >= SetterSpecificity.ManualValueSetter)
+ var binding = bpContext?.Bindings.GetValue();
+ if (bpContext != null
+ && bpContext.Values.GetSpecificity() >= SetterSpecificity.ManualValueSetter
+ && (!force || binding is null))
+ {
return;
+ }
- if (ReferenceEquals(bindable._inheritedContext?.Target, value))
+ var inheritedContext = Volatile.Read(ref bindable._inheritedContext);
+ var inheritedBindingContext = Volatile.Read(ref bindable._inheritedBindingContext);
+ if (!force
+ && inheritedContext is not PendingInheritedBindingContextCleanup
+ && inheritedBindingContext is not PendingInheritedBindingContextCleanup
+ && ReferenceEquals((inheritedContext ?? inheritedBindingContext)?.Target, value))
return;
- var binding = bpContext?.Bindings.GetValue();
-
if (binding != null)
{
- binding.Context = value;
- bindable._inheritedContext = null;
+ var bindingContext = new InheritedBindingContextReference(value);
+ binding.Context = bindingContext;
+ Volatile.Write(ref bindable._inheritedContext, null);
+ Volatile.Write(ref bindable._inheritedBindingContext, bindingContext);
// OnBindingContextChanged fires from within BindingContextProperty propertyChanged callback
bindable.ApplyBinding(bpContext, fromBindingContextChanged: true);
}
else
{
- bindable._inheritedContext = new WeakReference(value);
+ Volatile.Write(ref bindable._inheritedBindingContext, null);
+ Volatile.Write(ref bindable._inheritedContext, new InheritedBindingContextReference(value));
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
}
}
+ internal WeakReference MarkInheritedBindingContextForCleanup()
+ {
+ var inheritedContext = Volatile.Read(ref _inheritedContext);
+ if (inheritedContext is not null)
+ {
+ if (inheritedContext is PendingInheritedBindingContextCleanup)
+ return null;
+
+ return MarkInheritedBindingContextForCleanup(
+ ref _inheritedContext,
+ inheritedContext,
+ isBindingContextBinding: false);
+ }
+
+ var inheritedBindingContext = Volatile.Read(ref _inheritedBindingContext);
+ if (inheritedBindingContext is null or PendingInheritedBindingContextCleanup)
+ return null;
+
+ return MarkInheritedBindingContextForCleanup(
+ ref _inheritedBindingContext,
+ inheritedBindingContext,
+ isBindingContextBinding: true);
+ }
+
+ static WeakReference MarkInheritedBindingContextForCleanup(
+ ref WeakReference inheritedContext,
+ WeakReference observedContext,
+ bool isBindingContextBinding)
+ {
+ var pendingCleanup = new PendingInheritedBindingContextCleanup(observedContext, isBindingContextBinding);
+ return ReferenceEquals(
+ Interlocked.CompareExchange(ref inheritedContext, pendingCleanup, observedContext),
+ observedContext)
+ ? pendingCleanup
+ : null;
+ }
+
+ internal void CancelInheritedBindingContextCleanup(WeakReference cleanupToken)
+ {
+ if (cleanupToken is not PendingInheritedBindingContextCleanup pendingCleanup)
+ return;
+
+ if (pendingCleanup.IsBindingContextBinding)
+ {
+ Interlocked.CompareExchange(
+ ref _inheritedBindingContext,
+ pendingCleanup.InheritedContext,
+ pendingCleanup);
+ }
+ else
+ {
+ Interlocked.CompareExchange(
+ ref _inheritedContext,
+ pendingCleanup.InheritedContext,
+ pendingCleanup);
+ }
+ }
+
+ internal void DispatchInheritedBindingContextCleanup(bool clearIfDispatchNotRequired = false)
+ {
+ var pendingCleanup = Volatile.Read(ref _inheritedContext) as PendingInheritedBindingContextCleanup
+ ?? Volatile.Read(ref _inheritedBindingContext) as PendingInheritedBindingContextCleanup;
+ if (pendingCleanup is null)
+ return;
+
+ // Finalizer callers only queue work here. Binding callbacks run on the dispatcher
+ // or remain pending for a normal access path to clear safely.
+ var dispatcher = GetDispatcherIfAvailable();
+ // Only normal access paths may resolve dispatchers because discovery can read
+ // handler or application service providers that are already disposed at finalization.
+ if (dispatcher is null &&
+ clearIfDispatchNotRequired &&
+ this.TryFindDispatcher(
+ includeParents: false) is IDispatcher discoveredDispatcher)
+ {
+ // The parent reference has already been cleared, so walking the parent
+ // hierarchy here would re-enter this cleanup path.
+ SetDispatcherIfUnset(discoveredDispatcher);
+ dispatcher = GetDispatcherIfAvailable();
+ }
+
+ bool isDispatchRequired = false;
+ try
+ {
+ isDispatchRequired = dispatcher?.IsDispatchRequired == true;
+ }
+ catch (ObjectDisposedException)
+ {
+ // A cached dispatcher can outlive its window. Keep the cleanup pending
+ // instead of leaking disposal through Parent or BindingContext access.
+ return;
+ }
+
+ if (isDispatchRequired)
+ {
+ if (!pendingCleanup.TryScheduleDispatch())
+ return;
+
+ bool dispatchAccepted = false;
+ try
+ {
+ dispatchAccepted = dispatcher.Dispatch(() => ClearPendingInheritedBindingContext(pendingCleanup));
+ }
+ catch (ObjectDisposedException)
+ {
+ return;
+ }
+ finally
+ {
+ if (!dispatchAccepted)
+ pendingCleanup.ResetScheduledDispatch();
+ }
+
+ return;
+ }
+
+ if (clearIfDispatchNotRequired)
+ ClearPendingInheritedBindingContext(pendingCleanup);
+ }
+
+ void ClearPendingInheritedBindingContext(PendingInheritedBindingContextCleanup pendingCleanup)
+ {
+ ref WeakReference inheritedContext = ref (pendingCleanup.IsBindingContextBinding
+ ? ref _inheritedBindingContext
+ : ref _inheritedContext);
+
+ if (!ReferenceEquals(
+ Interlocked.CompareExchange(ref inheritedContext, null, pendingCleanup),
+ pendingCleanup))
+ {
+ return;
+ }
+
+ SetInheritedBindingContextCore(this, null, force: true);
+ }
+
///
/// Applies all the current bindings to .
///
@@ -738,24 +967,66 @@ void ApplyBinding(BindablePropertyContext context, bool fromBindingContextChange
var specificity = kvp.Key;
binding.Unapply(fromBindingContextChanged);
- binding.Apply(BindingContext, this, context.Property, fromBindingContextChanged, specificity);
+ binding.Apply(
+ GetBindingContextForBindingApplication(context.Property),
+ this,
+ context.Property,
+ fromBindingContextChanged,
+ specificity);
+ }
+
+ object GetBindingContextForBindingApplication(BindableProperty property)
+ {
+ if (!ReferenceEquals(property, BindingContextProperty))
+ return BindingContext;
+
+ var inheritedBindingContext = Volatile.Read(ref _inheritedBindingContext);
+ if (inheritedBindingContext is PendingInheritedBindingContextCleanup)
+ return null;
+
+ if (Volatile.Read(ref _inheritedContext) is PendingInheritedBindingContextCleanup)
+ return null;
+
+ return inheritedBindingContext is null
+ ? BindingContext
+ : inheritedBindingContext.Target;
}
static void BindingContextPropertyBindingChanging(BindableObject bindable, BindingBase oldBindingBase, BindingBase newBindingBase)
{
- object context = bindable._inheritedContext?.Target;
- var oldBinding = oldBindingBase as Binding;
- var newBinding = newBindingBase as Binding;
+ var inheritedBindingContext = Volatile.Read(ref bindable._inheritedBindingContext);
+ var inheritedContext = Volatile.Read(ref bindable._inheritedContext);
+
+ if (newBindingBase is null)
+ {
+ if (inheritedBindingContext is not PendingInheritedBindingContextCleanup)
+ Volatile.Write(ref bindable._inheritedBindingContext, null);
+ return;
+ }
+
+ if (inheritedBindingContext is PendingInheritedBindingContextCleanup
+ || inheritedContext is PendingInheritedBindingContextCleanup)
+ {
+ newBindingBase.Context = new InheritedBindingContextReference(null);
+ return;
+ }
+
+ if (inheritedContext is null)
+ inheritedContext = inheritedBindingContext;
- if (context == null && oldBinding != null)
- context = oldBinding.Context;
- if (context != null && newBinding != null)
- newBinding.Context = context;
+ if (inheritedContext is null)
+ return;
+
+ var bindingContext = inheritedContext as InheritedBindingContextReference
+ ?? new InheritedBindingContextReference(inheritedContext.Target);
+ Volatile.Write(ref bindable._inheritedContext, null);
+ Volatile.Write(ref bindable._inheritedBindingContext, bindingContext);
+ newBindingBase.Context = bindingContext;
}
static void BindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
- bindable._inheritedContext = null;
+ Volatile.Write(ref bindable._inheritedContext, null);
bindable.ApplyBindings(fromBindingContextChanged: true);
bindable.OnBindingContextChanged();
}
@@ -806,6 +1077,10 @@ void RemoveBinding(BindableProperty property, BindablePropertyContext context, S
var currentbinding = context.Bindings.GetValue();
var binding = context.Bindings[specificity];
var isCurrent = binding == currentbinding;
+ var pendingCleanup = isCurrent && ReferenceEquals(property, BindingContextProperty)
+ ? Volatile.Read(ref _inheritedBindingContext) as PendingInheritedBindingContextCleanup
+ ?? Volatile.Read(ref _inheritedContext) as PendingInheritedBindingContextCleanup
+ : null;
if (isCurrent)
{
@@ -817,10 +1092,55 @@ void RemoveBinding(BindableProperty property, BindablePropertyContext context, S
property.BindingChanging?.Invoke(this, binding, currentbinding);
- currentbinding?.Apply(BindingContext, this, property, false, context.Bindings.GetClearedSpecificity());
+ currentbinding?.Apply(
+ GetBindingContextForBindingApplication(property),
+ this,
+ property,
+ false,
+ context.Bindings.GetClearedSpecificity());
}
context.Bindings.Remove(specificity);
+ if (pendingCleanup is not null)
+ CompletePendingInheritedBindingContextCleanupAfterBindingRemoval(context, specificity, pendingCleanup);
+ }
+
+ void CompletePendingInheritedBindingContextCleanupAfterBindingRemoval(
+ BindablePropertyContext context,
+ SetterSpecificity removedSpecificity,
+ PendingInheritedBindingContextCleanup pendingCleanup)
+ {
+ bool hasBinding = context.Bindings.GetValue() is not null;
+ var clearedContext = new InheritedBindingContextReference(null);
+ bool claimedCleanup;
+
+ if (pendingCleanup.IsBindingContextBinding)
+ {
+ claimedCleanup = ReferenceEquals(
+ Interlocked.CompareExchange(
+ ref _inheritedBindingContext,
+ hasBinding ? clearedContext : null,
+ pendingCleanup),
+ pendingCleanup);
+ }
+ else
+ {
+ claimedCleanup = ReferenceEquals(
+ Interlocked.CompareExchange(ref _inheritedContext, null, pendingCleanup),
+ pendingCleanup);
+ if (claimedCleanup && hasBinding)
+ Volatile.Write(ref _inheritedBindingContext, clearedContext);
+ }
+
+ if (!claimedCleanup)
+ return;
+
+ ClearValueCore(BindingContextProperty, removedSpecificity);
+ if (!hasBinding)
+ {
+ Volatile.Write(ref _inheritedBindingContext, null);
+ Volatile.Write(ref _inheritedContext, clearedContext);
+ }
}
///
diff --git a/src/Controls/src/Core/BindingBase.cs b/src/Controls/src/Core/BindingBase.cs
index cefb63920392..82a2acdc34a0 100644
--- a/src/Controls/src/Core/BindingBase.cs
+++ b/src/Controls/src/Core/BindingBase.cs
@@ -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; }
diff --git a/src/Controls/src/Core/DispatcherExtensions.cs b/src/Controls/src/Core/DispatcherExtensions.cs
index 92bfedc3a2f3..cf3ea75cbbaf 100644
--- a/src/Controls/src/Core/DispatcherExtensions.cs
+++ b/src/Controls/src/Core/DispatcherExtensions.cs
@@ -9,15 +9,36 @@ 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() is IDispatcher handlerDispatcher)
- return handlerDispatcher;
+ bindableObject is Element element)
+ {
+ var context = includeParents
+ ? element.FindMauiContext()
+ : (element as Maui.IElement)?.Handler?.MauiContext;
+
+ if (context?.Services.GetService() is IDispatcher handlerDispatcher)
+ return handlerDispatcher;
+ }
// maybe this thread has a dispatcher
if (Dispatcher.GetForCurrentThread() is IDispatcher globalDispatcher)
@@ -25,26 +46,28 @@ bindableObject is Element element &&
// 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() 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();
}
public static void DispatchIfRequired(this IDispatcher? dispatcher, Action action)
diff --git a/src/Controls/src/Core/Element/Element.cs b/src/Controls/src/Core/Element/Element.cs
index 9982d9f2422a..5d9fde603c96 100644
--- a/src/Controls/src/Core/Element/Element.cs
+++ b/src/Controls/src/Core/Element/Element.cs
@@ -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
{
@@ -333,32 +335,80 @@ internal Element ParentOverride
}
WeakReference _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()?
.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 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);
+
+ return true;
}
/// For internal use by .NET MAUI.
@@ -369,9 +419,9 @@ public Element RealParent
private set
{
if (value is null)
- _realParent = null;
+ Volatile.Write(ref _realParent, null);
else
- _realParent = new WeakReference(value);
+ Volatile.Write(ref _realParent, new WeakReference(value));
}
}
@@ -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)
@@ -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;
@@ -1086,6 +1145,9 @@ void SetHandler(IElementHandler newHandler)
OnHandlerChangingCore(new HandlerChangingEventArgs(_previousHandler, newHandler));
_handler = newHandler;
+ if (!HasDispatcher &&
+ _handler?.MauiContext?.Services.GetService() 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
diff --git a/src/Controls/src/Core/GradientBrush.cs b/src/Controls/src/Core/GradientBrush.cs
index 8a3fb106e6b0..7e2d07373ed3 100644
--- a/src/Controls/src/Core/GradientBrush.cs
+++ b/src/Controls/src/Core/GradientBrush.cs
@@ -1,5 +1,6 @@
#nullable disable
using System;
+using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -15,6 +16,10 @@ public GradientBrush()
GradientStops = new GradientStopCollection();
}
+ GradientStopSubscriptions _gradientStopSubscriptions;
+ NotifyCollectionChangedEventHandler _gradientStopsCollectionChanged;
+ PropertyChangedEventHandler _gradientStopPropertyChanged;
+
public event EventHandler InvalidateGradientBrushRequested;
/// Bindable property for .
@@ -41,49 +46,63 @@ protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
- foreach (var gradientStop in GradientStops)
+ var gradientStops = GradientStops;
+ if (gradientStops is null)
+ return;
+
+ foreach (var gradientStop in gradientStops)
+ {
+ if (gradientStop is null)
+ continue;
+
SetInheritedBindingContext(gradientStop, BindingContext);
+ }
}
void UpdateGradientStops(GradientStopCollection oldCollection, GradientStopCollection newCollection)
{
if (oldCollection != null)
{
- oldCollection.CollectionChanged -= OnGradientStopCollectionChanged;
-
- foreach (var oldStop in oldCollection)
- {
- oldStop.Parent = null;
- oldStop.PropertyChanged -= OnGradientStopPropertyChanged;
- }
+ _gradientStopSubscriptions?.UnsubscribeAll(this);
}
- if (newCollection == null)
- return;
+ if (newCollection != null)
+ {
+ _gradientStopsCollectionChanged ??= OnGradientStopCollectionChanged;
+ _gradientStopPropertyChanged ??= OnGradientStopPropertyChanged;
- newCollection.CollectionChanged += OnGradientStopCollectionChanged;
+ var subscriptions = _gradientStopSubscriptions ??= new GradientStopSubscriptions();
+ subscriptions.Subscribe(newCollection, _gradientStopsCollectionChanged);
- foreach (var newStop in newCollection)
- {
- if (newStop is not null)
+ foreach (var newStop in newCollection)
{
- newStop.Parent = this;
- newStop.PropertyChanged += OnGradientStopPropertyChanged;
+ if (newStop is not null)
+ {
+ newStop.Parent = this;
+ subscriptions.Add(newStop, _gradientStopPropertyChanged);
+ }
}
}
+
+ // Collection replacement must invalidate even when the new value is null or empty.
+ Invalidate();
}
void OnGradientStopCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- if (e.OldItems != null)
+ if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ _gradientStopSubscriptions?.ResetStops(this);
+ }
+ else if (e.OldItems != null)
{
foreach (var oldItem in e.OldItems)
{
if (!(oldItem is GradientStop oldStop))
continue;
- oldStop.Parent = null;
- oldStop.PropertyChanged -= OnGradientStopPropertyChanged;
+ _gradientStopSubscriptions?.Remove(oldStop);
+ ClearGradientStopParentIfUnused(oldStop);
}
}
@@ -95,13 +114,113 @@ void OnGradientStopCollectionChanged(object sender, NotifyCollectionChangedEvent
continue;
newStop.Parent = this;
- newStop.PropertyChanged += OnGradientStopPropertyChanged;
+ _gradientStopSubscriptions?.Add(newStop, _gradientStopPropertyChanged);
}
}
Invalidate();
}
+ void ClearGradientStopParentIfUnused(GradientStop gradientStop)
+ {
+ if (!ReferenceEquals(gradientStop.Parent, this))
+ return;
+
+ var gradientStops = GradientStops;
+ if (gradientStops is not null)
+ {
+ for (int i = 0; i < gradientStops.Count; i++)
+ {
+ if (ReferenceEquals(gradientStops[i], gradientStop))
+ return;
+ }
+ }
+
+ gradientStop.Parent = null;
+ }
+
+ sealed class GradientStopSubscriptions
+ {
+ readonly WeakNotifyCollectionChangedProxy _collectionProxy = new();
+ readonly List _stopProxies = new();
+
+ ~GradientStopSubscriptions() => UnsubscribeAllForFinalization();
+
+ public void Subscribe(GradientStopCollection source, NotifyCollectionChangedEventHandler handler)
+ {
+ _collectionProxy.Subscribe(source, handler);
+ }
+
+ public void Add(GradientStop source, PropertyChangedEventHandler handler)
+ {
+ _stopProxies.Add(new WeakNotifyPropertyChangedProxy(source, handler));
+ }
+
+ public void Remove(GradientStop source)
+ {
+ bool removed = false;
+ for (int i = _stopProxies.Count - 1; i >= 0; i--)
+ {
+ var proxy = _stopProxies[i];
+ if (!proxy.TryGetSource(out var proxySource))
+ {
+ proxy.Unsubscribe();
+ _stopProxies.RemoveAt(i);
+ }
+ else if (!removed && ReferenceEquals(proxySource, source))
+ {
+ proxy.Unsubscribe();
+ _stopProxies.RemoveAt(i);
+ removed = true;
+ }
+ }
+ }
+
+ public void ResetStops(GradientBrush owner)
+ {
+ UnsubscribeStops(owner);
+ }
+
+ public void UnsubscribeAll(GradientBrush owner)
+ {
+ UnsubscribeAllCore(owner);
+ }
+
+ void UnsubscribeAllForFinalization()
+ {
+ UnsubscribeAllCore(owner: null);
+ }
+
+ void UnsubscribeAllCore(GradientBrush owner)
+ {
+ _collectionProxy.Unsubscribe();
+ UnsubscribeStops(owner);
+ }
+
+ void UnsubscribeStops(GradientBrush owner)
+ {
+ var proxies = _stopProxies.ToArray();
+ _stopProxies.Clear();
+
+ foreach (var proxy in proxies)
+ {
+ GradientStop gradientStop = null;
+ if (proxy.TryGetSource(out var source))
+ gradientStop = source as GradientStop;
+
+ proxy.Unsubscribe();
+
+ if (gradientStop is null)
+ continue;
+
+ if (owner is not null)
+ owner.ClearGradientStopParentIfUnused(gradientStop);
+ else
+ gradientStop.ClearRealParentAndInheritedContextIfCollected();
+ }
+ }
+ }
+
void OnGradientStopPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Invalidate();
diff --git a/src/Controls/src/Core/MultiBinding.cs b/src/Controls/src/Core/MultiBinding.cs
index 1f59b28d48f6..2b1a9fdf2cbd 100644
--- a/src/Controls/src/Core/MultiBinding.cs
+++ b/src/Controls/src/Core/MultiBinding.cs
@@ -20,6 +20,7 @@ public sealed class MultiBinding : BindingBase
BindableObject _targetObject;
BindableObject _proxyObject;
BindableProperty[] _bpProxies;
+ SetterSpecificity _specificity;
bool _applying;
///
@@ -114,7 +115,12 @@ internal override void Apply(bool fromTarget)
return;
}
// ManualValueSetter specificity ensures TwoWay bindings continue updating after ConvertBack.
- _targetObject.SetValueCore(_targetProperty, value, SetValueFlags.ClearDynamicResource, BindableObject.SetValuePrivateFlags.Default | BindableObject.SetValuePrivateFlags.Converted, specificity: SetterSpecificity.ManualValueSetter);
+ // BindingContext bindings retain binding specificity so inherited-context cleanup can distinguish
+ // them from explicit local values.
+ var specificity = ReferenceEquals(_targetProperty, BindableObject.BindingContextProperty)
+ ? _specificity
+ : SetterSpecificity.ManualValueSetter;
+ _targetObject.SetValueCore(_targetProperty, value, SetValueFlags.ClearDynamicResource, BindableObject.SetValuePrivateFlags.Default | BindableObject.SetValuePrivateFlags.Converted, specificity);
_applying = false;
}
}
@@ -151,6 +157,7 @@ internal override void Apply(object context, BindableObject targetObject, Bindab
throw new InvalidOperationException("Cannot apply MultiBinding because both Converter and StringFormat are null.");
base.Apply(context, targetObject, targetProperty, fromBindingContextChanged, specificity);
+ _specificity = specificity;
if (!ReferenceEquals(_targetObject, targetObject))
{
@@ -173,7 +180,7 @@ internal override void Apply(object context, BindableObject targetObject, Bindab
_applying = false;
}
}
- _proxyObject.BindingContext = context;
+ BindableObject.SetInheritedBindingContextForBinding(_proxyObject, context);
if (this.GetRealizedMode(_targetProperty) == BindingMode.OneWayToSource)
return;
diff --git a/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs b/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs
index f1f9316cf5e7..d6dcc98f428d 100644
--- a/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs
+++ b/src/Controls/tests/Core.UnitTests/BindableObjectUnitTests.cs
@@ -1,7 +1,10 @@
using System;
using System.ComponentModel;
using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
using Microsoft.Maui.Controls.Internals;
+using Microsoft.Maui.UnitTests;
using Xunit;
using Xunit.Sdk;
@@ -75,6 +78,14 @@ internal class ToBazConverter : TypeConverter
public class BindableObjectUnitTests : BaseTestFixture
{
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static WeakReference SetParentWithBindingContext(Element child, object bindingContext)
+ {
+ var parent = new ContentView { BindingContext = bindingContext };
+ child.Parent = parent;
+ return new WeakReference(parent);
+ }
+
[Fact]
public void BindingContext()
{
@@ -720,6 +731,132 @@ public void BindingContextGetter()
Assert.Same(label.BindingContext, label.GetValue(BindableObject.BindingContextProperty));
}
+ [Fact]
+ public async Task BoundBindingContextReturnsNullWhileInheritedCleanupIsPending()
+ {
+ bool dispatchAccepted = false;
+ int dispatchAttempts = 0;
+ Action dispatchedCleanup = null;
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = action => dispatchedCleanup = action;
+ DispatcherProviderStubOptions.DispatchResult = () =>
+ {
+ dispatchAttempts++;
+ return dispatchAccepted;
+ };
+
+ MockBindable bindable;
+ try
+ {
+ bindable = new MockBindable();
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ DispatcherProviderStubOptions.DispatchResult = null;
+ }
+
+ var inheritedContext = new MockViewModel { Text = "FooBar" };
+ bindable.SetBinding(BindableObject.BindingContextProperty, nameof(MockViewModel.Text));
+ var weakParent = SetParentWithBindingContext(bindable, inheritedContext);
+ int bindingContextChanged = 0;
+ bindable.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.Equal("FooBar", bindable.BindingContext);
+ Assert.False(await weakParent.WaitForCollect(), "Parent should not be alive!");
+
+ Assert.Null(bindable.Parent);
+ Assert.Null(bindable.BindingContext);
+ Assert.Null(dispatchedCleanup);
+ Assert.True(dispatchAttempts > 0);
+ Assert.Equal(0, bindingContextChanged);
+
+ int attemptsBeforeAcceptedDispatch = dispatchAttempts;
+ dispatchAccepted = true;
+ Assert.Null(bindable.BindingContext);
+ Assert.NotNull(dispatchedCleanup);
+ Assert.Equal(attemptsBeforeAcceptedDispatch + 1, dispatchAttempts);
+ Assert.Equal(0, bindingContextChanged);
+
+ for (int i = 0; i < 10; i++)
+ Assert.Null(bindable.BindingContext);
+
+ Assert.Equal(attemptsBeforeAcceptedDispatch + 1, dispatchAttempts);
+
+ dispatchedCleanup();
+
+ Assert.Null(bindable.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(inheritedContext);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task CleanupLeavesPendingWhenDispatcherIsDisposed(bool throwFromIsDispatchRequired)
+ {
+ bool dispatcherDisposed = true;
+ bool dispatchRequired = true;
+ DispatcherProviderStubOptions.IsInvokeRequired = () =>
+ {
+ if (dispatcherDisposed && throwFromIsDispatchRequired)
+ throw new ObjectDisposedException("dispatcher");
+
+ return dispatchRequired;
+ };
+ DispatcherProviderStubOptions.DispatchResult = () =>
+ {
+ if (dispatcherDisposed && !throwFromIsDispatchRequired)
+ throw new ObjectDisposedException("dispatcher");
+
+ return true;
+ };
+
+ MockBindable bindable;
+ try
+ {
+ bindable = new MockBindable();
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.DispatchResult = null;
+ }
+
+ var inheritedContext = new MockViewModel { Text = "FooBar" };
+ bindable.SetBinding(BindableObject.BindingContextProperty, nameof(MockViewModel.Text));
+ var weakParent = SetParentWithBindingContext(bindable, inheritedContext);
+ int bindingContextChanged = 0;
+ bindable.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.Equal("FooBar", bindable.BindingContext);
+ Assert.False(await weakParent.WaitForCollect(), "Parent should not be alive!");
+
+ var exception = Record.Exception(bindable.ClearRealParentAndInheritedContextIfCollected);
+
+ Assert.Null(exception);
+
+ Element parent = null;
+ object bindingContext = new();
+ var parentException = Record.Exception(() => parent = bindable.Parent);
+ var bindingContextException = Record.Exception(() => bindingContext = bindable.BindingContext);
+
+ Assert.Null(parentException);
+ Assert.Null(bindingContextException);
+ Assert.Null(parent);
+ Assert.Null(bindingContext);
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal("FooBar", bindable.GetValue(BindableObject.BindingContextProperty));
+
+ dispatcherDisposed = false;
+ dispatchRequired = false;
+
+ Assert.Null(bindable.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(inheritedContext);
+ }
+
[Fact]
public void BoundBindingContextUpdate()
{
diff --git a/src/Controls/tests/Core.UnitTests/DispatcherExtensionsTests.cs b/src/Controls/tests/Core.UnitTests/DispatcherExtensionsTests.cs
index 868c8ebd6f0c..1ff202eae09b 100644
--- a/src/Controls/tests/Core.UnitTests/DispatcherExtensionsTests.cs
+++ b/src/Controls/tests/Core.UnitTests/DispatcherExtensionsTests.cs
@@ -1,6 +1,8 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.Maui.Dispatching;
+using Microsoft.Maui.UnitTests;
using NSubstitute;
using Xunit;
@@ -8,6 +10,80 @@ namespace Microsoft.Maui.Controls.Core.UnitTests;
public class DispatcherExtensionsTest : BaseTestFixture
{
+ [Fact]
+ public void DispatcherGetterPreservesDispatcherCapturedDuringHandlerAttachment()
+ {
+ MockBindable parent;
+ MockBindable bindable;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ parent = new MockBindable();
+ bindable = new MockBindable { Parent = parent };
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ using var lookupEntered = new ManualResetEventSlim();
+ using var releaseLookup = new ManualResetEventSlim();
+ var backgroundDispatcher = Substitute.For();
+ var parentServices = Substitute.For();
+ parentServices.GetService(typeof(IDispatcher)).Returns(_ =>
+ {
+ lookupEntered.Set();
+ releaseLookup.Wait();
+ return backgroundDispatcher;
+ });
+ var parentMauiContext = Substitute.For();
+ parentMauiContext.Services.Returns(parentServices);
+ var parentHandler = Substitute.For();
+ IMauiContext currentParentMauiContext = null;
+ parentHandler.MauiContext.Returns(_ => currentParentMauiContext);
+ parent.Handler = parentHandler;
+ currentParentMauiContext = parentMauiContext;
+
+ var attachedDispatcher = Substitute.For();
+ var attachedServices = Substitute.For();
+ attachedServices.GetService(typeof(IDispatcher)).Returns(attachedDispatcher);
+ var attachedMauiContext = Substitute.For();
+ attachedMauiContext.Services.Returns(attachedServices);
+ var attachedHandler = Substitute.For();
+ attachedHandler.MauiContext.Returns(attachedMauiContext);
+
+ IDispatcher resolvedDispatcher = null;
+ Exception getterException = null;
+ var getterThread = new Thread(() =>
+ {
+ try
+ {
+ resolvedDispatcher = bindable.Dispatcher;
+ }
+ catch (Exception exception)
+ {
+ getterException = exception;
+ }
+ });
+ getterThread.IsBackground = true;
+
+ getterThread.Start();
+ try
+ {
+ Assert.True(lookupEntered.Wait(TimeSpan.FromSeconds(5)), "Dispatcher lookup did not start.");
+ bindable.Handler = attachedHandler;
+ }
+ finally
+ {
+ releaseLookup.Set();
+ }
+
+ Assert.True(getterThread.Join(TimeSpan.FromSeconds(5)), "Dispatcher lookup did not complete.");
+ Assert.Null(getterException);
+ Assert.Same(attachedDispatcher, resolvedDispatcher);
+ Assert.Same(attachedDispatcher, bindable.Dispatcher);
+ }
+
[Fact]
public void DispatchIfRequired_ShouldCallDispatch_WhenDispatchIsRequired()
{
diff --git a/src/Controls/tests/Core.UnitTests/GradientBrushMemoryTests.cs b/src/Controls/tests/Core.UnitTests/GradientBrushMemoryTests.cs
new file mode 100644
index 000000000000..a206b4e244d5
--- /dev/null
+++ b/src/Controls/tests/Core.UnitTests/GradientBrushMemoryTests.cs
@@ -0,0 +1,1244 @@
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Maui.Dispatching;
+using Microsoft.Maui.UnitTests;
+using NSubstitute;
+using Xunit;
+
+namespace Microsoft.Maui.Controls.Core.UnitTests
+{
+ public class GradientBrushMemoryTests : BaseTestFixture
+ {
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static WeakReference CreateBrushWithSharedGradientStops(GradientStopCollection sharedStops, object bindingContext = null)
+ {
+ var brush = new LinearGradientBrush { GradientStops = sharedStops };
+ if (bindingContext is not null)
+ brush.BindingContext = bindingContext;
+
+ return new WeakReference(brush);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static (WeakReference Brush, WeakReference BindingContext) CreateBrushWithNewBindingContext(
+ GradientStopCollection sharedStops)
+ {
+ var bindingContext = new GradientStop { Offset = 0.25f };
+ var brush = new LinearGradientBrush
+ {
+ BindingContext = bindingContext,
+ GradientStops = sharedStops
+ };
+
+ return (new WeakReference(brush), new WeakReference(bindingContext));
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static WeakReference CreateBrushWithHandlerAndSharedGradientStops(
+ GradientStopCollection sharedStops,
+ object bindingContext,
+ IMauiContext mauiContext)
+ {
+ var handler = new ElementHandlerStub();
+ handler.SetMauiContext(mauiContext);
+ var brush = new LinearGradientBrush
+ {
+ BindingContext = bindingContext,
+ Handler = handler,
+ GradientStops = sharedStops
+ };
+
+ return new WeakReference(brush);
+ }
+
+ static IMauiContext CreateMauiContext(IDispatcher dispatcher)
+ {
+ var services = Substitute.For();
+ services.GetService(typeof(IDispatcher)).Returns(dispatcher);
+ var mauiContext = Substitute.For();
+ mauiContext.Services.Returns(services);
+ return mauiContext;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ static WeakReference CreateFinalizerBlocker(
+ ManualResetEventSlim finalizerEntered,
+ ManualResetEventSlim releaseFinalizer)
+ {
+ var blocker = new FinalizerBlocker(finalizerEntered, releaseFinalizer);
+ return new WeakReference(blocker);
+ }
+
+ [Fact]
+ public async Task GradientBrushDoesNotLeakWhenSharingGradientStops()
+ {
+ // A long-lived/shared GradientStopCollection, exactly as the issue describes.
+ var sharedStops = new GradientStopCollection
+ {
+ new GradientStop()
+ };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops);
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushDoesNotLeaveStaleGradientStopParent()
+ {
+ ApplicationExtensions.CreateAndSetMockApplication();
+ try
+ {
+ var stop = new GradientStop();
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops);
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Null(stop.Parent);
+ Assert.DoesNotContain(MockApplication.MockLogger.Messages,
+ message => message.Contains("RealParent", StringComparison.Ordinal));
+ GC.KeepAlive(sharedStops);
+ }
+ finally
+ {
+ Application.ClearCurrent();
+ }
+ }
+
+ [Fact]
+ public async Task CollectedBrushClearsGradientStopInheritedBindingContext()
+ {
+ var bindingContext = new GradientStop { Offset = 0.25f };
+ var stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+
+ Assert.Same(bindingContext, stop.BindingContext);
+ Assert.Equal(0.25f, stop.Offset);
+
+ bindingContext.Offset = 0.5f;
+ Assert.Equal(0.5f, stop.Offset);
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Null(stop.Parent);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+
+ bindingContext.Offset = 0.75f;
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushClearsGradientStopBindingContextPropertyBindingSource()
+ {
+ var inheritedContext = new GradientStop { Offset = 0.25f };
+ var stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops, inheritedContext);
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.Equal(0.25f, stop.BindingContext);
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Null(stop.Parent);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+
+ inheritedContext.Offset = 0.5f;
+ Assert.Null(stop.BindingContext);
+ GC.KeepAlive(inheritedContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushWithoutDispatcherReleasesBindingContextPropertyBindingSource()
+ {
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ WeakReference weakBindingContext;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ (weakBrush, weakBindingContext) = CreateBrushWithNewBindingContext(sharedStops);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.False(await weakBindingContext.WaitForCollect(), "Inherited binding context should not be alive!");
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.GetValue(BindableObject.BindingContextProperty));
+
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushWithoutDispatcherReleasesMultiBindingContextPropertyBindingSource()
+ {
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ WeakReference weakBindingContext;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, new MultiBinding
+ {
+ StringFormat = "{0}",
+ Bindings =
+ {
+ new Binding(nameof(GradientStop.Offset))
+ }
+ });
+ sharedStops = new GradientStopCollection { stop };
+ (weakBrush, weakBindingContext) = CreateBrushWithNewBindingContext(sharedStops);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.False(await weakBindingContext.WaitForCollect(), "Inherited binding context should not be alive!");
+ Assert.Equal(0, bindingContextChanged);
+
+ Assert.Equal(string.Empty, stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public void CollectedInheritedContextStillClearsAppliedBindingValues()
+ {
+ using var finalizerEntered = new ManualResetEventSlim();
+ using var releaseFinalizer = new ManualResetEventSlim();
+ CreateFinalizerBlocker(finalizerEntered, releaseFinalizer);
+ GC.Collect();
+
+ Assert.True(finalizerEntered.Wait(TimeSpan.FromSeconds(5)), "Finalizer blocker did not start.");
+
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ WeakReference weakBindingContext;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ (weakBrush, weakBindingContext) = CreateBrushWithNewBindingContext(sharedStops);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ try
+ {
+ for (int attempt = 0;
+ attempt < 20 && (weakBrush.IsAlive || weakBindingContext.IsAlive);
+ attempt++)
+ {
+ GC.Collect();
+ Thread.Sleep(10);
+ }
+
+ Assert.False(weakBrush.IsAlive, "LinearGradientBrush should not be alive!");
+ Assert.False(weakBindingContext.IsAlive, "Inherited binding context should not be alive!");
+ Assert.Equal(0.25f, stop.GetValue(GradientStop.OffsetProperty));
+ Assert.Equal(0, bindingContextChanged);
+ }
+ finally
+ {
+ releaseFinalizer.Set();
+ GC.WaitForPendingFinalizers();
+ }
+
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.GetValue(GradientStop.OffsetProperty));
+
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushDispatchesGradientStopInheritedBindingContextCleanup()
+ {
+ var dispatchedCleanup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = action => dispatchedCleanup.TrySetResult(action);
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ var cleanup = await dispatchedCleanup.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ cleanup();
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushUsesDispatcherAttachedAfterGradientStopCreation()
+ {
+ var dispatchedCleanup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var dispatcher = new DispatcherStub(
+ () => true,
+ action => dispatchedCleanup.TrySetResult(action));
+ var mauiContext = CreateMauiContext(dispatcher);
+ var handler = Substitute.For();
+ handler.MauiContext.Returns(mauiContext);
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ stop.Handler = handler;
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ var cleanup = await dispatchedCleanup.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ cleanup();
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushFinalizerDoesNotResolveLateHandlerDispatcher()
+ {
+ var dispatchedCleanup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var dispatcher = new DispatcherStub(
+ () => true,
+ action => dispatchedCleanup.TrySetResult(action));
+ int dispatcherAvailable = 0;
+ int dispatcherResolutionCount = 0;
+ var services = Substitute.For();
+ services.GetService(typeof(IDispatcher)).Returns(_ =>
+ {
+ if (Volatile.Read(ref dispatcherAvailable) == 0)
+ return null;
+
+ Interlocked.Increment(ref dispatcherResolutionCount);
+ return dispatcher;
+ });
+ var mauiContext = Substitute.For();
+ mauiContext.Services.Returns(services);
+ var handler = Substitute.For();
+ handler.MauiContext.Returns(mauiContext);
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ stop.Handler = handler;
+ Volatile.Write(ref dispatcherAvailable, 1);
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Equal(0, Volatile.Read(ref dispatcherResolutionCount));
+ Assert.False(dispatchedCleanup.Task.IsCompleted);
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, Volatile.Read(ref dispatcherResolutionCount));
+ var cleanup = await dispatchedCleanup.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(0, bindingContextChanged);
+ cleanup();
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectedBrushUsesDispatcherAvailableWhenParentIsAssigned()
+ {
+ var dispatchedCleanup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var dispatcher = new DispatcherStub(
+ () => true,
+ action => dispatchedCleanup.TrySetResult(action));
+ var mauiContext = CreateMauiContext(dispatcher);
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ DispatcherProviderStubOptions.SkipDispatcherCreation = true;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithHandlerAndSharedGradientStops(
+ sharedStops,
+ bindingContext,
+ mauiContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.SkipDispatcherCreation = false;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ var cleanup = await dispatchedCleanup.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ cleanup();
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task BindingContextAccessBeforeDispatchedCleanupDoesNotRunCallbacks()
+ {
+ var dispatchedCleanup = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = action => dispatchedCleanup.TrySetResult(action);
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => Interlocked.Increment(ref bindingContextChanged);
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ var cleanup = await dispatchedCleanup.Task.WaitAsync(TimeSpan.FromSeconds(5));
+
+ Assert.Null(await Task.Run(() => stop.BindingContext));
+ int callbacksBeforeCleanup = Volatile.Read(ref bindingContextChanged);
+
+ cleanup();
+
+ Assert.Equal(0, callbacksBeforeCleanup);
+ Assert.Equal(1, Volatile.Read(ref bindingContextChanged));
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task ParentAccessAfterCollectedBrushClearsGradientStopInheritedBindingContext()
+ {
+ var bindingContext = new GradientStop { Offset = 0.25f };
+ var stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ Assert.Null(stop.Parent);
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Equal(0f, stop.Offset);
+
+ bindingContext.Offset = 0.75f;
+ Assert.Equal(0f, stop.Offset);
+ Assert.Null(stop.BindingContext);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task FailedDispatchLeavesGradientStopInheritedBindingContextCleanupPending()
+ {
+ bool dispatchRequired = true;
+ DispatcherProviderStubOptions.IsInvokeRequired = () => dispatchRequired;
+ DispatcherProviderStubOptions.DispatchResult = () => false;
+
+ GradientStop bindingContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ try
+ {
+ bindingContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.DispatchResult = null;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ Assert.Null(stop.Parent);
+
+ Assert.Equal(0, bindingContextChanged);
+ Assert.Equal(0.25f, stop.Offset);
+
+ dispatchRequired = false;
+ Assert.Null(stop.Parent);
+
+ Assert.Equal(1, bindingContextChanged);
+ Assert.Equal(0f, stop.Offset);
+
+ bindingContext.Offset = 0.75f;
+ Assert.Equal(0f, stop.Offset);
+ Assert.Null(stop.BindingContext);
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public void ParentAccessBeforeSubscriptionFinalizerClearsInheritedBindingContext()
+ {
+ using var finalizerEntered = new ManualResetEventSlim();
+ using var releaseFinalizer = new ManualResetEventSlim();
+ CreateFinalizerBlocker(finalizerEntered, releaseFinalizer);
+ GC.Collect();
+
+ Assert.True(finalizerEntered.Wait(TimeSpan.FromSeconds(5)), "Finalizer blocker did not start.");
+
+ var bindingContext = new GradientStop { Offset = 0.25f };
+ var stop = new GradientStop();
+ stop.SetBinding(GradientStop.OffsetProperty, nameof(GradientStop.Offset));
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops, bindingContext);
+
+ try
+ {
+ GC.Collect();
+
+ Assert.False(weakBrush.IsAlive, "LinearGradientBrush should not be alive!");
+ Assert.Null(stop.Parent);
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(0f, stop.Offset);
+
+ bindingContext.Offset = 0.75f;
+ Assert.Equal(0f, stop.Offset);
+ }
+ finally
+ {
+ releaseFinalizer.Set();
+ GC.WaitForPendingFinalizers();
+ }
+
+ GC.KeepAlive(bindingContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task CollectingPreviousBrushPreservesCurrentGradientStopParent()
+ {
+ var stop = new GradientStop();
+ var firstStops = new GradientStopCollection { stop };
+ var weakFirstBrush = CreateBrushWithSharedGradientStops(firstStops);
+ var secondBindingContext = new object();
+ var secondBrush = new LinearGradientBrush
+ {
+ BindingContext = secondBindingContext,
+ GradientStops = new GradientStopCollection { stop }
+ };
+
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.False(await weakFirstBrush.WaitForCollect(), "Previous LinearGradientBrush should not be alive!");
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.Same(secondBindingContext, stop.BindingContext);
+ GC.KeepAlive(firstStops);
+ GC.KeepAlive(secondBrush);
+ }
+
+ [Fact]
+ public async Task CollectingPreviousBrushPreservesCurrentGradientStopBindingContextPropertyBindingSource()
+ {
+ var stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ var firstBindingContext = new GradientStop { Offset = 0.25f };
+ var firstStops = new GradientStopCollection { stop };
+ var weakFirstBrush = CreateBrushWithSharedGradientStops(firstStops, firstBindingContext);
+ var secondBindingContext = new GradientStop { Offset = 0.5f };
+ var secondBrush = new LinearGradientBrush
+ {
+ BindingContext = secondBindingContext,
+ GradientStops = new GradientStopCollection { stop }
+ };
+
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.Equal(0.5f, stop.BindingContext);
+ Assert.False(await weakFirstBrush.WaitForCollect(), "Previous LinearGradientBrush should not be alive!");
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.Equal(0.5f, stop.BindingContext);
+ GC.KeepAlive(firstStops);
+ GC.KeepAlive(firstBindingContext);
+ GC.KeepAlive(secondBrush);
+ GC.KeepAlive(secondBindingContext);
+ }
+
+ [Fact]
+ public async Task ClearingManualBindingContextAfterCollectedBrushDoesNotRestoreStaleBindingValue()
+ {
+ var inheritedContext = new GradientStop { Offset = 0.25f };
+ var stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ var sharedStops = new GradientStopCollection { stop };
+ var weakBrush = CreateBrushWithSharedGradientStops(sharedStops, inheritedContext);
+ var manualBindingContext = stop.BindingContext;
+ stop.BindingContext = manualBindingContext;
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.Null(stop.Parent);
+ Assert.Same(manualBindingContext, stop.BindingContext);
+ Assert.Equal(0, bindingContextChanged);
+
+ stop.ClearValue(BindableObject.BindingContextProperty);
+
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(inheritedContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task ReplacingBindingContextBindingWhileCleanupIsPendingUsesNullInheritedSource()
+ {
+ var dispatchedCleanups = new ConcurrentQueue();
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = dispatchedCleanups.Enqueue;
+
+ GradientStop inheritedContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ try
+ {
+ inheritedContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, inheritedContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.True(dispatchedCleanups.TryDequeue(out var cleanup));
+
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+
+ Assert.Null(stop.GetValue(BindableObject.BindingContextProperty));
+ Assert.Equal(1, bindingContextChanged);
+
+ cleanup();
+
+ Assert.Null(stop.GetValue(BindableObject.BindingContextProperty));
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(inheritedContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task RemovingBindingContextBindingWhileCleanupIsPendingClearsStoredBindingValue()
+ {
+ var dispatchedCleanups = new ConcurrentQueue();
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = dispatchedCleanups.Enqueue;
+
+ GradientStop inheritedContext;
+ GradientStop stop;
+ GradientStopCollection sharedStops;
+ WeakReference weakBrush;
+ try
+ {
+ inheritedContext = new GradientStop { Offset = 0.25f };
+ stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ sharedStops = new GradientStopCollection { stop };
+ weakBrush = CreateBrushWithSharedGradientStops(sharedStops, inheritedContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ }
+
+ var manualBindingContext = stop.BindingContext;
+ stop.BindingContext = manualBindingContext;
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakBrush.WaitForCollect(), "LinearGradientBrush should not be alive!");
+ Assert.True(dispatchedCleanups.TryDequeue(out var cleanup));
+
+ stop.RemoveBinding(BindableObject.BindingContextProperty);
+
+ Assert.Same(manualBindingContext, stop.BindingContext);
+ Assert.Equal(0, bindingContextChanged);
+
+ cleanup();
+ stop.ClearValue(BindableObject.BindingContextProperty);
+
+ Assert.Null(stop.BindingContext);
+ Assert.Equal(1, bindingContextChanged);
+ GC.KeepAlive(inheritedContext);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public async Task StaleDispatchedCleanupDoesNotClearNewPendingBindingContextCleanup()
+ {
+ var dispatchedCleanups = new ConcurrentQueue();
+ DispatcherProviderStubOptions.IsInvokeRequired = () => true;
+ DispatcherProviderStubOptions.InvokeOnMainThread = dispatchedCleanups.Enqueue;
+
+ GradientStop stop;
+ GradientStop firstBindingContext;
+ GradientStopCollection firstStops;
+ WeakReference weakFirstBrush;
+ try
+ {
+ stop = new GradientStop();
+ stop.SetBinding(BindableObject.BindingContextProperty, nameof(GradientStop.Offset));
+ firstBindingContext = new GradientStop { Offset = 0.25f };
+ firstStops = new GradientStopCollection { stop };
+ weakFirstBrush = CreateBrushWithSharedGradientStops(firstStops, firstBindingContext);
+ }
+ finally
+ {
+ DispatcherProviderStubOptions.IsInvokeRequired = null;
+ DispatcherProviderStubOptions.InvokeOnMainThread = null;
+ }
+
+ int bindingContextChanged = 0;
+ stop.BindingContextChanged += (_, _) => bindingContextChanged++;
+
+ Assert.False(await weakFirstBrush.WaitForCollect(), "First LinearGradientBrush should not be alive!");
+ Assert.True(dispatchedCleanups.TryDequeue(out var firstCleanup));
+
+ var secondBindingContext = new GradientStop { Offset = 0.5f };
+ var secondStops = new GradientStopCollection { stop };
+ var weakSecondBrush = CreateBrushWithSharedGradientStops(secondStops, secondBindingContext);
+ while (dispatchedCleanups.TryDequeue(out var duplicateFirstCleanup))
+ duplicateFirstCleanup();
+ int changesAfterReparenting = bindingContextChanged;
+
+ Assert.False(await weakSecondBrush.WaitForCollect(), "Second LinearGradientBrush should not be alive!");
+ Assert.True(dispatchedCleanups.TryDequeue(out var secondCleanup));
+
+ firstCleanup();
+
+ Assert.Equal(changesAfterReparenting, bindingContextChanged);
+ Assert.Equal(0.5f, stop.GetValue(BindableObject.BindingContextProperty));
+
+ secondCleanup();
+
+ Assert.Equal(changesAfterReparenting + 1, bindingContextChanged);
+ Assert.Null(stop.GetValue(BindableObject.BindingContextProperty));
+ GC.KeepAlive(firstStops);
+ GC.KeepAlive(firstBindingContext);
+ GC.KeepAlive(secondStops);
+ GC.KeepAlive(secondBindingContext);
+ }
+
+ [Fact]
+ public async Task GradientStopChangesStillInvalidateAfterGc()
+ {
+ var stop = new GradientStop();
+ var brush = new LinearGradientBrush
+ {
+ GradientStops = new GradientStopCollection { stop }
+ };
+ bool invalidated = false;
+ brush.InvalidateGradientBrushRequested += (_, __) => invalidated = true;
+
+ await TestHelpers.Collect();
+
+ stop.Offset = 0.5f;
+
+ Assert.True(invalidated);
+ GC.KeepAlive(brush);
+ }
+
+ [Fact]
+ public async Task SharedGradientStopsInvalidateEachLiveBrushAfterGc()
+ {
+ var stop = new GradientStop();
+ var sharedStops = new GradientStopCollection { stop };
+ var firstBrush = new LinearGradientBrush { GradientStops = sharedStops };
+ var secondBrush = new LinearGradientBrush { GradientStops = sharedStops };
+ int firstInvalidationCount = 0;
+ int secondInvalidationCount = 0;
+ firstBrush.InvalidateGradientBrushRequested += (_, __) => firstInvalidationCount++;
+ secondBrush.InvalidateGradientBrushRequested += (_, __) => secondInvalidationCount++;
+
+ await TestHelpers.Collect();
+
+ stop.Offset = 0.5f;
+
+ Assert.Equal(1, firstInvalidationCount);
+ Assert.Equal(1, secondInvalidationCount);
+ GC.KeepAlive(firstBrush);
+ GC.KeepAlive(secondBrush);
+ }
+
+ [Fact]
+ public async Task AliveBrushStillInvalidatesAfterSiblingBrushIsCollected()
+ {
+ var stop = new GradientStop();
+ var sharedStops = new GradientStopCollection { stop };
+ var weakCollectedBrush = CreateBrushWithSharedGradientStops(sharedStops);
+ var aliveBrush = new LinearGradientBrush { GradientStops = sharedStops };
+ int invalidationCount = 0;
+ aliveBrush.InvalidateGradientBrushRequested += (_, __) => invalidationCount++;
+
+ Assert.False(await weakCollectedBrush.WaitForCollect(), "Sibling LinearGradientBrush should not be alive!");
+
+ stop.Offset = 0.5f;
+
+ Assert.Equal(1, invalidationCount);
+ GC.KeepAlive(aliveBrush);
+ GC.KeepAlive(sharedStops);
+ }
+
+ [Fact]
+ public void RemovingAndReplacingGradientStopsMovesSubscriptions()
+ {
+ var removedStop = new GradientStop { Offset = 0.1f };
+ var retainedStop = new GradientStop { Offset = 0.2f };
+ var oldStops = new GradientStopCollection { removedStop, retainedStop };
+ var brush = new LinearGradientBrush { GradientStops = oldStops };
+ int invalidationCount = 0;
+ brush.InvalidateGradientBrushRequested += (_, __) => invalidationCount++;
+
+ oldStops.Remove(removedStop);
+ invalidationCount = 0;
+
+ removedStop.Offset = 0.3f;
+ Assert.Equal(0, invalidationCount);
+
+ retainedStop.Offset = 0.4f;
+ Assert.Equal(1, invalidationCount);
+
+ var replacementStop = new GradientStop { Offset = 0.1f };
+ brush.GradientStops = new GradientStopCollection { replacementStop };
+ invalidationCount = 0;
+
+ retainedStop.Offset = 0.5f;
+ oldStops.Add(new GradientStop());
+
+ Assert.Equal(0, invalidationCount);
+
+ replacementStop.Offset = 0.6f;
+
+ Assert.Equal(1, invalidationCount);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReplacingGradientStopsInvalidatesBrush(bool replaceWithNull)
+ {
+ var brush = new LinearGradientBrush
+ {
+ GradientStops = new GradientStopCollection { new GradientStop() }
+ };
+ int invalidationCount = 0;
+ brush.InvalidateGradientBrushRequested += (_, __) => invalidationCount++;
+
+ brush.GradientStops = replaceWithNull ? null : new GradientStopCollection();
+
+ Assert.Equal(1, invalidationCount);
+ }
+
+ [Fact]
+ public void NullGradientStopCollectionAllowsBindingContextChange()
+ {
+ var brush = new LinearGradientBrush { GradientStops = null };
+ var bindingContext = new object();
+
+ brush.BindingContext = bindingContext;
+
+ Assert.Same(bindingContext, brush.BindingContext);
+ }
+
+ [Fact]
+ public void NullGradientStopEntryAllowsBindingContextChange()
+ {
+ var brush = new LinearGradientBrush
+ {
+ GradientStops = new GradientStopCollection { null }
+ };
+ var bindingContext = new object();
+
+ brush.BindingContext = bindingContext;
+
+ Assert.Same(bindingContext, brush.BindingContext);
+ }
+
+ [Fact]
+ public void SharedGradientStopsPreserveExistingMostRecentlyAssignedParentBehavior()
+ {
+ var stop = new GradientStop();
+ var sharedStops = new GradientStopCollection { stop };
+ var firstBindingContext = new object();
+ var firstBrush = new LinearGradientBrush
+ {
+ BindingContext = firstBindingContext,
+ GradientStops = sharedStops
+ };
+
+ Assert.Same(firstBrush, stop.Parent);
+ Assert.Same(firstBindingContext, stop.BindingContext);
+
+ var secondBindingContext = new object();
+ var secondBrush = new LinearGradientBrush
+ {
+ BindingContext = secondBindingContext,
+ GradientStops = sharedStops
+ };
+
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.Same(secondBindingContext, stop.BindingContext);
+ GC.KeepAlive(firstBrush);
+ }
+
+ [Theory]
+ [InlineData(SharedStopDetachment.ClearCollection)]
+ [InlineData(SharedStopDetachment.RemoveStop)]
+ [InlineData(SharedStopDetachment.ReplaceCollection)]
+ public void DetachingSharedStopFromPreviousBrushPreservesCurrentParent(SharedStopDetachment detachment)
+ {
+ var stop = new GradientStop();
+ var firstStops = new GradientStopCollection { stop };
+ var secondStops = new GradientStopCollection { stop };
+ var firstBrush = new LinearGradientBrush { GradientStops = firstStops };
+ var secondBindingContext = new object();
+ var secondBrush = new LinearGradientBrush
+ {
+ BindingContext = secondBindingContext,
+ GradientStops = secondStops
+ };
+
+ switch (detachment)
+ {
+ case SharedStopDetachment.ClearCollection:
+ firstStops.Clear();
+ break;
+ case SharedStopDetachment.RemoveStop:
+ firstStops.Remove(stop);
+ break;
+ case SharedStopDetachment.ReplaceCollection:
+ firstBrush.GradientStops = new GradientStopCollection();
+ break;
+ }
+
+ Assert.Same(secondBrush, stop.Parent);
+ Assert.Same(secondBindingContext, stop.BindingContext);
+ }
+
+ [Fact]
+ public void DuplicateGradientStopsPreserveOccurrenceSubscriptions()
+ {
+ var sharedStop = new GradientStop();
+ var stops = new GradientStopCollection { sharedStop, sharedStop };
+ var brush = new LinearGradientBrush { GradientStops = stops };
+ int invalidationCount = 0;
+ brush.InvalidateGradientBrushRequested += (_, __) => invalidationCount++;
+
+ sharedStop.Offset = 0.1f;
+ Assert.Equal(2, invalidationCount);
+ Assert.Same(brush, sharedStop.Parent);
+
+ stops.Remove(sharedStop);
+ invalidationCount = 0;
+
+ sharedStop.Offset = 0.2f;
+ Assert.Equal(1, invalidationCount);
+ Assert.Same(brush, sharedStop.Parent);
+
+ stops.Remove(sharedStop);
+ invalidationCount = 0;
+
+ sharedStop.Offset = 0.3f;
+ Assert.Equal(0, invalidationCount);
+ Assert.Null(sharedStop.Parent);
+ }
+
+ [Theory]
+ [InlineData(ValueEqualStopDetachment.RemoveStop)]
+ [InlineData(ValueEqualStopDetachment.ReplaceStop)]
+ [InlineData(ValueEqualStopDetachment.ReplaceCollection)]
+ public void DetachingValueEqualStopClearsRemovedParent(ValueEqualStopDetachment detachment)
+ {
+ var removedStop = new GradientStop { Offset = 0.5f };
+ var equalStop = new GradientStop { Offset = 0.5f };
+ var stops = new GradientStopCollection { removedStop, equalStop };
+ var brush = new LinearGradientBrush { GradientStops = stops };
+
+ Assert.Equal(removedStop, equalStop);
+
+ switch (detachment)
+ {
+ case ValueEqualStopDetachment.RemoveStop:
+ stops.RemoveAt(0);
+ break;
+ case ValueEqualStopDetachment.ReplaceStop:
+ stops[0] = new GradientStop { Offset = 0.5f };
+ break;
+ case ValueEqualStopDetachment.ReplaceCollection:
+ brush.GradientStops = new GradientStopCollection { equalStop };
+ break;
+ }
+
+ Assert.Null(removedStop.Parent);
+ Assert.All(brush.GradientStops, stop => Assert.Same(brush, stop.Parent));
+ }
+
+ [Fact]
+ public void RemovingGradientStopAllowsReentrantReuse()
+ {
+ var stop = new GradientStop();
+ var stops = new GradientStopCollection { stop };
+ var brush = new LinearGradientBrush { GradientStops = stops };
+ bool replaced = false;
+ int invalidationCount = 0;
+ brush.InvalidateGradientBrushRequested += (_, __) =>
+ {
+ invalidationCount++;
+ if (!replaced && stop.Parent is null)
+ {
+ replaced = true;
+ brush.GradientStops = new GradientStopCollection { stop };
+ }
+ };
+
+ stops.Remove(stop);
+
+ Assert.True(replaced);
+ Assert.Same(brush, stop.Parent);
+
+ invalidationCount = 0;
+ stop.Offset = 0.5f;
+
+ Assert.Equal(1, invalidationCount);
+ }
+
+ [Fact]
+ public void ClearingGradientStopsAllowsReentrantReplacement()
+ {
+ var brush = new LinearGradientBrush
+ {
+ GradientStops = new GradientStopCollection { new GradientStop() }
+ };
+ bool replaced = false;
+ int invalidationCount = 0;
+ brush.InvalidateGradientBrushRequested += (_, __) =>
+ {
+ invalidationCount++;
+ if (!replaced && brush.GradientStops.Count == 0)
+ {
+ replaced = true;
+ brush.GradientStops = new GradientStopCollection { new GradientStop() };
+ }
+ };
+
+ brush.GradientStops.Clear();
+
+ Assert.True(replaced);
+ Assert.Single(brush.GradientStops);
+ // Clear and the reentrant replacement are distinct state changes.
+ Assert.Equal(2, invalidationCount);
+ }
+
+ [Fact]
+ public async Task ClearingAndReusingGradientStopsKeepsNewStopsSubscribed()
+ {
+ var stops = new GradientStopCollection { new GradientStop() };
+ var brush = new LinearGradientBrush
+ {
+ GradientStops = stops
+ };
+ bool invalidated = false;
+ brush.InvalidateGradientBrushRequested += (_, __) => invalidated = true;
+
+ stops.Clear();
+ var newStop = new GradientStop();
+ stops.Add(newStop);
+
+ await TestHelpers.Collect();
+ invalidated = false;
+
+ newStop.Offset = 0.5f;
+
+ Assert.True(invalidated);
+ GC.KeepAlive(brush);
+ }
+
+ public enum SharedStopDetachment
+ {
+ ClearCollection,
+ RemoveStop,
+ ReplaceCollection,
+ }
+
+ public enum ValueEqualStopDetachment
+ {
+ RemoveStop,
+ ReplaceStop,
+ ReplaceCollection,
+ }
+
+ sealed class FinalizerBlocker
+ {
+ readonly ManualResetEventSlim _finalizerEntered;
+ readonly ManualResetEventSlim _releaseFinalizer;
+
+ public FinalizerBlocker(
+ ManualResetEventSlim finalizerEntered,
+ ManualResetEventSlim releaseFinalizer)
+ {
+ _finalizerEntered = finalizerEntered;
+ _releaseFinalizer = releaseFinalizer;
+ }
+
+ ~FinalizerBlocker()
+ {
+ _finalizerEntered.Set();
+ _releaseFinalizer.Wait();
+ }
+ }
+ }
+}
diff --git a/src/Core/tests/UnitTests/TestClasses/DispatcherStub.cs b/src/Core/tests/UnitTests/TestClasses/DispatcherStub.cs
index 1de488cc13e4..cdb683505f53 100644
--- a/src/Core/tests/UnitTests/TestClasses/DispatcherStub.cs
+++ b/src/Core/tests/UnitTests/TestClasses/DispatcherStub.cs
@@ -10,11 +10,16 @@ class DispatcherStub : IDispatcher
{
readonly Func? _isInvokeRequired;
readonly Action? _invokeOnMainThread;
+ readonly Func? _dispatchResult;
- public DispatcherStub(Func? isInvokeRequired, Action? invokeOnMainThread)
+ public DispatcherStub(
+ Func? isInvokeRequired,
+ Action? invokeOnMainThread,
+ Func? dispatchResult = null)
{
_isInvokeRequired = isInvokeRequired;
_invokeOnMainThread = invokeOnMainThread;
+ _dispatchResult = dispatchResult;
ManagedThreadId = Environment.CurrentManagedThreadId;
}
@@ -26,6 +31,9 @@ public DispatcherStub(Func? isInvokeRequired, Action? invokeOnMain
public bool Dispatch(Action action)
{
+ if (_dispatchResult?.Invoke() == false)
+ return false;
+
if (_invokeOnMainThread is null)
action();
else
@@ -98,7 +106,8 @@ class DispatcherProviderStub : IDispatcherProvider, IDisposable
? null
: new DispatcherStub(
DispatcherProviderStubOptions.IsInvokeRequired,
- DispatcherProviderStubOptions.InvokeOnMainThread));
+ DispatcherProviderStubOptions.InvokeOnMainThread,
+ DispatcherProviderStubOptions.DispatchResult));
public void Dispose() =>
s_dispatcherInstance.Dispose();
@@ -126,6 +135,9 @@ public class DispatcherProviderStubOptions
[ThreadStatic]
public static Action? InvokeOnMainThread;
+
+ [ThreadStatic]
+ public static Func? DispatchResult;
}
public static class DispatcherTest