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
19 changes: 15 additions & 4 deletions src/Controls/src/Core/Border/Border.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ public class Border : View, IContentView, IBorderView, IPaddingElement, ISafeAre
PropertyChangedEventHandler? _strokeShapeChanged;
WeakNotifyPropertyChangedProxy? _strokeProxy = null;
PropertyChangedEventHandler? _strokeChanged;
WeakNotifyCollectionChangedProxy? _strokeDashArrayProxy = null;
NotifyCollectionChangedEventHandler? _strokeDashArrayChanged;

~Border()
{
_strokeShapeProxy?.Unsubscribe();
_strokeProxy?.Unsubscribe();
_strokeDashArrayProxy?.Unsubscribe();
}

/// <summary>Bindable property for <see cref="Content"/>.</summary>
Expand Down Expand Up @@ -310,13 +313,21 @@ public float[]? StrokeDashPattern
{
get
{
if (StrokeDashArray is INotifyCollectionChanged oldCollection)
oldCollection.CollectionChanged -= OnStrokeDashArrayChanged;

_strokeDashPattern = StrokeDashArray?.ToFloatArray();

// Subscribe weakly so a shared/long-lived DoubleCollection does not strongly
// root this Border for the collection's lifetime. The proxy is torn down in
// ~Border(), mirroring the Stroke/StrokeShape weak proxies above.
if (StrokeDashArray is INotifyCollectionChanged newCollection)
newCollection.CollectionChanged += OnStrokeDashArrayChanged;
{
_strokeDashArrayChanged ??= OnStrokeDashArrayChanged;
_strokeDashArrayProxy ??= new();
_strokeDashArrayProxy.Subscribe(newCollection, _strokeDashArrayChanged);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 AI-Generated Review (multi-model)

[moderate] Performance-Critical Path Optimization -- Avoid re-subscribing the weak collection proxy on every StrokeDashPattern read when the dash collection has not changed. WeakNotifyCollectionChangedProxy.Subscribe always detaches/re-attaches the event and replaces its source/handler WeakReferences, so each platform mapper read (including the read triggered by OnStrokeDashArrayChanged after every collection mutation) allocates additional weak references and churns event subscriptions. Guard with the current proxy source (for example, only call Subscribe when it is not already subscribed to newCollection) while still unsubscribing when the collection changes or becomes null.

}
else
{
_strokeDashArrayProxy?.Unsubscribe();
}

return _strokeDashPattern;
}
Expand Down
25 changes: 25 additions & 0 deletions src/Controls/tests/Core.UnitTests/BorderUnitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#nullable enable
using System;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Maui.Controls.Core.UnitTests
Expand Down Expand Up @@ -44,5 +46,28 @@ public void ChildrenHaveParentsWhenContentIsSet()
border.Content = null;
Assert.Null(label.Parent);
}

[Fact]
public async Task SharedStrokeDashArrayDoesNotLeakBorder()
{
// A shared / long-lived DoubleCollection, exactly as the issue describes
// (e.g. a static field or a value reused from a ResourceDictionary).
var sharedDashArray = new DoubleCollection { 2, 2 };

WeakReference weakBorder;
{
var border = new Border { StrokeDashArray = sharedDashArray };

// Reading StrokeDashPattern installs the CollectionChanged subscription,
// exactly as the platform border handler does when rendering the dash.
_ = border.StrokeDashPattern;

weakBorder = new WeakReference(border);
// Drop the only strong reference to `border`; `sharedDashArray` stays alive.
}

Assert.False(await weakBorder.WaitForCollect(), "Border should not be alive!");
GC.KeepAlive(sharedDashArray);
}
}
}
Loading