Visitar URL original
fix: Stop shared dependency objects retaining discarded elements and their DataContext by mikernet · Pull Request #25102 · unoplatform/uno · GitHub
Skip to content

fix: Stop shared dependency objects retaining discarded elements and their DataContext - #25102

Open
mikernet wants to merge 5 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/shared-dependency-objects-25099
Open

mikernet wants to merge 5 commits into
unoplatform:masterfrom
mikernet:dev/mikernet/shared-dependency-objects-25099

Conversation

@mikernet

@mikernet mikernet commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

GitHub Issue: closes #25099

PR Type:

🐞 Bugfix

What changed? 🚀

On Skia, dependency objects that are shared between elements kept the last element they were applied to alive, and through that element's inherited DataContext, whole pages after navigation. The issue lists the reference chains found with a reflection walk from static roots in a real app. Two of the problems it found have since been fixed on master: the strong single-parent association of a shareable value (#26123, which also clears a released child's DataContext) and ToolTipService.m_CurrentToolTip (#25137). This PR, rebased on current master, fixes the two that remain, plus one stale-DataContext case that master's weak association exposes.

1. Transform.Changed handlers accumulated (unbounded)

A Style setter hands the same ScaleTransform to every element the style applies to. NativeRenderTransformAdapter subscribed to the transform's Changed event with a strong handler and only unsubscribed when the element's RenderTransform was replaced, never when the element was discarded. A shared transform therefore held one handler per element ever styled with it (147 after a few minutes of navigating in the reporting app), each rooting a FontIcon and its page.

Transform now exposes an internal RegisterChanged built on WeakEventHelper, the same pattern Brush.RegisterInvalidateRender uses, and the adapter registers through it and disposes the registration. The registration disposable captures the delegate, so the handler lives exactly as long as the adapter holds it, and the transform never roots its subscribers. Popup's own RenderTransform handler and UIElement's Projection.Changed handler had the same shape and were migrated the same way (Projection gets the same RegisterChanged). So were Brush (Transform, RelativeTransform), Geometry (Transform) and TransformGroup (its children): a transform is multi-parent shareable, as in WinUI, so a single one can be used by many short-lived brushes, geometries or groups (a resource referenced from a template, for instance), and each of them was kept alive by it; a geometry also keeps the Path it draws, or the element it clips, alive through GeometryChanged. With no strong subscriber left, the strong Changed events of Transform and Projection are removed, so every subscriber goes through the weak registration. TransformGroup also handles the Reset that Children.Clear() raises without the removed items, so it lets go of cleared children. There is no WinUI counterpart for the adapter: WinUI dirties the composition tree through non-owning parent associations, so this stays an Uno-native design.

2. Shared values held their owner's DataContext strongly (bounded, one page per shared object)

A non-FrameworkElement dependency object set as a property value caches the DataContext pushed down from the element it is set on (_inheritedDataContext in the non-FE binding store), so {Binding}s added or resumed on it later resolve against it. This is how Uno models WinUI's inheritance context, which Given_NonFE_DataContextBinding and the MultiParentNonFE_* tests guard. The cache was a strong reference, so a style setter value, a static brush, a shared TransitionCollection or a static InputScope retained the DataContext of the last element it was applied to, and with it that element's page. In WinUI the inheritance context is a non-owning link to the mentor, and the DataContext belongs to the mentor.

The cache is now a ManagedWeakReference rented from WeakReferencePool. No inheritance rule changes: the value inherits exactly as before, it just no longer owns what it inherited. A value a binding has already copied into an object-typed property is released when the owner's subtree leaves the tree (the owner loses its inherited DataContext and pushes null into its values).

3. A collected parent's DataContext survived re-association

Since #26123, a shareable value whose single parent was collected without unassociating counts that parent as no parent, and the next owner becomes its single parent, as in WinUI, where a destroyed parent removes itself from its values (CDependencyObject::ReleaseImpl -> ResetReferencesFromChildren -> RemoveParent). The expired slot was reused without clearing the DataContext the dead parent had pushed in, and the on-attach push in UpdateChildBindable only forwards a non-null DataContext, so on an owner with no DataContext the value's bindings kept resolving against the old view model whenever it was still alive (the weak reference does not help then). AssociateParent now clears the inherited DataContext when it reuses an expired slot, as the parent's removal does in WinUI, unless the value has a parent of its own other than the new owner (the same guard as the on-attach push). The dead parent's slot also no longer counts as a holder: it never released its bindable slot, so master's guarded clear on release (which waits for the last holder) never fired again for that value, and the next owner's DataContext stayed behind once it let go. The ALC sweep frees an already-collected parent's slot the same way.

Tests

Runtime tests, each failing on master and passing with its commit:

  • Given_Transform.When_RenderTransform_Shared_By_Style_Then_Discarded_Elements_Collected: five elements styled with a shared transform are discarded and collected, then a new element styled the same way still renders at the new scale after a full collection when the shared transform changes.
  • Given_Transform.When_RenderTransform_Shared_By_Popup_Style_Then_Discarded_Popups_Collected and When_Projection_Shared_By_Style_Then_Discarded_Elements_Collected: the same for a Popup style sharing a transform and for a style-shared PlaneProjection.
  • Given_Transform.When_Transform_Shared_By_Brushes_Then_Discarded_Brushes_Collected, When_Transform_Shared_By_Geometries_Then_Discarded_Elements_Collected and When_Transform_Shared_By_TransformGroups_Then_Discarded_Groups_Collected: one transform used by several gradient brushes (RelativeTransform), clip geometries or TransformGroups; the discarded brushes, clipped elements and groups are collected, and a survivor still renders the transform's next change after a full collection.
  • Given_BindingMemoryLeak.When_BrushShared_SingleOwner_Removed_Then_Owner_And_ViewModel_Collected, When_TransitionsShared_Removed_Then_ViewModel_Collected and When_InputScopeShared_Removed_Then_ViewModel_Collected: a long-lived brush applied to one element at a time, a TransitionCollection shared through a Style setter and a plain shared InputScope do not retain the owner's DataContext.
  • Given_UIElement.SingleParentNonFE_Parent_Collected_Then_Next_Parent_Starts_Clean: a bound brush whose first owner is collected while that owner's view model stays alive starts clean on a new owner with no DataContext, then inherits the new owner's DataContext, and drops it again once the new owner replaces it.

Regression guards, passing before and after: Given_Transform.When_TransformGroup_Children_Cleared_Then_Removed_Child_Collected (a transform cleared from a long-lived group is collected, and the group follows a child added afterwards) covers the TransformGroup registrations, and Given_UIElement.SharedNonFE_ObjectBinding_Subtree_Severed_Then_Bound_Value_Released (a bound object-typed value on a shared non-FE object is released when the owner's subtree is severed) covers the weak cache.

Validation on Skia desktop, on the rebased head: Given_Transform, Given_BindingMemoryLeak, Given_UIElement, Given_CollectibleAlcAssociations, Given_NonFE_DataContextBinding, Given_ResourceDictionary_DataContext, Given_SharedTemplate_DataContext, Given_ToolTip and Given_FrameworkElement_And_Leak pass: 727 passed, 3 failed, 1 skipped, where the 3 failures are Given_UIElement ActualOffset tests that fail the same way on master on this machine (125% DPI rounding). With the product changes reverted to master, every test in the first list fails and the two guards pass; with only the weak DataContext, or without the holder-count fix (its last step), SingleParentNonFE_Parent_Collected_Then_Next_Parent_Starts_Clean still fails and the Given_BindingMemoryLeak tests pass. The Given_Transform and Given_BindingMemoryLeak tests are unchanged from the previous round, where they passed on native WinUI with the WinAppSDK head built locally (the projection test and the Given_UIElement tests are Skia-only).

PR Checklist ✅

Copilot AI balanced review requested due to automatic review settings October 8, 2026 05:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The transform test does not verify adapter updates, test content lacks guaranteed cleanup, and ALC comments describe obsolete strong-reference behavior.

3 open findings
What changed in this PR

Fixes memory retention caused by shared dependency objects, transform handlers, and closed tooltips.

Changes:

  • Uses weak references for inherited DataContexts and associated parents.
  • Adds weak transform-change subscriptions and clears closed tooltips.
  • Adds runtime memory-leak regression tests.
File Description
ResourceDictionary.alc.cs Updates association documentation.
Transform.cs Adds weak change registration.
DependencyPropertyDetailsCollection.cs Releases cached inherited context.
DependencyPropertyDetailsCollection.Bindings.cs Stores inherited DataContext weakly.
DependencyObject.Binder.cs Stores associated parents weakly.
DependencyObject.alc.cs Adapts ALC cleanup to weak parents.
ToolTipService.cs Releases closed automatic tooltips.
Application.Alc.cs Updates ALC cleanup reference.
NativeRenderTransformAdapter.cs Uses weak transform subscriptions.
Given_Transform.cs Tests shared-transform collection.
Given_BindingMemoryLeak.cs Tests shared dependency-object collection.
Given_ToolTip.cs Tests tooltip owner collection.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/Uno.UI/UI/Xaml/Application.Alc.cs Resolved
Copilot AI balanced review requested due to automatic review settings October 8, 2026 05:52
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from bd244a7 to 8f90e27 Compare October 8, 2026 05:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A replacement parent can inherit stale DataContext state, and the ALC cleanup tests no longer meaningfully exercise cleanup.

2 open findings
3 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Copilot AI balanced review requested due to automatic review settings October 8, 2026 06:12
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 8f90e27 to 92cb63c Compare October 8, 2026 06:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Bound target values can still retain or display a discarded owner’s DataContext after collection or explicit unassociation.

2 open findings
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Copilot AI balanced review requested due to automatic review settings October 8, 2026 06:29
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 92cb63c to 39e0d36 Compare October 8, 2026 06:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

One runtime test leaves shared window state uncleared on failure.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Copilot AI balanced review requested due to automatic review settings October 8, 2026 06:39
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 39e0d36 to 180430d Compare October 8, 2026 06:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Strong popup transform subscriptions remain, and dead inherited-context weak references are not reliably released.

2 open findings
1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread src/Uno.UI/UI/Xaml/Media/Transform.cs Resolved
Copilot AI balanced review requested due to automatic review settings October 8, 2026 07:51
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 180430d to 6acb2ad Compare October 8, 2026 07:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes foundational binding, weak-reference, rendering, and ALC lifetime semantics that warrant final human validation.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

@MartinZikmund
MartinZikmund requested a review from Xiaoy312 October 8, 2026 08:38
@MartinZikmund

Copy link
Copy Markdown
Member

@Xiaoy312 can you take a look please?

@MartinZikmund

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Copilot AI balanced review requested due to automatic review settings October 8, 2026 08:42
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 6acb2ad to 2601aba Compare October 8, 2026 08:42
mikernet added a commit to Singulink/uno that referenced this pull request Oct 16, 2026
…ort)

Port of the review round on unoplatform#25102 (704e17f,
7031c45 and 77899d4).

