Revert PR #33584: restore BindableObject property-context lookup model - #35970
Conversation
Co-authored-by: rmarinho <1235097+rmarinho@users.noreply.github.com>
|
@copilot can you add tests that would have caught this regression please |
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Added regression coverage in commit New tests in
I also ran the targeted |
This comment has been minimized.
This comment has been minimized.
Addresses the optional test-coverage suggestion from the PR review: the
single-PR BLOCKED fixture never exercised the multi-PR comma-joined naming
("#a, #b") the release captain sees. Add a two-p/0-PR fixture asserting the
count and the joined #35970, #35971 format.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SR readiness report derived blocking only from ship-checks and Tier-1 regression issues; open PRs were informational. A p/0-labelled PR targeting an SR branch (e.g. #35970 against release/10.0.1xx-sr8) was therefore not flagged as blocking on the SR tracker issue, unlike the Preview lane which already treats p/0 PRs as blockers. Port Test-IsP0Pr into Get-ReleaseReadiness.ps1 and add a Get-P0PrChecks function that emits a 'P/0 release-branch PRs' ship-check (BLOCKED when any open p/0 PR targets the SR branch, READY otherwise). The check reuses the already-fetched open-PR list (no extra gh call) and is merged into shipChecks, so a BLOCKED result is auto-hoisted into the top-of-issue blocking summary and escalates the verdict to Not Ready via the existing ship-check machinery. Add deterministic synthetic-fixture unit tests for both functions. Discovered while working on deflake PR #36004. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses the optional test-coverage suggestion from the PR review: the
single-PR BLOCKED fixture never exercised the multi-PR comma-joined naming
("#a, #b") the release captain sees. Add a two-p/0-PR fixture asserting the
count and the joined #35970, #35971 format.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The SR readiness report derived blocking only from ship-checks and Tier-1 regression issues; open PRs were informational. A p/0-labelled PR targeting an SR branch (e.g. #35970 against release/10.0.1xx-sr8) was therefore not flagged as blocking on the SR tracker issue, unlike the Preview lane which already treats p/0 PRs as blockers. Port Test-IsP0Pr into Get-ReleaseReadiness.ps1 and add a Get-P0PrChecks function that emits a 'P/0 release-branch PRs' ship-check (BLOCKED when any open p/0 PR targets the SR branch, READY otherwise). The check reuses the already-fetched open-PR list (no extra gh call) and is merged into shipChecks, so a BLOCKED result is auto-hoisted into the top-of-issue blocking summary and escalates the verdict to Not Ready via the existing ship-check machinery. Add deterministic synthetic-fixture unit tests for both functions. Discovered while working on deflake PR #36004. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Addresses the optional test-coverage suggestion from the PR review: the
single-PR BLOCKED fixture never exercised the multi-PR comma-joined naming
("#a, #b") the release captain sees. Add a two-p/0-PR fixture asserting the
count and the joined #35970, #35971 format.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…R lane parity with Preview) (#36006) <!-- Please let the below note in for people that find this PR --> > [!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](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### The gap The release-readiness skill has two report generators. The **Preview lane** (`Get-PreviewReadiness.ps1`) already surfaces open PRs carrying the `p/0` label as release **blockers**. The **SR lane** (`Get-ReleaseReadiness.ps1`) did **not**: it derived blocking only from ship-checks (`BLOCKED`) and Tier-1 `regressed-in-*` **issue** classifications. Open PRs were listed purely informationally. Consequence: a `p/0`-labelled PR targeting an SR branch was never flagged as blocking on the SR tracker issue. Concretely, PR #35970 (`Revert PR #33584…`, base `release/10.0.1xx-sr8`, label `p/0`) did **not** show as blocking on the SR8 tracker issue #35876. ### What changed - **Ported `Test-IsP0Pr`** from the Preview lane into `Get-ReleaseReadiness.ps1`. It is StrictMode-safe and accepts both the production `gh --json` PSCustomObject shape and the IDictionary/hashtable shape used by test mocks. - **Added `Get-P0PrChecks`**, which emits a single `P/0 release-branch PRs` ship-check — `BLOCKED` (naming each offending PR, e.g. `#35970`) when any open `p/0` PR targets the SR branch, `READY` otherwise. - **Merged the check into `shipChecks`** in the main flow, reusing the already-fetched open-PR list so there is **no extra `gh` call**. Because it's a standard `BLOCKED` ship-check, it is automatically hoisted into the top-of-issue `🔴 Blocking` summary and escalates the verdict to **Not Ready** — no verdict or renderer changes were needed. - **Added deterministic synthetic-fixture unit tests** for both functions (no network, no wall-clock dependence). ### Notes - This is a PowerShell-only `.github/skills/**` change; the framework `maui-pr` pipeline intentionally skips for skills-only PRs. - Discovered while working on the separate deflake PR #36004. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen
left a comment
There was a problem hiding this comment.
@copilot Adversarial review — 3 independent reviewers (different model families) with cross-validated consensus.
Verdict: the revert is faithful, complete, and safe to merge. All three reviewers independently verified the change against the local source and the pre-#33584 baseline; no must-fix or should-fix correctness issues were found.
Verified (3/3 consensus):
- ✅ Faithful revert —
BindableProperty.csis byte-identical to the pre-#33584 version;BindableObject.csmatches pre-#33584 except it correctly preserves the later-addedIsSetExplicitlymethod._propertiesis back toDictionary<BindableProperty, BindablePropertyContext>, andGetContext/GetOrCreateContext/CreateAndAddContextall key byBindableProperty. - ✅ No dangling references — repo-wide search found zero remaining uses of
InternalId,_nextInternalId, orBindableObjectBenchmarker. The deleted benchmark is auto-discovered viaBenchmarkSwitcher.FromAssembly, so there's no registration list to update. - ✅
GetValues<T>equivalence — the new explicitelse { IsSet = false; Value = default(T); }is semantically identical to the optimized version's zero-initialized tuple array, and in-place mutation of the(bool, T)array elements is valid C#. - ✅ Tests compile & assert correctly — they reach the
internalAPIs viaInternalsVisibleTo;values[1].Value == 0for the unsetprop1(declared default1) is the correct contract, sinceGetValues<T>returnsdefault(T)for unset properties.
One non-blocking note (3/3): the two added tests don't actually catch the #33584 regression — they pass under both implementations. See the inline comment for detail and a concrete suggestion if a failing-without-fix test is wanted.
Test coverage: the changed GetValues<T> and GetLocalValueEnumerator paths are now covered by unit tests; the other reverted paths (GetContext/GetOrCreateContext) are exercised indirectly by the existing BindableObject suite.
Prior reviews: none on this PR.
Methodology: 3 independent reviewers with adversarial consensus. CI status intentionally out of scope.
| } | ||
|
|
||
| [Fact] | ||
| public void GetValuesReturnsSetStateAndValue() |
There was a problem hiding this comment.
GetValuesReturnsSetStateAndValueasserts behavior the optimizedGetValues<T>already satisfied (misses were zero-initialized to(false, default(T))).LocalValueEnumeratorReturnsLocallySetValuesexercisesGetLocalValueEnumerator(), an API that BindableObject property access micro-optimizations #33584 deleted — it wouldn't even compile against BindableObject property access micro-optimizations #33584, so it guards the API's existence rather than reproducing a runtime defect.
If the goal (per @PureWeen's request) is a test that fails without this revert, the most likely #33584 culprit was the ref from CollectionsMarshal.GetValueRefOrAddDefault being invalidated when the dictionary resizes during a re-entrant add — e.g. a DefaultValueCreator that sets several other properties mid-evaluation. A test that forces that re-entrancy would fail against #33584 and pass here.
(This root cause is a hypothesis; the PR frames the revert as precautionary. If no specific failure was reproduced, it's worth stating that explicitly in the thread rather than implying these are true regression tests.)
Flagged by: 3/3 reviewers
Co-authored-by: PureWeen <5375137+PureWeen@users.noreply.github.com>
Addressed in I added |
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@copilot — new AI review results are available based on this last commit:
3888c23.
Add reentrant default value creator regression test To request a fresh review after new comments or commits, comment/review rerun.
Review Sessions — click to expand
Gate — Test Before & After Fix
Gate Result: ❌ FAILED
Platform: ANDROID
⚠️ verify-tests-fail.ps1exited before writing a verification report. Diagnostics below.
Exit code: 1
Artifacts written before exit:
verification-log.txt(9.2 KB)
Gate output log (last 60 lines)
[2026-06-19 16:07:46] - [UnitTest] BindableObjectUnitTests (filter: BindableObjectUnitTests)
[2026-06-19 16:07:46] Platform: android
[2026-06-19 16:07:46] FixFiles: eng/Versions.props, src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs, src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs, src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs, src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs, src/Controls/src/Core/BindableObject.cs, src/Controls/src/Core/BindableProperty.cs, src/Controls/src/Core/Border/Border.cs, src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs, src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs, src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs, src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs, src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs, src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs, src/Controls/src/Core/ContentPage/ContentPage.cs, src/Controls/src/Core/ContentView/ContentView.cs, src/Controls/src/Core/FlyoutPage/FlyoutPage.cs, src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs, src/Controls/src/Core/Handlers/Items/Android/Adapters/GroupableItemsViewAdapter.cs, src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs, src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs, src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs, src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs, src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs, src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs, src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs, src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs, src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs, src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs, src/Controls/src/Core/Layout/Layout.cs, src/Controls/src/Core/NavigationPage/NavigationPage.cs, src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs, src/Controls/src/Core/Page/Page.cs, src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs, src/Controls/src/Core/Platform/Android/TabbedPageManager.cs, src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs, src/Controls/src/Core/RadioButton/RadioButton.cs, src/Controls/src/Core/ScrollView/ScrollView.cs, src/Controls/src/Core/Shell/BaseShellItem.cs, src/Controls/src/Core/Shell/Shell.cs, src/Controls/src/Core/VisualElement/VisualElement.cs, src/Controls/src/Core/VisualStateManager.cs, src/Controls/src/Core/Window/Window.Android.cs, src/Controls/src/Core/Window/Window.cs, src/Core/maps/src/Handlers/Map/MapHandler.iOS.cs, src/Core/maps/src/Platform/iOS/MauiMKMapView.cs, src/Core/src/Core/IMauiRecyclerView.cs, src/Core/src/Core/ISafeAreaView2.cs, src/Core/src/Graphics/MauiDrawable.Android.cs, src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs, src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs, src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.cs, src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs, src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs, src/Core/src/Handlers/SearchBar/SearchBarHandler.iOS.cs, src/Core/src/Hosting/LifecycleEvents/AppHostBuilderExtensions.Android.cs, src/Core/src/ImageSources/FontImageSourceService/FontImageSourceService.Windows.cs, src/Core/src/Platform/Android/ContentViewGroup.cs, src/Core/src/Platform/Android/IBackNavigationState.cs, src/Core/src/Platform/Android/LayoutViewGroup.cs, src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs, src/Core/src/Platform/Android/MauiAppCompatActivity.cs, src/Core/src/Platform/Android/MauiScrollView.cs, src/Core/src/Platform/Android/MauiWindowInsetListener.cs, src/Core/src/Platform/Android/SemanticExtensions.cs, src/SingleProject/Resizetizer/src/SkiaSharpSvgTools.cs
[2026-06-19 16:07:46] BaseBranch: main
[2026-06-19 16:07:46] MergeBase: 32d494c2a2d2e53c790d586bd829c92d518d8ed8
[2026-06-19 16:07:46]
[2026-06-19 16:07:46] Verifying fix files exist...
[2026-06-19 16:07:46] ✓ eng/Versions.props exists
[2026-06-19 16:07:46] ✓ src/Compatibility/Core/src/Android/CollectionView/TemplatedItemViewHolder.cs exists
[2026-06-19 16:07:46] ✓ src/Compatibility/Core/src/Windows/CollectionView/ItemContentControl.cs exists
[2026-06-19 16:07:46] ✓ src/Compatibility/Core/src/iOS/CollectionView/TemplatedCell.cs exists
[2026-06-19 16:07:46] ✓ src/Compatibility/Core/src/iOS/Renderers/UIContainerCell.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/BindableObject.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/BindableProperty.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Border/Border.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFlyoutRecyclerAdapter.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerCell.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/ContentPage/ContentPage.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/ContentView/ContentView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/FlyoutPage/FlyoutPage.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/Adapters/GroupableItemsViewAdapter.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/ItemContentView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/MauiRecyclerView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/Tizen/ItemTemplateAdaptor.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items/iOS/TemplatedCell.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Items2/iOS/TemplatedCell2.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Handlers/Shell/Windows/ShellFlyoutItemView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Layout/Layout.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/NavigationPage/NavigationPage.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/NavigationPage/NavigationPageToolbar.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Page/Page.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Platform/Android/TabbedPageManager.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Platform/Windows/CollectionView/ItemContentControl.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/RadioButton/RadioButton.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/ScrollView/ScrollView.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Shell/BaseShellItem.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Shell/Shell.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/VisualElement/VisualElement.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/VisualStateManager.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Window/Window.Android.cs exists
[2026-06-19 16:07:46] ✓ src/Controls/src/Core/Window/Window.cs exists
[2026-06-19 16:07:46] ✓ src/Core/maps/src/Handlers/Map/MapHandler.iOS.cs exists
[2026-06-19 16:07:46] ✓ src/Core/maps/src/Platform/iOS/MauiMKMapView.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Core/IMauiRecyclerView.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Core/ISafeAreaView2.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Graphics/MauiDrawable.Android.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Standard.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Tizen.cs exists
[2026-06-19 16:07:46] ✓ src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.cs exists
[2026-06-19 16:07:46] ERROR: Fix file not found: src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs
UI Tests
Full UI test matrix will run (no specific categories detected from PR changes).
Pre-Flight — Context & Validation
Issue: #unknown - unavailable (GitHub CLI unauthenticated)
PR: #35970 - unavailable (local squashed PR branch)
Platforms Affected: Android primary for requested testing; PR also changes iOS, Windows, Tizen, HybridWebView, Resizetizer, and tests.
Files Changed: 66 implementation files in gate fix list, many tests/snapshots in local diff.
Key Findings
- Gate failed before test execution because
src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.csis deleted in the PR but still appeared in the verification fix-file list. - Android CollectionView safe-area tests require new explicit safe-area/listener-refresh API surface.
- Android grouped CollectionView header/footer rebinds require a separate logical-child idempotency fix.
- GitHub PR metadata, comments, and CI could not be fetched because
ghis unauthenticated.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: medium
Errors: 0 | Warnings: 2 | Suggestions: 0
Key code review findings:
⚠️ Gate verification is blocked by deleted-file handling forHybridWebViewHelper.cs.⚠️ Safe-area listener changes alone are insufficient; grouped header/footer logical-child rebind must also be addressed.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #35970 | Broad multi-area fix including Android safe-area, CollectionView recycling/logical-child changes, predictive back, HybridWebView, and tests | ❌ Gate failed before verification | many | Original PR; gate blocked by deleted file in fix-file list |
Code Review — Deep Analysis
Code Review — PR #35970
Independent Assessment
What this changes: The local squashed PR touches a broad set of MAUI controls/platform code. For Android, the main functional areas are CollectionView safe-area/listener behavior, CollectionView logical-child/recycling behavior, predictive back registration, toolbar/navigation state, and HybridWebView consolidation.
Inferred motivation: The changes appear to address multiple Android regressions around safe-area/inset handling in recycler-hosted views, CollectionView item/header/footer rebinds, navigation/back behavior, and HybridWebView JavaScript invocation behavior.
Reconciliation with PR Narrative
Author claims: Unavailable — gh is unauthenticated and public web fetch returned a GitHub action-block page.
Agreement/disagreement: Local diff and gate artifacts were used instead. The gate artifact shows verification failed before tests due to a deleted HybridWebView helper file being included in the fix-file list.
Prior Review Reconciliation
No prior ❌ Error findings could be fetched because GitHub CLI authentication is unavailable.
Blast Radius Assessment
- Runs for all instances: yes for safe-area/platform-view and Android activity/window navigation paths; localized for CollectionView logical-child fix.
- Startup impact: possible for
MauiAppCompatActivityback callback registration andWindowlifecycle notification. - Static/shared state:
MauiWindowInsetListenermaintains static registered-view state; cleanup paths are important.
CI Status
- Required-check result: undetermined —
gh pr checks --requiredunavailable due missing GitHub authentication. - Classification: undetermined.
- Action taken: capped confidence and used local Android device tests for try-fix candidates.
Findings
⚠️ Warning — Gate verification is blocked by deleted file handling
gate/content.md reports ERROR: Fix file not found: src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs. This means the pre-run gate failure is an infrastructure/deleted-file handling blocker, not evidence that the PR fix behavior failed at runtime.
⚠️ Warning — Android CollectionView safe-area fix alone is insufficient
Try-fix-2 proved the Android safe-area subset passes the new safe-area tests but fails grouped header/footer logical-child tests unless a rebind idempotency fix is also included.
Failure-Mode Probing
- RecyclerView item without explicit SafeAreaEdges: should not attach local inset listener; covered by passing try-fix-3 tests.
- RecyclerView EmptyView without explicit SafeAreaEdges: should still attach listener; covered by passing try-fix-3 tests.
- Grouped header/footer rebind: must not grow logical children; try-fix-2 failed, try-fix-3 passed after making logical-child add idempotent.
- Deleted file in fix list: gate script fails before verification; needs deletion-aware fix-file handling outside code fix selection.
Verdict: NEEDS_DISCUSSION
Confidence: medium
Summary: Local Android testing found a passing alternative candidate (try-fix-3) that combines the safe-area opt-in contract with an idempotent logical-child rebind guard. The PR gate remains blocked by deleted-file handling, and GitHub PR narrative/CI could not be fetched due authentication.
Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix-1 | EmptyView container marker only | ❌ FAILED | 3 files | Product compile fixed, but PR-added tests require broader safe-area API absent from baseline |
| 2 | try-fix-2 | Safe-area opt-in contract subset | ❌ FAILED | 21 files | Safe-area tests passed, but grouped header/footer logical-child tests failed (expected 8/10, actual 9/11) |
| 3 | try-fix-3 | Safe-area contract plus idempotent logical-child rebind | ✅ PASSED | 21 files | Android CollectionView device tests passed: XHarness exit code 0; 57 passed, 0 failed in filtered run |
| PR | PR #35970 | Broad PR fix | ❌ Gate failed before verification | many | Gate script included deleted HybridWebViewHelper.cs |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| maui-expert-reviewer | 1 | Yes | Suggested toolbar state machine, RecyclerView reuse/dispose split, predictive-back capability, and safe-area container policy |
| maui-expert-reviewer | 2 | Yes | After try-fix-1 failure, recommended safe-area opt-in contract instead of EmptyView marker |
| local test feedback | 3 | Yes | After try-fix-2 failure, added logical-child idempotency guard for grouped header/footer rebinds |
Exhausted: No — stopped because candidate #3 passed the Android CollectionView device-test command and is demonstrably better-scoped than the broad PR fix for the tested failure surface.
Selected Fix: Candidate #3 — It passes the Android CollectionView device tests and isolates the additional logical-child fix to TemplatedItemViewHolder.Bind rather than relying on broader recycle/disconnect behavior.
Report — Final Recommendation
Comparative Report - PR #35970
Candidate ranking
| Rank | Candidate | Regression result | Assessment |
|---|---|---|---|
| 1 | try-fix-3 |
PASSED | Best candidate. It combines the Android safe-area opt-in/listener-refresh contract with an idempotent grouped header/footer logical-child rebind fix, and the saved Android CollectionView device-test run completed successfully with XHarness exit code 0. |
| 2 | pr-plus-reviewer |
Not rerun / gate unavailable | Better than raw pr because it incorporates the expert reviewer's required HybridWebView and predictive-back corrections, but it has no passing regression result and still inherits the PR's broad blast radius. |
| 3 | pr |
Gate failed before verification | The raw PR fix was not behaviorally verified because the gate stopped on deleted-file handling for HybridWebViewHelper.cs. Expert review also found three major issues in the PR fix. |
| 4 | try-fix-2 |
FAILED | The Android safe-area contract compiled and safe-area tests passed, but grouped header/footer logical-child regression tests failed (expected 8/10, actual 9/11). |
| 5 | try-fix-1 |
FAILED | Too narrow. It failed to compile against the PR-added tests because it did not provide the required safe-area explicitness/listener-refresh API surface. |
Comparison
The raw PR attempts a broad multi-area fix, but its Android gate result is not a pass: verification exited before test execution because the deleted src/Core/src/Handlers/HybridWebView/HybridWebViewHelper.cs was still listed as a fix file. In addition, expert review found major actionable issues in predictive back and HybridWebView task/result handling, so the raw PR should not be selected as-is.
pr-plus-reviewer addresses those expert findings conceptually and is therefore stronger than raw pr, but it remains a broad unverified candidate. No saved STEP 5a run demonstrates that the reviewer-amended PR passes the requested Android regression surface, and the user explicitly required failed regression candidates to rank below passing candidates.
Among the try-fix candidates, try-fix-1 failed at build time and try-fix-2 failed two grouped CollectionView regression tests. try-fix-3 is the only candidate with a saved passing Android CollectionView device-test result. It also has the tightest scope for the tested failure surface: it preserves the safe-area contract from try-fix-2 and adds a focused logical-child idempotency guard rather than taking the PR's unrelated broad changes.
Winner
Winner: try-fix-3
try-fix-3 is the single winning candidate because it is the only candidate that passed the requested Android regression tests and it directly covers both proven failure surfaces: RecyclerView-hosted safe-area listener behavior and grouped header/footer logical-child growth on rebind. The PR-based candidates remain lower-ranked due to missing/failed gate verification and unresolved or only-proposed expert-review issues.
Future Action — alternative fix proposed (try-fix-3)
Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-3 as the strongest fix.
Why: try-fix-3 won because it is the only candidate with a saved passing Android CollectionView device-test result. It covers both proven failure surfaces: the safe-area listener contract and grouped header/footer logical-child rebind idempotency, while the PR-based candidates remain unverified and have major expert-review issues.
Please consider applying the candidate diff below (or use it as guidance). Once you push an update, this workflow will re-trigger and re-evaluate.
Candidate diff (try-fix-3)
diff --git a/src/Controls/src/Core/BindableObject.cs b/src/Controls/src/Core/BindableObject.cs
index e1b2d40925..f60147f819 100644
--- a/src/Controls/src/Core/BindableObject.cs
+++ b/src/Controls/src/Core/BindableObject.cs
@@ -6,7 +6,6 @@ using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Dispatching;
@@ -39,8 +38,8 @@ namespace Microsoft.Maui.Controls
}
internal ushort _triggerCount = 0;
- internal Dictionary<TriggerBase, SetterSpecificity> _triggerSpecificity = new();
- readonly Dictionary<int, BindablePropertyContext> _properties = new(4);
+ internal Dictionary<TriggerBase, SetterSpecificity> _triggerSpecificity = new Dictionary<TriggerBase, SetterSpecificity>();
+ readonly Dictionary<BindableProperty, BindablePropertyContext> _properties = new Dictionary<BindableProperty, BindablePropertyContext>(4);
bool _applying;
WeakReference _inheritedContext;
@@ -173,19 +172,66 @@ namespace Microsoft.Maui.Controls
return context == null ? property.DefaultValue : context.Values.GetValue();
}
+ internal LocalValueEnumerator GetLocalValueEnumerator() => new LocalValueEnumerator(this);
+
+ internal sealed class LocalValueEnumerator : IEnumerator<LocalValueEntry>
+ {
+ Dictionary<BindableProperty, BindablePropertyContext>.Enumerator _propertiesEnumerator;
+ internal LocalValueEnumerator(BindableObject bindableObject) => _propertiesEnumerator = bindableObject._properties.GetEnumerator();
+
+ object IEnumerator.Current => Current;
+ public LocalValueEntry Current { get; private set; }
+
+ public bool MoveNext()
+ {
+ if (_propertiesEnumerator.MoveNext())
+ {
+ Current = new LocalValueEntry(_propertiesEnumerator.Current.Key, _propertiesEnumerator.Current.Value.Values.GetValue(), _propertiesEnumerator.Current.Value.Attributes);
+ return true;
+ }
+ return false;
+ }
+
+ public void Dispose() => _propertiesEnumerator.Dispose();
+
+ void IEnumerator.Reset()
+ {
+ ((IEnumerator)_propertiesEnumerator).Reset();
+ Current = null;
+ }
+ }
+
+ internal sealed class LocalValueEntry
+ {
+ internal LocalValueEntry(BindableProperty property, object value, BindableContextAttributes attributes)
+ {
+ Property = property;
+ Value = value;
+ Attributes = attributes;
+ }
+
+ public BindableProperty Property { get; }
+ public object Value { get; }
+ public BindableContextAttributes Attributes { get; }
+ }
+
internal (bool IsSet, T Value)[] GetValues<T>(BindableProperty[] propArray)
{
- var properties = _properties;
+ Dictionary<BindableProperty, BindablePropertyContext> properties = _properties;
var resultArray = new (bool IsSet, T Value)[propArray.Length];
for (int i = 0; i < propArray.Length; i++)
{
- ref var result = ref resultArray[i];
- if (properties.TryGetValue(propArray[i].InternalId, out var context))
+ if (properties.TryGetValue(propArray[i], out var context))
{
var pair = context.Values.GetSpecificityAndValue();
- result.IsSet = pair.Key != SetterSpecificity.DefaultValue;
- result.Value = (T)pair.Value;
+ resultArray[i].IsSet = pair.Key != SetterSpecificity.DefaultValue;
+ resultArray[i].Value = (T)pair.Value;
+ }
+ else
+ {
+ resultArray[i].IsSet = false;
+ resultArray[i].Value = default(T);
}
}
@@ -208,6 +254,19 @@ namespace Microsoft.Maui.Controls
return bpcontext.Values.GetSpecificity() != SetterSpecificity.DefaultValue;
}
+ /// <summary>
+ /// Determines whether a bindable property has been set by a local value, style, binding, or other non-default specificity.
+ /// Unlike IsSet, default-value creation does not count as explicit.
+ /// </summary>
+ /// <param name="targetProperty">The bindable property to check if a value is explicitly set.</param>
+ /// <returns><see langword="true"/> if the target property exists and has been explicitly set. Otherwise <see langword="false"/>.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="targetProperty"/> is <see langword="null"/>.</exception>
+ internal bool IsSetExplicitly(BindableProperty targetProperty)
+ {
+ var bpcontext = GetContext(targetProperty ?? throw new ArgumentNullException(nameof(targetProperty)));
+ return bpcontext is not null && bpcontext.Values.GetSpecificity() != SetterSpecificity.DefaultValue;
+ }
+
/// <summary>
/// Removes a previously set binding from a bindable property.
@@ -703,7 +762,7 @@ namespace Microsoft.Maui.Controls
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- BindablePropertyContext CreateContext(BindableProperty property)
+ BindablePropertyContext CreateAndAddContext(BindableProperty property)
{
var defaultValueCreator = property.DefaultValueCreator;
var context = new BindablePropertyContext { Property = property };
@@ -712,31 +771,15 @@ namespace Microsoft.Maui.Controls
if (defaultValueCreator != null)
context.Attributes = BindableContextAttributes.IsDefaultValueCreated;
+ _properties.Add(property, context);
return context;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property.InternalId, out var result) ? result : null;
+ internal BindablePropertyContext GetContext(BindableProperty property) => _properties.TryGetValue(property, out var result) ? result : null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- BindablePropertyContext GetOrCreateContext(BindableProperty property)
- {
-#if NETSTANDARD
- var context = GetContext(property);
- if (context is null)
- {
- context = CreateContext(property);
- _properties.Add(property.InternalId, context);
- }
-#else
- ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_properties, property.InternalId, out var exists);
- if (!exists)
- {
- context = CreateContext(property);
- }
-#endif
- return context;
- }
+ BindablePropertyContext GetOrCreateContext(BindableProperty property) => GetContext(property) ?? CreateAndAddContext(property);
void RemoveBinding(BindableProperty property, BindablePropertyContext context, SetterSpecificity specificity)
{
diff --git a/src/Controls/src/Core/BindableProperty.cs b/src/Controls/src/Core/BindableProperty.cs
index 371446d3bb..31e0fc7bbf 100644
--- a/src/Controls/src/Core/BindableProperty.cs
+++ b/src/Controls/src/Core/BindableProperty.cs
@@ -6,7 +6,6 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
-using System.Threading;
using Microsoft.Maui.Controls.Xaml;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Graphics.Converters;
@@ -182,9 +181,6 @@ namespace Microsoft.Maui.Controls
/// <summary>A sentinel object used to indicate that a BindableProperty value has not been set.</summary>
public static readonly object UnsetValue = new object();
- private static int _nextInternalId = int.MinValue;
- internal readonly int InternalId;
-
BindableProperty(string propertyName, [DynamicallyAccessedMembers(ReturnTypeMembers)] Type returnType, [DynamicallyAccessedMembers(DeclaringTypeMembers)] Type declaringType, object defaultValue, BindingMode defaultBindingMode = BindingMode.OneWay,
ValidateValueDelegate validateValue = null, BindingPropertyChangedDelegate propertyChanged = null, BindingPropertyChangingDelegate propertyChanging = null,
CoerceValueDelegate coerceValue = null, BindablePropertyBindingChanging bindingChanging = null, bool isReadOnly = false, CreateDefaultValueDelegate defaultValueCreator = null)
@@ -195,8 +191,6 @@ namespace Microsoft.Maui.Controls
throw new ArgumentNullException(nameof(returnType));
if (declaringType is null)
throw new ArgumentNullException(nameof(declaringType));
-
- InternalId = Interlocked.Increment(ref _nextInternalId);
// don't use Enum.IsDefined as its redonkulously expensive for what it does
if (defaultBindingMode != BindingMode.Default && defaultBindingMode != BindingMode.OneWay && defaultBindingMode != BindingMode.OneWayToSource && defaultBindingMode != BindingMode.TwoWay && defaultBindingMode != BindingMode.OneTime)
diff --git a/src/Controls/src/Core/Border/Border.cs b/src/Controls/src/Core/Border/Border.cs
index eef7d2615a..1a505ccf7e 100644
--- a/src/Controls/src/Core/Border/Border.cs
+++ b/src/Controls/src/Core/Border/Border.cs
@@ -457,6 +457,9 @@ namespace Microsoft.Maui.Controls
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for borders
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => IsSetExplicitly(SafeAreaEdgesProperty);
+
/// <summary>
/// Provides the default value for the <see cref="SafeAreaEdges"/> property.
/// </summary>
diff --git a/src/Controls/src/Core/ContentPage/ContentPage.cs b/src/Controls/src/Core/ContentPage/ContentPage.cs
index f98ada0945..c34fa417c3 100644
--- a/src/Controls/src/Core/ContentPage/ContentPage.cs
+++ b/src/Controls/src/Core/ContentPage/ContentPage.cs
@@ -170,6 +170,9 @@ namespace Microsoft.Maui.Controls
return (this as ICrossPlatformLayout).CrossPlatformMeasure(widthConstraint, heightConstraint);
}
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => IsSetExplicitly(SafeAreaEdgesProperty);
+
/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
diff --git a/src/Controls/src/Core/ContentView/ContentView.cs b/src/Controls/src/Core/ContentView/ContentView.cs
index ea256fa81f..e59d177552 100644
--- a/src/Controls/src/Core/ContentView/ContentView.cs
+++ b/src/Controls/src/Core/ContentView/ContentView.cs
@@ -91,6 +91,9 @@ namespace Microsoft.Maui.Controls
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for content views
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => IsSetExplicitly(SafeAreaEdgesProperty);
+
/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
diff --git a/src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs b/src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs
index 449ffeaf72..e8ddf4779e 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/Adapters/EmptyViewAdapter.cs
@@ -235,14 +235,14 @@ namespace Microsoft.Maui.Controls.Handlers.Items
if (content is not View formsView)
{
// No template, EmptyView is not a Forms View, so just display EmptyView.ToString
- return SimpleViewHolder.FromText(content?.ToString(), context, () => GetWidth(parent), () => GetHeight(parent), ItemsView);
+ return SimpleViewHolder.FromText(content?.ToString(), context, () => GetWidth(parent), () => GetHeight(parent), ItemsView, isEmptyView: true);
}
// EmptyView is a Forms View; display that
- return SimpleViewHolder.FromFormsView(formsView, context, () => GetWidth(parent), () => GetHeight(parent), ItemsView);
+ return SimpleViewHolder.FromFormsView(formsView, context, () => GetWidth(parent), () => GetHeight(parent), ItemsView, isEmptyView: true);
}
- var itemContentView = new SizedItemContentView(parent.Context, () => GetWidth(parent), () => GetHeight(parent));
+ var itemContentView = new EmptyViewContentView(parent.Context, () => GetWidth(parent), () => GetHeight(parent));
return new TemplatedItemViewHolder(itemContentView, template, isSelectionEnabled: false);
}
diff --git a/src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs b/src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs
index 848579ca10..fe572940fb 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/SimpleViewHolder.cs
@@ -27,14 +27,14 @@ namespace Microsoft.Maui.Controls.Handlers.Items
itemsView.RemoveLogicalChild(View);
}
- public static SimpleViewHolder FromText(string text, Context context, Func<double> width = null, Func<double> height = null, ItemsView container = null, bool fill = true)
+ public static SimpleViewHolder FromText(string text, Context context, Func<double> width = null, Func<double> height = null, ItemsView container = null, bool fill = true, bool isEmptyView = false)
{
if (fill)
{
// When displaying an EmptyView with Header and Footer, we need to account for the Header and Footer sizes in layout calculations.
// This prevents the EmptyView from occupying the full remaining space.
Label label = new Label() { Text = text, VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center };
- SizedItemContentView itemContentControl = new SizedItemContentView(context, width, height);
+ SizedItemContentView itemContentControl = CreateSizedItemContentView(context, width, height, isEmptyView);
itemContentControl.RealizeContent(label, container);
return new SimpleViewHolder(itemContentControl, null);
}
@@ -43,9 +43,9 @@ namespace Microsoft.Maui.Controls.Handlers.Items
return new SimpleViewHolder(textView, null);
}
- public static SimpleViewHolder FromFormsView(View formsView, Context context, Func<double> width, Func<double> height, ItemsView container)
+ public static SimpleViewHolder FromFormsView(View formsView, Context context, Func<double> width, Func<double> height, ItemsView container, bool isEmptyView = false)
{
- var itemContentControl = new SizedItemContentView(context, width, height);
+ var itemContentControl = CreateSizedItemContentView(context, width, height, isEmptyView);
// Make sure the Visual property is available during renderer creation
Internals.PropertyPropagationExtensions.PropagatePropertyChanged(null, formsView, container);
@@ -60,5 +60,12 @@ namespace Microsoft.Maui.Controls.Handlers.Items
itemContentControl.RealizeContent(formsView, container);
return new SimpleViewHolder(itemContentControl, formsView);
}
+
+ static SizedItemContentView CreateSizedItemContentView(Context context, Func<double> width, Func<double> height, bool isEmptyView)
+ {
+ return isEmptyView
+ ? new EmptyViewContentView(context, width, height)
+ : new SizedItemContentView(context, width, height);
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs b/src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs
index 6f92783558..dc9e3107de 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/SizedItemContentView.cs
@@ -51,4 +51,12 @@ namespace Microsoft.Maui.Controls.Handlers.Items
static double NormalizeDimension(double value) => value == int.MaxValue ? double.PositiveInfinity : value;
}
+
+ internal class EmptyViewContentView : SizedItemContentView, IMauiRecyclerViewEmptyView
+ {
+ public EmptyViewContentView(Context context, Func<double> width, Func<double> height)
+ : base(context, width, height)
+ {
+ }
+ }
}
diff --git a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
index 184d53455b..68936ca66f 100644
--- a/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
+++ b/src/Controls/src/Core/Handlers/Items/Android/TemplatedItemViewHolder.cs
@@ -94,6 +94,7 @@ namespace Microsoft.Maui.Controls.Handlers.Items
View.BindingContext = itemBindingContext;
}
+ itemsView.RemoveLogicalChild(View);
itemsView.AddLogicalChild(View);
}
diff --git a/src/Controls/src/Core/Layout/Layout.cs b/src/Controls/src/Core/Layout/Layout.cs
index 14e83aa3e1..7f7d632612 100644
--- a/src/Controls/src/Core/Layout/Layout.cs
+++ b/src/Controls/src/Core/Layout/Layout.cs
@@ -411,6 +411,9 @@ namespace Microsoft.Maui.Controls
/// <inheritdoc cref="ISafeAreaView2.SafeAreaInsets"/>
Thickness ISafeAreaView2.SafeAreaInsets { set { } } // Default no-op implementation for layouts
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => IsSetExplicitly(SafeAreaEdgesProperty);
+
/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
diff --git a/src/Controls/src/Core/Page/Page.cs b/src/Controls/src/Core/Page/Page.cs
index 693a744288..ea46e5b006 100644
--- a/src/Controls/src/Core/Page/Page.cs
+++ b/src/Controls/src/Core/Page/Page.cs
@@ -276,6 +276,10 @@ namespace Microsoft.Maui.Controls
}
}
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ // Base Page uses legacy IgnoreSafeArea behavior, not the SafeAreaEdges property.
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => false;
+
/// <summary>
/// Raised when the children of this page, and thus potentially the layout, have changed.
/// </summary>
@@ -699,6 +703,15 @@ namespace Microsoft.Maui.Controls
OnAppearing();
Appearing?.Invoke(this, EventArgs.Empty);
+ // Refresh Enabled on the predictive back callback so the animation preview reflects the new state.
+ // Guard with Window.Page check so only the outermost SendAppearing in a recursive chain fires
+ // once, avoiding redundant re-walks in deep hierarchies (e.g. Window → Shell → NavigationPage → ContentPage).
+ var mauiWindow = this.Window as Window;
+ if (mauiWindow?.Page == this)
+ {
+ mauiWindow.NotifyNavigationStateChanged();
+ }
+
var pageContainer = this as IPageContainer<Page>;
pageContainer?.CurrentPage?.SendAppearing();
@@ -840,6 +853,7 @@ namespace Microsoft.Maui.Controls
internal void SendNavigatedTo(NavigatedToEventArgs args)
{
+ // Prevent duplicate OnNavigatedTo during a single navigation burst (fixes #23902).
if (HasNavigatedTo)
{
return;
@@ -848,7 +862,21 @@ namespace Microsoft.Maui.Controls
HasNavigatedTo = true;
NavigatedTo?.Invoke(this, args);
OnNavigatedTo(args);
- (this as IPageContainer<Page>)?.CurrentPage?.SendNavigatedTo(args);
+
+ // Cascade to child page (e.g. TabbedPage → CurrentPage).
+ // On Pop, reset the child flag first — a prior tab change while a modal was open
+ // can leave it true, which would incorrectly block the pop-return (fixes #35756).
+ // PopToRoot is excluded: SendNavigatedFrom already resets all flags before PopToRoot cascades.
+ var containerChild = (this as IPageContainer<Page>)?.CurrentPage;
+ if (containerChild is not null)
+ {
+ if (args.NavigationType == NavigationType.Pop)
+ {
+ containerChild.HasNavigatedTo = false;
+ }
+
+ containerChild.SendNavigatedTo(args);
+ }
}
internal void SendNavigatingFrom(NavigatingFromEventArgs args)
diff --git a/src/Controls/src/Core/ScrollView/ScrollView.cs b/src/Controls/src/Core/ScrollView/ScrollView.cs
index 075b9b9d97..ac7d843dfe 100644
--- a/src/Controls/src/Core/ScrollView/ScrollView.cs
+++ b/src/Controls/src/Core/ScrollView/ScrollView.cs
@@ -566,6 +566,9 @@ namespace Microsoft.Maui.Controls
}
}
+ /// <inheritdoc cref="ISafeAreaView2.HasExplicitSafeAreaEdges"/>
+ bool ISafeAreaView2.HasExplicitSafeAreaEdges => IsSetExplicitly(SafeAreaEdgesProperty);
+
/// <inheritdoc cref="ISafeAreaView2.GetSafeAreaRegionsForEdge"/>
SafeAreaRegions ISafeAreaView2.GetSafeAreaRegionsForEdge(int edge)
{
diff --git a/src/Controls/src/Core/Window/Window.Android.cs b/src/Controls/src/Core/Window/Window.Android.cs
index 9385b83581..270425f8b3 100644
--- a/src/Controls/src/Core/Window/Window.Android.cs
+++ b/src/Controls/src/Core/Window/Window.Android.cs
@@ -7,8 +7,15 @@ using Microsoft.Maui.Handlers;
namespace Microsoft.Maui.Controls
{
- public partial class Window : IPlatformEventsListener
+ public partial class Window : IPlatformEventsListener, IBackNavigationState
{
+ bool IBackNavigationState.CanConsumeBackNavigation =>
+ Navigation.ModalStack.Count > 0 || CanConsumeBackNavigation(Page);
+
+ void RefreshPredictiveBackRegistration() =>
+ (Handler?.PlatformView as MauiAppCompatActivity)
+ ?.UpdatePredictiveBackRegistration();
+
internal Activity PlatformActivity =>
(Handler?.PlatformView as Activity) ?? throw new InvalidOperationException("Window should have an Activity set.");
diff --git a/src/Controls/src/Core/Window/Window.cs b/src/Controls/src/Core/Window/Window.cs
index 3275d5aac1..ac71553a47 100644
--- a/src/Controls/src/Core/Window/Window.cs
+++ b/src/Controls/src/Core/Window/Window.cs
@@ -467,6 +467,10 @@ namespace Microsoft.Maui.Controls
ModalPopped?.Invoke(this, args);
Application?.NotifyOfWindowModalEvent(args);
+ // Refresh the predictive back callback — programmatic PopModalAsync doesn't go through
+ // BackButtonClicked, so we update here to keep Enabled in sync with the modal stack.
+ NotifyNavigationStateChanged();
+
#if WINDOWS
this.Handler?.UpdateValue(nameof(IWindow.TitleBarDragRectangles));
this.Handler?.UpdateValue(nameof(ITitledElement.Title));
@@ -488,6 +492,10 @@ namespace Microsoft.Maui.Controls
ModalPushed?.Invoke(this, args);
Application?.NotifyOfWindowModalEvent(args);
+ // Refresh the predictive back callback — programmatic PushModalAsync doesn't go through
+ // BackButtonClicked, so we update here to keep Enabled in sync with the modal stack.
+ NotifyNavigationStateChanged();
+
#if WINDOWS
this.Handler?.UpdateValue(nameof(IWindow.TitleBarDragRectangles));
this.Handler?.UpdateValue(nameof(ITitledElement.Title));
@@ -751,12 +759,83 @@ namespace Microsoft.Maui.Controls
bool IWindow.BackButtonClicked()
{
+ bool handled;
+
if (Navigation.ModalStack.Count > 0)
{
- return Navigation.ModalStack[Navigation.ModalStack.Count - 1].SendBackButtonPressed();
+ handled = Navigation.ModalStack[Navigation.ModalStack.Count - 1].SendBackButtonPressed();
}
+ else
+ {
+ handled = this.Page?.SendBackButtonPressed() ?? false;
+ }
+
+ // Refresh Enabled on the predictive back callback after a back press changes the navigation state.
+ NotifyNavigationStateChanged();
+ return handled;
+ }
+
+ // Notifies that navigation state has changed so the Android predictive back callback can be updated.
+ // Dispatches to the main thread when called post-await on a thread-pool thread.
+ // No-op on non-Android platforms.
+ internal void NotifyNavigationStateChanged()
+ {
+#if ANDROID
+ if (MainThread.IsMainThread)
+ RefreshPredictiveBackRegistration();
+ else
+ MainThread.BeginInvokeOnMainThread(RefreshPredictiveBackRegistration);
+#endif
+ }
- return this.Page?.SendBackButtonPressed() ?? false;
+ // Returns true when there is in-app back navigation to consume, so the system should not
+ // play the back-to-home animation. Kept in the shared file so it can be unit-tested
+ // without a device (the Android-specific entry point in Window.Android.cs calls this).
+ internal static bool CanConsumeBackNavigation(Page? page)
+ {
+ if (page is null)
+ return false;
+
+ switch (page)
+ {
+ case Shell shell:
+ if (CanConsumeBackNavigation(shell.CurrentPage))
+ return true;
+
+ // Only consume back to close the flyout when it is user-dismissible.
+ // Locked and Disabled both mean the flyout is not user-controllable via back.
+ var flyoutBehavior = shell.GetEffectiveFlyoutBehavior();
+ if (shell.FlyoutIsPresented && flyoutBehavior == FlyoutBehavior.Flyout)
+ {
+ return true;
+ }
+
+ // Shell section nav stack depth (non-NavigationPage modern Shell shape).
+ return shell.CurrentItem?.CurrentItem?.Stack.Count > 1;
+
+ case NavigationPage navigationPage:
+ if (CanConsumeBackNavigation(navigationPage.CurrentPage))
+ return true;
+
+ return navigationPage.Navigation.NavigationStack.Count > 1;
+
+ case FlyoutPage flyoutPage:
+ // In split-mode (tablets), CanChangeIsPresented=false and IsPresented is locked true;
+ // back should NOT be consumed in that state — only consume when the flyout can close.
+ if (flyoutPage.IsPresented && ((IFlyoutPageController)flyoutPage).CanChangeIsPresented)
+ return true;
+
+ return CanConsumeBackNavigation(flyoutPage.Detail);
+
+ case MultiPage<Page> multiPage:
+ return CanConsumeBackNavigation(multiPage.CurrentPage);
+
+ default:
+ // Conservative default: return false for unknown page types.
+ // We cannot know whether a custom container's OnBackButtonPressed() returns true,
+ // so we avoid suppressing the back-to-home animation speculatively.
+ return false;
+ }
}
static double ValidatePositive(double value, [CallerMemberName] string? name = null) =>
diff --git a/src/Core/src/Core/IMauiRecyclerView.cs b/src/Core/src/Core/IMauiRecyclerView.cs
index 7fc4632a43..75c9b0940c 100644
--- a/src/Core/src/Core/IMauiRecyclerView.cs
+++ b/src/Core/src/Core/IMauiRecyclerView.cs
@@ -5,4 +5,8 @@ namespace Microsoft.Maui
internal interface IMauiRecyclerView
{
}
+
+ internal interface IMauiRecyclerViewEmptyView
+ {
+ }
}
diff --git a/src/Core/src/Core/ISafeAreaView2.cs b/src/Core/src/Core/ISafeAreaView2.cs
index 6ded20b814..347b2a32cb 100644
--- a/src/Core/src/Core/ISafeAreaView2.cs
+++ b/src/Core/src/Core/ISafeAreaView2.cs
@@ -3,11 +3,13 @@
/// <summary>
/// Provides functionality for the Page's SafeAreaInsets that may be changed in the future.
/// </summary>
- /// <remarks>
- /// This interface is only recognized on the iOS/Mac Catalyst platforms; other platforms will ignore it.
- /// </remarks>
internal interface ISafeAreaView2
{
+ /// <summary>
+ /// Gets whether the view has an explicitly configured SafeAreaEdges value.
+ /// </summary>
+ bool HasExplicitSafeAreaEdges { get; }
+
/// <summary>
/// Internal property for the Page's SafeAreaInsets Thickness that may be changed in the future.
/// </summary>
diff --git a/src/Core/src/Platform/Android/ContentViewGroup.cs b/src/Core/src/Platform/Android/ContentViewGroup.cs
index ef7a195828..2a58cdaaaa 100644
--- a/src/Core/src/Platform/Android/ContentViewGroup.cs
+++ b/src/Core/src/Platform/Android/ContentViewGroup.cs
@@ -173,6 +173,7 @@ namespace Microsoft.Maui.Platform
/// </summary>
internal void MarkSafeAreaEdgeConfigurationChanged()
{
+ _isInsetListenerSet = MauiWindowInsetListenerExtensions.RefreshMauiWindowInsetListener(this, _context);
_didSafeAreaEdgeConfigurationChange = true;
// Ensure a layout pass so that OnLayout will trigger InvalidateWindowInsets
RequestLayout();
diff --git a/src/Core/src/Platform/Android/LayoutViewGroup.cs b/src/Core/src/Platform/Android/LayoutViewGroup.cs
index b7e1361299..28afa45d7c 100644
--- a/src/Core/src/Platform/Android/LayoutViewGroup.cs
+++ b/src/Core/src/Platform/Android/LayoutViewGroup.cs
@@ -190,6 +190,7 @@ namespace Microsoft.Maui.Platform
/// </summary>
internal void MarkSafeAreaEdgeConfigurationChanged()
{
+ _isInsetListenerSet = MauiWindowInsetListenerExtensions.RefreshMauiWindowInsetListener(this, _context);
_didSafeAreaEdgeConfigurationChange = true;
RequestLayout();
}
diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
index 7c0280da8a..1f813c41cd 100644
--- a/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
+++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.Lifecycle.cs
@@ -148,7 +148,25 @@ namespace Microsoft.Maui
});
if (!preventBackPropagation)
- base.OnBackPressed();
+ {
+ // Disable the callback before dispatching base.OnBackPressed() to prevent re-entry
+ // through OnBackPressedDispatcher, which would re-invoke this same enabled callback.
+ if (_mauiOnBackPressedCallback is not null)
+ {
+ _mauiOnBackPressedCallback.Enabled = false;
+ }
+
+ try
+ {
+ base.OnBackPressed();
+ }
+ finally
+ {
+ // Always re-evaluate after propagation so Enabled reflects the new navigation state.
+ // Using finally ensures the callback isn't permanently disabled if OnBackPressed throws.
+ UpdatePredictiveBackRegistration();
+ }
+ }
}
}
}
\ No newline at end of file
diff --git a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
index aa9b9a210e..de7ae83171 100644
--- a/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
+++ b/src/Core/src/Platform/Android/MauiAppCompatActivity.cs
@@ -1,7 +1,6 @@
using System;
using Android.OS;
using Android.Views;
-using Android.Window;
using AndroidX.Activity;
using AndroidX.AppCompat.App;
using AndroidX.Core.Content.Resources;
@@ -35,27 +34,20 @@ namespace Microsoft.Maui
this.CreatePlatformWindow(IPlatformApplication.Current.Application, savedInstanceState);
}
- // Register predictive back callback (Android 13+/API 33+) if available.
- // This integrates MAUI lifecycle OnBackPressed events with the system back gesture animation.
- // Guidance: route custom back handling through AndroidX OnBackPressedDispatcher so
- // predictive back works correctly:
- // https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture#update-custom
- if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is null)
- {
- _predictiveBackCallback = new PredictiveBackCallback(this);
- // Priority 0 = PRIORITY_DEFAULT: callback invoked only when no higher-priority callback handles the event
- OnBackInvokedDispatcher?.RegisterOnBackInvokedCallback(0, _predictiveBackCallback);
- }
+ // Use OnBackPressedCallback (AndroidX) so the system predictive back-to-home
+ // animation plays when the app has nothing to handle (IsEnabled = false).
+ // IOnBackInvokedCallback (Android 13+ API) was avoided here because registering
+ // one always suppresses the back-to-home animation regardless of IsEnabled.
+ _mauiOnBackPressedCallback = new MauiOnBackPressedCallback(this);
+ OnBackPressedDispatcher.AddCallback(this, _mauiOnBackPressedCallback);
+ UpdatePredictiveBackRegistration();
}
protected override void OnDestroy()
{
- if (OperatingSystem.IsAndroidVersionAtLeast(33) && _predictiveBackCallback is not null)
- {
- OnBackInvokedDispatcher?.UnregisterOnBackInvokedCallback(_predictiveBackCallback);
- _predictiveBackCallback.Dispose();
- _predictiveBackCallback = null;
- }
+ _mauiOnBackPressedCallback?.Remove();
+ _mauiOnBackPressedCallback?.Dispose();
+ _mauiOnBackPressedCallback = null;
base.OnDestroy();
}
@@ -75,21 +67,51 @@ namespace Microsoft.Maui
return handled || implHandled;
}
- PredictiveBackCallback? _predictiveBackCallback;
+ MauiOnBackPressedCallback? _mauiOnBackPressedCallback;
+
+ // Must be called at every navigation state change so that Enabled reflects the current back
+ // stack before the predictive back drag preview starts (Android reads Enabled before commit).
+ // Call sites: Page.SendAppearing, Shell.SendNavigated, NavigationPage, Window.
+ internal void UpdatePredictiveBackRegistration()
+ {
+ if (_mauiOnBackPressedCallback is null)
+ return;
+
+ _mauiOnBackPressedCallback.Enabled = ShouldRegisterPredictiveBackCallback();
+ }
+
+ bool ShouldRegisterPredictiveBackCallback()
+ {
+ var services = IPlatformApplication.Current?.Services;
+ if (services is null)
+ return false;
+
+ // Early exit after the first match — avoids iterating all registered delegates unnecessarily.
+ bool hasAnyHandler = false;
+ foreach (var handler in services.GetLifecycleEventDelegates<AndroidLifecycle.OnBackPressed>())
+ {
+ hasAnyHandler = true;
+ break;
+ }
+
+ if (!hasAnyHandler)
+ return false;
+
+ return this.GetWindow() is IBackNavigationState { CanConsumeBackNavigation: true };
+ }
- sealed class PredictiveBackCallback : Java.Lang.Object, IOnBackInvokedCallback
+ sealed class MauiOnBackPressedCallback : OnBackPressedCallback
{
readonly MauiAppCompatActivity _activity;
- public PredictiveBackCallback(MauiAppCompatActivity activity)
+ public MauiOnBackPressedCallback(MauiAppCompatActivity activity) : base(false)
{
_activity = activity;
}
- public void OnBackInvoked()
+ public override void HandleOnBackPressed()
{
- // Reuse unified handling (will invoke lifecycle events and conditionally propagate).
_activity.HandleBackNavigation();
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Core/src/Platform/Android/MauiScrollView.cs b/src/Core/src/Platform/Android/MauiScrollView.cs
index bc92251800..fa01333a38 100644
--- a/src/Core/src/Platform/Android/MauiScrollView.cs
+++ b/src/Core/src/Platform/Android/MauiScrollView.cs
@@ -318,6 +318,7 @@ namespace Microsoft.Maui.Platform
/// </summary>
internal void MarkSafeAreaEdgeConfigurationChanged()
{
+ _isInsetListenerSet = MauiWindowInsetListenerExtensions.RefreshMauiWindowInsetListener(this, _context);
_didSafeAreaEdgeConfigurationChange = true;
RequestLayout();
}
diff --git a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
index 943a1330cf..546bb1a84f 100644
--- a/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
+++ b/src/Core/src/Platform/Android/MauiWindowInsetListener.cs
@@ -93,37 +93,26 @@ namespace Microsoft.Maui.Platform
/// <param name="view">The view to find a listener for</param>
/// <returns>The local listener if view is in a registered view hierarchy, null otherwise</returns>
internal static MauiWindowInsetListener? FindListenerForView(AView view)
+ {
+ if (!ShouldSetMauiWindowInsetListener(view))
+ {
+ return null;
+ }
+
+ return FindRegisteredListenerForView(view);
+ }
+
+ internal static MauiWindowInsetListener? FindRegisteredListenerForView(AView view)
{
// Walk up the view hierarchy looking for a registered view
var parent = view.Parent;
while (parent is not null)
{
- // Skip setting listener on views inside nested scroll containers or AppBarLayout (except MaterialToolbar)
- // We want the layout listener logic to get applied to the MaterialToolbar itself
- // But we don't want any layout listeners to get applied to the children of MaterialToolbar (like the TitleView)
- // CollectionView/CarouselView items are not excluded to enable per-item SafeAreaEdges control.
- // Performance overhead is negligible due to early pass-through for items without insets.
- if (view is not MaterialToolbar &&
- (parent is AppBarLayout || parent is MauiScrollView))
- {
- return null;
- }
-
if (parent is AView parentView)
{
- // Check if this parent view is registered
- // Clean up dead references while searching
- for (int i = _registeredViews.Count - 1; i >= 0; i--)
+ if (FindRegisteredListener(parentView) is MauiWindowInsetListener listener)
{
- var entry = _registeredViews[i];
- if (!entry.View.TryGetTarget(out var registeredView))
- {
- _registeredViews.RemoveAt(i);
- }
- else if (ReferenceEquals(registeredView, parentView))
- {
- return entry.Listener;
- }
+ return listener;
}
}
@@ -133,6 +122,60 @@ namespace Microsoft.Maui.Platform
return null;
}
+ internal static bool ShouldSetMauiWindowInsetListener(AView view)
+ {
+ var parent = view.Parent;
+ var isInsideRecyclerEmptyView = false;
+
+ while (parent is not null)
+ {
+ if (parent is IMauiRecyclerViewEmptyView)
+ {
+ isInsideRecyclerEmptyView = true;
+ }
+
+ // MaterialToolbar needs its own inset handling, so it is exempt from all listener-suppression branches.
+ // Skip listeners for views inside AppBarLayout/MauiScrollView, and for recycler item views
+ // unless SafeAreaEdges was explicitly set.
+ if (view is not MaterialToolbar &&
+ (parent is AppBarLayout ||
+ parent is MauiScrollView ||
+ (parent is IMauiRecyclerView && !isInsideRecyclerEmptyView && !HasExplicitSafeAreaEdges(view))))
+ {
+ return false;
+ }
+
+ parent = parent.Parent;
+ }
+
+ return true;
+ }
+
+ static MauiWindowInsetListener? FindRegisteredListener(AView parentView)
+ {
+ // Check if this parent view is registered. Clean up dead references while searching.
+ for (int i = _registeredViews.Count - 1; i >= 0; i--)
+ {
+ var entry = _registeredViews[i];
+ if (!entry.View.TryGetTarget(out var registeredView))
+ {
+ _registeredViews.RemoveAt(i);
+ }
+ else if (ReferenceEquals(registeredView, parentView))
+ {
+ return entry.Listener;
+ }
+ }
+
+ return null;
+ }
+
+ static bool HasExplicitSafeAreaEdges(AView view)
+ {
+ return view is ICrossPlatformLayoutBacking { CrossPlatformLayout: ISafeAreaView2 safeAreaView } &&
+ safeAreaView.HasExplicitSafeAreaEdges;
+ }
+
/// <summary>
/// Sets up a view to use this listener for inset handling.
/// This method registers the view and attaches the listener.
@@ -474,7 +517,6 @@ internal static class MauiWindowInsetListenerExtensions
/// <param name="context">The Android context to get the listener from</param>
public static bool TrySetMauiWindowInsetListener(this View view, Context context)
{
- // Check if this view is contained within a registered view first
if (MauiWindowInsetListener.FindListenerForView(view) is MauiWindowInsetListener localListener)
{
ViewCompat.SetOnApplyWindowInsetsListener(view, localListener);
@@ -486,6 +528,37 @@ internal static class MauiWindowInsetListenerExtensions
return false;
}
+ /// <summary>
+ /// Refreshes the MauiWindowInsetListener attached to the specified view after SafeAreaEdges eligibility changes.
+ /// Unlike TrySetMauiWindowInsetListener, this finds the registered parent listener before applying
+ /// eligibility checks so it can detach the listener and reset applied safe areas when the view is
+ /// no longer eligible.
+ /// </summary>
+ /// <param name="view">The Android view to refresh the listener on</param>
+ /// <param name="context">The Android context to get the listener from</param>
+ public static bool RefreshMauiWindowInsetListener(this View view, Context context)
+ {
+ var listener = MauiWindowInsetListener.FindRegisteredListenerForView(view);
+ if (listener is null)
+ {
+ ViewCompat.SetOnApplyWindowInsetsListener(view, null);
+ ViewCompat.SetWindowInsetsAnimationCallback(view, null);
+ return false;
+ }
+
+ if (MauiWindowInsetListener.ShouldSetMauiWindowInsetListener(view))
+ {
+ ViewCompat.SetOnApplyWindowInsetsListener(view, listener);
+ ViewCompat.SetWindowInsetsAnimationCallback(view, listener);
+ return true;
+ }
+
+ ViewCompat.SetOnApplyWindowInsetsListener(view, null);
+ ViewCompat.SetWindowInsetsAnimationCallback(view, null);
+ listener.ResetAppliedSafeAreas(view);
+ return false;
+ }
+
/// <summary>
/// Removes the MauiWindowInsetListener from the specified view and resets its tracked state.
/// This should be called when a view is being detached to ensure proper cleanup.
@@ -499,7 +572,7 @@ internal static class MauiWindowInsetListenerExtensions
ViewCompat.SetWindowInsetsAnimationCallback(view, null);
// Reset view state - prefer local listener if available, otherwise use global
- var listener = MauiWindowInsetListener.FindListenerForView(view);
+ var listener = MauiWindowInsetListener.FindRegisteredListenerForView(view);
listener?.ResetView(view);
}
-}
\ No newline at end of file
+}
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!
This reverts the
BindableObjectmicro-optimization changes from PR #33584 onrelease/10.0.1xx-sr8due to regression risk. The revert restores the previous property-context storage/access behavior and removes theInternalIdpath added in that PR.BindableObject revert (Controls.Core)
_propertiestoDictionary<BindableProperty, BindablePropertyContext>.CollectionsMarshal-based lookup/add path andNETSTANDARDsplit logic.CreateAndAddContextbehavior (_properties.Add(property, context)).GetContextandGetValues<T>toBindableProperty-keyed lookups.GetLocalValueEnumerator(),LocalValueEnumerator, andLocalValueEntry.BindableProperty revert (Controls.Core)
InternalIdinfrastructure:using System.Threading_nextInternalIdInternalIdfieldInterlocked.Increment(...)assignment in constructorBenchmark cleanup
src/Core/tests/Benchmarks/Benchmarks/BindableObjectBenchmarker.csintroduced by PR BindableObject property access micro-optimizations #33584.Regression tests added (Controls.Core.UnitTests)
GetValuesReturnsSetStateAndValueto validateGetValues<T>preserves expected set-state/value semantics for set vs unset properties.LocalValueEnumeratorReturnsLocallySetValuesto validate the restored local value enumeration path (GetLocalValueEnumerator/LocalValueEntry) over locally set values.DefaultValueCreatorCachesValueWhenReentrantPropertyAddsResizeStoreto validate re-entrantDefaultValueCreatorexecution (including additional property sets that grow the backing store) still caches the created value and invokes the creator once.Example of restored lookup path: