Repository navigation
Use weak events for FormattedString span subscriptions - #38408
wbv-profile wants to merge 1 commit into
Conversation
FormattedString subscribed to PropertyChanged and PropertyChanging on every Span in its collection with plain CLR handlers. A Span the app holds on to -- or shares between FormattedStrings -- therefore rooted the FormattedString and the view that owned it. Those per-span subscriptions now go through WeakNotifyPropertyChanged- Proxy and WeakNotifyPropertyChangingProxy, and the type gains the finalizer they require. The _spans collection subscription in the constructor is deliberately left alone: SpanCollection is private and owned by this instance, so that handler cannot outlive it. It now carries a comment saying so, since the surrounding code is changing for the opposite reason. SpanCollection.ClearItems already raises Remove with the removed items rather than a bare Reset, so unlike GradientBrush there is no gap on Clear(); a test pins that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 38408Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 38408" |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Hey there @@wbv-profile! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
Hey there @wbv-profile! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
|
Closing as overlapping with #36294, already merged into This fixes a real shared- The issue remains visible on |
Description of Change
FormattedStringsubscribes toPropertyChangedandPropertyChangingon everySpanadded to its collection, with plain CLR handlers. ASpanthat the app holds on to — or shares between twoFormattedStrings — therefore roots theFormattedString, and through it the label or view that owns it.The per-span subscriptions now go through the existing
WeakNotifyPropertyChangedProxyandWeakNotifyPropertyChangingProxyinsrc/Controls/src/Core/Internals/WeakEventProxy.cs, and the type gains the finalizer those proxies require.Two things are deliberately left alone:
_spanscollection subscription in the constructor.SpanCollectionis private and owned by thisFormattedString, so that handler cannot outlive it; a cycle between the two is collectable. It now carries a comment saying so, since the surrounding code is being changed for exactly the opposite reason.Resethandling.SpanCollection.ClearItemsis already overridden to raise aRemovecarrying the removed items rather than a bareReset, so unlikeGradientBrush(Use weak events for GradientBrush gradient stop subscriptions #38397) there is no gap onClear(). A test pins that behaviour.A span that appears more than once in the collection is subscribed once and detached only when its last occurrence is gone, matching the approach taken in the earlier PRs in this series.
This is the same defect class as #38397, #38403 and #38405.
Tests
src/Controls/tests/Core.UnitTests/FormattedStringWeakEventTests.csadds five tests: two leak tests (a retained span, and a span that was removed from the collection), plus three behavioural guards — a span still raisingPropertyChanged(nameof(Spans)), a removed span no longer doing so, andClear()detaching its spans.Reverting the
src/Controls/src/Corechanges and re-running fails exactly one of them,RetainedSpanDoesNotRootFormattedString. The other four pass either way, and that is deliberate: the existingRemovepath already unsubscribed correctly andClearItemsalready raisesRemoverather thanReset, so those four pin behaviour this change must not break rather than proving a fix. Only the retained-span-still-in-the-collection case was actually leaking.With the fix all five pass and the full
Controls.Core.UnitTestssuite is green.Issues Fixed
Fixes #36289
Fixes #38180