Transforms are multi-parent shareable, so a transform shared by many
short-lived brushes, geometries or transform groups rooted each of them
through the strong Transform.Changed event (and a geometry also roots the
Path it draws or the element it clips). Brush, Geometry and
TransformGroup now hold a weak RegisterChanged registration instead, and
the strong Changed events on Transform and Projection, which had no
subscribers left, are removed. TransformGroup also drops its child
registrations on a Reset, so Children.Clear() no longer keeps the
removed children alive.
mikernet added a commit to mikernet/uno that referenced this pull request Oct 16, 2026
…ver it closed

ToolTipService kept the automatic ToolTip it last scheduled or opened as
its current one until another one opened. A tooltip closed by a path the
service does not drive (its popup closed with the other popups, or the
app setting IsOpen) stayed referenced, and so did one whose opening was
cancelled during the show delay, and with either the owner it is set on.

WinUI clears ToolTipServiceMetadata::m_tpCurrentToolTip in the ToolTip's
own IsOpen change rather than in the service's close paths, and drops the
pending owner when an automatic tooltip is cancelled before it opens. The
ToolTip now tells the service when it has closed, and the service drops
it if it is still the current one. The service also drops the current
tooltip when it closes or cancels it itself; those two hunks are unoplatform#25102's
verbatim, so the two changes merge cleanly in either order.

