Skip to content

[Regression] Fix TypedBinding nested property re-subscription (#34428)#34449

Merged
simonrozsival merged 1 commit intomainfrom
backport/fix-34428-to-main
Mar 13, 2026
Merged

[Regression] Fix TypedBinding nested property re-subscription (#34428)#34449
simonrozsival merged 1 commit intomainfrom
backport/fix-34428-to-main

Conversation

@StephaneDelcroix
Copy link
Copy Markdown
Contributor

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Description

Cherry-pick of 5d6e5a20 from net11.0, adapted for main.

Fixes #34428

Problem

Release 10.0.50 introduced a performance optimization (_isSubscribed flag) in TypedBinding that prevented re-subscribing to intermediate INPC objects when they changed. This caused compiled bindings with nested property paths (e.g. {Binding ViewModel.Text}) to stop updating when the intermediate object was replaced.

Fix

Remove the _isSubscribed guard and always call Subscribe() on every Apply. The Subscribe() implementation is already idempotent — it diffs old vs new subscription targets — so calling it repeatedly is safe with minimal overhead.

Tests

Two regression tests added to TypedBindingUnitTests.cs:

  • TypedBinding_NestedProperty_ResubscribesAfterNullIntermediateBecomesNonNull
  • TypedBinding_NestedProperty_ResubscribesAfterIntermediateReplaced

…ate becomes non-null

When an intermediate object in a TypedBinding path starts as null and later
becomes non-null (or is replaced with a different object), the binding must
re-establish INPC subscriptions to the new object's nested properties.

The previous optimization used a '_isSubscribed' boolean flag to skip
IPropertyChangeHandler.Subscribe() after the first Apply. This prevented
re-subscribing when intermediate objects changed. For example:

  vm.Child = null  // subscribe to vm, but NOT vm.Child.Name (null)
  vm.Child = new Child()  // Subscribe skipped due to _isSubscribed=true
  vm.Child.Name = "x"  // binding never fires — regression!

The fix removes '_isSubscribed' and always calls Subscribe on each Apply.
Both IPropertyChangeHandler implementations (PropertyChangeHandler and
LegacyPropertyChangeHandler) are already idempotent — they use reference
equality checks to avoid re-subscribing to unchanged objects — so calling
Subscribe on every Apply is safe and has minimal overhead.

Adds two regression tests:
- TypedBinding_NestedProperty_ResubscribesAfterNullIntermediateBecomesNonNull
- TypedBinding_NestedProperty_ResubscribesAfterIntermediateReplaced

Reported in: #32382 (comment)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@StephaneDelcroix StephaneDelcroix added the t/bug Something isn't working label Mar 12, 2026
Copilot AI review requested due to automatic review settings March 12, 2026 13:27
@StephaneDelcroix StephaneDelcroix added i/regression This issue described a confirmed regression on a currently supported version t/bug Something isn't working labels Mar 12, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34449

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34449"

@StephaneDelcroix
Copy link
Copy Markdown
Contributor Author

/backport to release/10.0.1xx-sr5

@github-actions
Copy link
Copy Markdown
Contributor

Started backporting to release/10.0.1xx-sr5 (link to workflow run)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a regression in TypedBinding where compiled bindings with nested property paths could stop updating after an intermediate object was replaced, by ensuring subscription updates are re-evaluated during binding application.

Changes:

  • Removed the _isSubscribed guard so TypedBinding can re-subscribe when intermediate INotifyPropertyChanged objects change.
  • Added two regression tests covering null-to-non-null and replaced-intermediate scenarios for nested property paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Controls/src/Core/TypedBinding.cs Ensures Subscribe() is invoked during applies so nested-path subscriptions are refreshed when intermediates change.
src/Controls/tests/Core.UnitTests/TypedBindingUnitTests.cs Adds regression coverage for nested property re-subscription when intermediates become non-null or are replaced.

// Subscribe on every Apply so that intermediate objects that changed are re-subscribed.
// Subscribe() is idempotent: it diffs old vs new subscription targets and only
// updates what changed, so calling this repeatedly is safe.
if (isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Subscribe() is now called on every ApplyCore, including when fromTarget == true for BindingMode.TwoWay (i.e., target-driven updates). That means each UI-originated change will re-run the handler chain and subscription diffing, which can be a measurable regression compared to the _isSubscribed optimization, even though subscriptions likely don’t need to change on target updates. Consider gating the Subscribe(...) call to only run when fromTarget is false (or when needsGetter is true), so intermediate re-subscription still happens for source/context changes without adding per-keystroke overhead for TwoWay bindings.

Suggested change
if (isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null)
if (isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null && (!fromTarget || needsGetter))

Copilot uses AI. Check for mistakes.
@StephaneDelcroix StephaneDelcroix added this to the .NET 10 SR5 milestone Mar 12, 2026
@PureWeen PureWeen modified the milestones: .NET 10 SR5, .NET 10 SR5.1 Mar 12, 2026
@StephaneDelcroix
Copy link
Copy Markdown
Contributor Author

/azp run maui-pr-uitests, maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@simonrozsival simonrozsival merged commit e10b0be into main Mar 13, 2026
167 checks passed
@simonrozsival simonrozsival deleted the backport/fix-34428-to-main branch March 13, 2026 09:26
@StephaneDelcroix
Copy link
Copy Markdown
Contributor Author

/backport to release/10.0.1xx-sr5

@github-actions
Copy link
Copy Markdown
Contributor

Started backporting to release/10.0.1xx-sr5 (link to workflow run)

PureWeen pushed a commit that referenced this pull request Mar 13, 2026
…re-subscription (#34428) (#34450)

Backport of #34449 to release/10.0.1xx-sr5

/cc @StephaneDelcroix

Co-authored-by: Stephane Delcroix <stephane@delcroix.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

i/regression This issue described a confirmed regression on a currently supported version t/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Regression] 10.0.50: Compiled Bindings (TypedBinding) does not resubscribe to intermediary values

4 participants