Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 35 additions & 20 deletions src/Controls/src/Core/Element/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,30 +333,45 @@ internal Element ParentOverride
}

WeakReference<Element> _realParent;
/// <summary>For internal use by .NET MAUI.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public Element RealParent

Element GetRealParent(bool logWarningIfParentHasBeenCollected = true)
{
get
if (_realParent is null)
{
if (_realParent is null)
{
return null;
}
if (_realParent.TryGetTarget(out var parent))
{
return parent;
}
else
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;

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");
}

return null;
}

return null;
}

Element GetParent(bool logWarningIfParentHasBeenCollected = true)
{
return ParentOverride ?? GetRealParent(logWarningIfParentHasBeenCollected);
}

/// <summary>For internal use by .NET MAUI.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public Element RealParent
{
get => GetRealParent();
private set
{
if (value is null)
Expand All @@ -380,15 +395,15 @@ void IElementDefinition.AddResourcesChangedListener(Action<object, ResourcesChan
/// <remarks>Most application authors will not need to set the parent element by hand.</remarks>
public Element Parent
{
get { return ParentOverride ?? RealParent; }
get { return GetParent(); }
set => SetParent(value);
}

void SetParent(Element value)
{
Element realParent = RealParent;
Element currentParent = GetParent(false);

if (realParent == value)
if (currentParent == value)
{
return;
}
Expand All @@ -397,10 +412,10 @@ void SetParent(Element value)

if (_parentOverride == null)
{
OnParentChangingCore(Parent, value);
OnParentChangingCore(currentParent, value);
}

if (realParent is IElementDefinition element)
if (currentParent is IElementDefinition element)
{
element.RemoveResourcesChangedListener(OnParentResourcesChanged);

Expand Down
102 changes: 102 additions & 0 deletions src/Controls/tests/Core.UnitTests/ElementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,5 +313,107 @@ public void ChildRemoved()
root.Children.Remove(child);
Assert.True(removed);
}

[Fact]
public void RealParent_ReturnsNullWhenNoParent()
{
var element = new TestElement();
Assert.Null(element.RealParent);
}

[Fact]
public void RealParent_ReturnsParentWhenSet()
{
var parent = new TestElement();
var child = new TestElement();

parent.Children.Add(child);

Assert.Same(parent, child.RealParent);
}

[Fact]
public void RealParent_ReturnsNullAfterParentGarbageCollected()
{
var child = new TestElement();
WeakReference parentRef;

// Create parent in separate scope to enable GC
void CreateParent()
{
var parent = new TestElement();
parentRef = new WeakReference(parent);
parent.Children.Add(child);
// parent goes out of scope here
}

CreateParent();

// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

// Verify parent was collected
Assert.False(parentRef.IsAlive);

// RealParent should return null and not throw
Assert.Null(child.RealParent);
}

[Fact]
public void RealParent_ClearsWeakReferenceAfterGarbageCollection()
{
var child = new TestElement();
WeakReference parentRef;

// Create parent in separate scope to enable GC
void CreateParent()
{
var parent = new TestElement();
parentRef = new WeakReference(parent);
parent.Children.Add(child);
}

CreateParent();

// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

// Access RealParent multiple times - should not repeatedly log warnings
Assert.Null(child.RealParent);
Assert.Null(child.RealParent);
Assert.Null(child.RealParent);
}

[Fact]
public void SetParent_DoesNotLogWarningWhenParentGarbageCollected()
{
var child = new TestElement();
WeakReference parentRef;

// Create parent in separate scope
void CreateParent()
{
var parent = new TestElement();
parentRef = new WeakReference(parent);
parent.Children.Add(child);
}

CreateParent();

// Force garbage collection
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

// Setting parent should not log warnings internally
var newParent = new TestElement();
newParent.Children.Add(child);

Assert.Same(newParent, child.RealParent);
}
}
}