MUX Reference: ToolTip_Partial.cpp, ToolTip::OnIsOpenChanged;
ToolTipService_Partial.cpp, ToolTipService::CancelAutomaticToolTip.

Runtime tests: a hovered tooltip whose popup is closed while the pointer
stays on the owner is no longer the service's current tooltip and its
owner is collected once removed; a tooltip whose opening is cancelled is
dropped by the service and its owner is collected once removed.
mikernet added a commit to mikernet/uno that referenced this pull request Oct 16, 2026
…ver it closed

ToolTipService kept the automatic ToolTip it last scheduled or opened as
its current one until another one opened. A tooltip closed by a path the
service does not drive (its popup closed with the other popups, or the
app setting IsOpen) stayed referenced, and so did one whose opening was
cancelled during the show delay, and with either the owner it is set on.

WinUI clears ToolTipServiceMetadata::m_tpCurrentToolTip in the ToolTip's
own IsOpen change rather than in the service's close paths, and drops the
pending owner when an automatic tooltip is cancelled before it opens. The
ToolTip now tells the service when it has closed, and the service drops
it if it is still the current one. The service also drops the current
tooltip when it closes or cancels it itself; those two hunks are unoplatform#25102's
verbatim, so the two changes merge cleanly in either order.

MUX Reference: ToolTip_Partial.cpp, ToolTip::OnIsOpenChanged;
ToolTipService_Partial.cpp, ToolTipService::CancelAutomaticToolTip.

Runtime tests: a hovered tooltip whose popup is closed while the pointer
stays on the owner is no longer the service's current tooltip and its
owner is collected once removed; a tooltip whose opening is cancelled is
dropped by the service and its owner is collected once removed.
@agneszitte

Copy link
Copy Markdown
Member

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@agneszitte

Copy link
Copy Markdown
Member

@MartinZikmund can you help follow up on this PR please

@unodevops

Copy link
Copy Markdown
Contributor

🤖 Your WebAssembly Sample App stage site is ready! Visit it here:

@nventive-devops

Copy link
Copy Markdown
Contributor

The build 242666 found UI Test snapshots differences: skia-linux-screenshots: 2458, skia-windows-screenshots: 2458

Details
  • skia-linux-screenshots: 2458 changed over 2492

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • Accessibility_ScreenReader.png-dark
    • AppBarButtonTest.png
    • AppBar_KeyBoard.png-dark
    • ApplicationViewSpanningRectsPage.png-dark
    • AppWindowFeatures.png-dark
    • AppWindowPositionAndSize.png
    • ArcSegment.png-dark
    • Attributed_text_FontSize_Changing.png-dark
    • Attributed_text_Supserscript.png
    • AuthenticationBroker_Cancel.png-dark
    • AutomationPropertiesExtensions_Role.png-dark
    • AutomationPropertiesExtensions_Role.png
    • BackGesture.png-dark
    • BackGesture.png
    • Basics.png-dark
    • Basics.png
    • BindableDrawerLayout_ChangePane.png-dark
    • BindableDrawerLayout_ChangePane.png
    • BitmapImage_vs_SvgImageSource.png-dark
    • BitmapImage_vs_SvgImageSource.png
  • skia-windows-screenshots: 2458 changed over 2492

    🚨🚨 Comparison Details (first 20) 🚨🚨
    • AndroidWindowInsets.png-dark
    • AnimatedBackVisualSourceSamplePage.png-dark
    • AnimatedVisualsTestbedPage.png-dark
    • AppBarButtonWithIconTest.png
    • AppBar_KeyBoard.png-dark
    • ApplicationViewSizing.png
    • AppWindowFeatures.png
    • AppXamlDefinedResources.png-dark
    • AppXamlDefinedResources.png
    • ArcSegment.png-dark
    • AuthenticationBroker_Cancel.png-dark
    • AuthenticationBroker_Cancel.png
    • AutomationProperties_AutomationId.png
    • AutomationProperties_Landmark.png-dark
    • AutoSuggestBoxChosenSuggestion.png-dark
    • AutoSuggestBoxChosenSuggestion.png
    • AutoSuggestBox_Keyboard.png
    • AutoSuggestBox_Reason.png-dark
    • BasicAutoSuggestBox.png-dark
    • BasicAutoSuggestBox.png

MartinZikmund pushed a commit that referenced this pull request Nov 2, 2026
An ImageSource kept its subscribers in a strong list, and the Skia
Image subscribed to the SourceLoaded and OpenFailed events of an
SvgImageSource. ImageBrush did the same with its subscription and with
a handler on ImageSource.Invalidated. All of these were only released
when the consumer's source changed, so a source that outlives its
consumers, such as an icon in a static field or in the application
resources, kept every Image and ImageBrush that ever used it alive,
along with everything they reference.

Hold the subscribers weakly instead: the disposable returned by
Subscribe keeps the callback alive for as long as the consumer holds
it, and an open in progress keeps the current subscribers alive until
it completes, as a SetSourceAsync awaiting it may be referenced by
nothing else. The SVG events and Invalidated are forwarded through
handlers that hold the consumer weakly and unsubscribe once it is gone.
When sources open and when ImageOpened and ImageFailed are raised is
unchanged.

This is a narrow servicing fix: on master, Image subscribing only while
loaded (#25124) fixes this as part of a much larger change. A source
used by a single element still references it as its inheritance
context parent until another element uses it, which is addressed
separately (#25102) and not covered here.

Fixes #25596

@MartinZikmund MartinZikmund left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed by reading the code against this PR's head, current master and the WinUI C++ sources. I haven't built or run it.

The transform and projection weak registrations look correct, and the weak _inheritedDataContext is still needed because master still holds it strongly. The WinUI references I checked hold up (ToolTip_Partial.cpp:394, CMultiParentShareableDependencyObject::AddParent/RemoveParent).

The main issue is that #26123 has merged and overlaps this PR, so it needs more than a mechanical rebase:

  1. Opposite rule for a collected parent. AssociateParent here counts a collected parent as a second parent, which permanently disables inheritance. Master now counts it as no parent, which is closer to WinUI, where a destroyed parent removes itself (depends.cpp:392, ResetReferencesFromChildren -> RemoveParent). The rule, the description and SingleParentNonFE_Parent_Collected_Then_Next_Parent_Counts_As_Second need to change.
  2. Stale DataContext once master's rule applies. Master reuses the expired slot without clearing the inherited DataContext, and the on-attach path in UpdateChildBindable only pushes non-null values. A value re-used on an owner with a null DataContext keeps resolving against the old one if that view model is still alive. 92cb63c fixed this shape and was later dropped, so its clear and test should come back. A weak reference alone doesn't cover this case.
  3. Overlapping clears. The unconditional ClearInheritedDataContext in UnassociateParent overlaps master's guarded clear in UpdateChildBindable (no parent of its own, last holder, same DataContext). It also clears a child that has its own parent, which master deliberately skips. Please keep one of them, with master's guards.
  4. Already on master. The ToolTipService hunks match what is there now (#25137), and _associatedParentRef already exists, using the owner's SelfWeakReference. Those can drop out, leaving the transform/projection registrations and the weak inherited DataContext as this PR's remaining value.

Analyze (csharp) is also failing on this head. I haven't looked into why.

@mergify

mergify Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue). If it does not match the queue conditions yet, it enters the queue once it does.

  • Queue this pull request

@Xiaoy312

Copy link
Copy Markdown
Contributor

Hi @mikernet, do you intend to keep finishing this PR, or would you like the uno-team to take over here?

@mikernet

Copy link
Copy Markdown
Contributor Author

@Xiaoy312 I was intending to get through all the PR feedback in the next day or two, unless you want to finish it sooner - up to you :)

NativeRenderTransformAdapter subscribed strongly to Transform.Changed and
only unsubscribed when the element's RenderTransform was replaced, so a
transform shared through a Style setter accumulated one handler per element
ever styled with it, keeping every discarded element (and its inherited
DataContext) alive. The adapter now registers through a weak registration
on the transform, so the transform never roots its subscribers.

unoplatform#25099
A non-FrameworkElement DependencyObject used as a property value caches
the DataContext pushed down from the element it is set on, so bindings
added or resumed later resolve against it. The cache was a strong
reference, so a long-lived shared value (a style setter value, a static
brush, TransitionCollection or InputScope) kept the DataContext of the
last element it was applied to, and with it that element's page, alive
after navigation.

Hold it weakly, as WinUI does: there the inheritance context is a
non-owning link to the mentor, and the DataContext belongs to the mentor.
The value still inherits exactly as before; it only no longer owns what
it inherited.

Fixes unoplatform#25099
VisualTreeHelper.CloseAllPopups is Uno-only, so the WinAppSDK build of
the runtime tests failed with CS0117. UITestHelper.CloseAllPopups falls
back to the open popups of the XamlRoot on WinUI.

The test also waits for its root to load before giving its XamlRoot to
the popups: on WinUI the root was sometimes not in the tree yet, and
opening a popup with a null XamlRoot throws.
Transforms are multi-parent shareable, as in WinUI, so one transform can
outlive the brushes, geometries and TransformGroups that use it (a
resource referenced from a template, for instance). Each of them still
subscribed to the strong Transform.Changed event, so the shared
transform kept every discarded one alive, and through a geometry's
GeometryChanged handler also the Path it draws or the element it clips.
WinUI tracks those parents with non-owning pointers.

They now go through RegisterChanged and hold the registration, like the
render transform adapter, Popup and the projection owner. With no
strong subscriber left, the strong Changed events of Transform and
Projection are removed so the leak cannot come back through them.
TransformGroup now also handles the Reset that Children.Clear() raises
without the removed items, so it lets go of the cleared children.
A shareable value whose single parent was collected without
unassociating takes the next owner as its single parent again, but kept
the DataContext the dead parent had pushed into it: the on-attach push
only forwards a non-null DataContext. Bound properties on the value kept
resolving against the old view model whenever that view model was still
alive, and the weak inherited DataContext does not help then.

WinUI has no such state: a destroyed parent removes itself from its
values (CDependencyObject::ReleaseImpl -> ResetReferencesFromChildren),
which takes the inheritance context with it. Clear the inherited
DataContext when the expired slot is reused, unless the value has a
parent of its own other than the new owner, like the on-attach push.

The dead parent never released its bindable slot either, so it kept
counting as a holder and the guarded clear on release, which waits for
the last holder, never fired again for that value: the next owner's
DataContext stayed behind once it let go. Give that slot back when the
expired association is freed, here and in the ALC sweep.
@mikernet
mikernet force-pushed the dev/mikernet/shared-dependency-objects-25099 branch from 77899d4 to 0efaf28 Compare November 12, 2026 23:03
@mikernet

Copy link
Copy Markdown
Contributor Author

I rebased onto current master and reworked the dependency-object part around #26123. The PR is now the transform/projection registrations, the weak inherited DataContext and the expired-slot clear from point 2.

  1. Collected parent. Agreed, and the rebase takes master's rule as is: a collected parent counts as no parent. SingleParentNonFE_Parent_Collected_Then_Next_Parent_Counts_As_Second is gone and the description no longer claims the old rule.

  2. Stale DataContext on an expired slot. Confirmed: on master, with the first owner's view model still alive, a bound brush moved onto an owner with no DataContext keeps the old value, since the on-attach push skips null. 0efaf28 brings back the clear from 92cb63c: AssociateParent clears the inherited DataContext when it reuses an expired slot (_associatedParentRef set but AssociatedParent null), which is what the parent's removal does in WinUI (ReleaseImpl -> ResetReferencesFromChildren -> RemoveParent). It also gives back the dead parent's holder count: that parent never released its bindable slot, so _bindableHolderCount stayed one too high and master's guarded clear on release never fired again for that value. The next owner's DataContext then stayed behind once that owner let go. The ALC sweep's collected-parent branch now frees the slot the same way. Its test, SingleParentNonFE_Parent_Collected_Then_Next_Parent_Starts_Clean, is back too. It fails on master, with only the weak DataContext, and (at its last step) without the holder-count fix; it passes with the full fix.

  3. Overlapping clears. I dropped the clear in UnassociateParent and kept master's guarded clear in UpdateChildBindable. That is UnassociateParent's only caller, and the guarded clear runs right after it. The new expired-slot clear in point 2 uses the same own-parent guard as master's on-attach push: it skips a value whose own parent is not the new owner. I dropped the test that went with my version (..._Removes_Value_Then_Next_Parent_Starts_Clean) because it now only covers master's path, which Given_SharedTemplate_DataContext already tests.

  4. Already on master. Dropped: the ToolTipService commit (and its test), _associatedParentRef/SetAssociatedParent, my ALC sweep changes in DependencyObject.alc.cs, the comment updates in Application.Alc.cs and ResourceDictionary.alc.cs, and the Given_CollectibleAlcAssociations rewrite. What is left is DependencyPropertyDetailsCollection.Bindings.cs (weak _inheritedDataContext, be0c5c1), the expired-slot handling in AssociateParent plus a one-line change in the ALC sweep's collected-parent branch (0efaf28), and the transform/projection/brush/geometry/group registrations (86e5b66, 880cfd2, c331145), which are unchanged apart from the rebase.

Analyze (csharp): the failing run was against the October merge ref. It stopped at restore with NU1902 on MessagePack 3.1.7 in Uno.UI.RemoteControl.Host (warning as error). Master is on 3.1.12 now, and the rebase picks that up.

Tests on Skia desktop on the rebased head: Given_Transform, Given_BindingMemoryLeak, Given_UIElement, Given_CollectibleAlcAssociations, Given_NonFE_DataContextBinding, Given_ResourceDictionary_DataContext, Given_SharedTemplate_DataContext, Given_ToolTip and Given_FrameworkElement_And_Leak pass: 727 passed, 3 failed, 1 skipped, where the 3 failures are ActualOffset tests that fail the same way on master here (125% DPI). With the product code reverted to master, the transform tests (except the TransformGroup clear guard), the three new Given_BindingMemoryLeak tests and ..._Starts_Clean fail.

mikernet added a commit to Singulink/uno that referenced this pull request Nov 12, 2026
Port of unoplatform#25102 commit 0efaf28, on top of this build's
port of that PR (800247f and be41da6).

A shareable value whose single parent was collected without
unassociating counted that dead parent as a first parent, so the next
owner turned its inheritance off for good. WinUI has no such state: a
destroyed parent removes itself from its values
(CDependencyObject::ReleaseImpl -> ResetReferencesFromChildren), taking
the inheritance context with it. A collected parent now counts as no
parent: its expired slot is reused for the next owner and the
DataContext it pushed is cleared, unless the value has a parent of its
own other than the new owner.

This build has no bindable holder count (its release clears
unconditionally), so only the AssociateParent part applies; the ALC
sweep already frees an expired slot.
mikernet added a commit to Singulink/uno that referenced this pull request Nov 14, 2026
Takes Uno's servicing/6.8 from 6951c2c up to 5254ace (42 pull
requests). Conflicts, by area:

IME and TextBox (unoplatform#25984, backport of unoplatform#25804; unoplatform#25952, backport of unoplatform#25619;
unoplatform#25976, backport of unoplatform#25818)
- Takes upstream's composition design (unoplatform#25804): the browser's input is
  the source of truth for the text, compositions included. This drops
  the composition parts of the unoplatform#24502 port (90746b8): its
  TypeScript composition handling, the StartIndex/TextChangePending
  event arguments, the IsReadOnly forwarding to the hidden input, and
  its Given_TextBox.BrowserSoftKeyboard tests.
- Keeps the non-IME browser fixes on top of it, in the form their
  master pull requests have on unoplatform#25804: Enter raised once per press and
  tracked by key code (unoplatform#25174, with Given_TextBox.BrowserEnterKey from
  master in place of the Enter tests that lived in the dropped file),
  the iOS refocus on tap (unoplatform#25185), pastes without text and isIOS from
  BrowserInputHelper.
- Moves the unoplatform#26005 MaxLength fixes onto unoplatform#25804's native input path, as
  master has them: a refused browser edit keeps the TextBox's selection,
  TextBoxView.UpdateTextFromNative reports whether the edit went in, and
  the IME keeps its range on the applied text (master be85699, which
  d226897 had ported onto the unoplatform#24502 IME). Adds that commit's browser
  and platform-applied IME tests.
- TextBox.pointers.skia.cs: upstream's file (unoplatform#25619 iOS touch editing,
  unoplatform#25818, unoplatform#25804), with the unoplatform#25126 port (the hold acts when it starts,
  a touch right-tap keeps the caret) in the form master's unoplatform#25126 branch
  has it on unoplatform#25619, including focusing before the iOS hold timer acts
  and its iOS not-focusable test.

Pointers (unoplatform#25976, backport of unoplatform#25818)
- Tapped and RightTapped are raised once the PointerReleased bubbled
  (unoplatform#25818), with the unoplatform#25732 port's window coordinates (2-argument
  routed event args, RawPosition for ContextRequested), as master's
  unoplatform#25732 branch has them.
- The release wrapper of unoplatform#25818 keeps the unoplatform#25126 port's
  StopContextMenuTimer at the start of the release.

DependencyObject (unoplatform#26142, backport of unoplatform#26123)
- Takes upstream, then the code of unoplatform#25839 (97a2f17), which replaces
  this branch's unoplatform#25102 ports (800247f, be41da6, fd40bf9,
  7546379). DependencyObjectStore.Binder.cs, .alc.cs,
  Application.Alc.cs, ResourceDictionary.alc.cs and their tests now
  match unoplatform#25839; this drops 800247f's own
  SingleParentNonFE_Parent_Removes_Value_Then_Next_Parent_Starts_Clean
  test, which unoplatform#25839 doesn't have.

Imaging (unoplatform#25597, unoplatform#25915, unoplatform#25911, unoplatform#25897)
- Keeps this branch's side: it has the later forms of these fixes
  (unoplatform#25124, unoplatform#25559, unoplatform#25633 and unoplatform#26370 ports), where upstream has the
  6.8-only unoplatform#25597 design and earlier snapshots. Upstream's shared
  download, IgnoreImageCache and failed-load fixes are covered by
  SharedImageDecode. Adds upstream's When_Source_Outlives_Brushes test.

ToolTip (unoplatform#26098, backport of unoplatform#25137 and unoplatform#26096)
- Keeps this branch's unoplatform#25137 port and adds unoplatform#26096 in master's form: the
  service releases only a ToolTip it opened, and leaves one the app
  opened during the show delay to the app. Keeps both sides' tests.

Flyout (unoplatform#26062, backport of unoplatform#26060)
- This branch had the same commits already; keeps the "Keep
  non-resizable flyouts in window" port and its tests.

Also removes a helper pair in Given_AccessibleTextBox that both sides
added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Skia] Shared dependency objects (style setter values, static brushes, input scopes, current tooltip) retain elements and DataContext of pages that are gone

8 participants