[ci-fix-net11] Resolve app theme dynamic resource from parent Application - #36885
Conversation
Resolve the synthetic application theme resource from the parent Application so lazy keys-only resource propagation matches full resource snapshots. Fixes #36794 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84e2cc34-f170-423f-bbba-bf23e780e86f
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36885Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36885" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Fixes a net11 resource-propagation inconsistency where GetMergedResourceKeys() could advertise the synthetic AppThemeBinding.AppThemeResource key, but on-demand TryGetResource() lookups couldn’t actually resolve it—causing app-theme dynamic resources to remain AppTheme.Unspecified after parent assignment in certain cases.
Changes:
- Teach
ResourcesExtensions.TryGetResourceto resolveAppThemeBinding.AppThemeResourceby walking the element parent chain to the owningApplicationand returningApplication.RequestedTheme.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/Core/ResourcesExtensions.cs | Adds a special-case resolution path so the synthetic app-theme resource key can be looked up consistently during lazy/on-demand resource resolution. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 0
This comment has been minimized.
This comment has been minimized.
PureWeen
left a comment
There was a problem hiding this comment.
Adversarial code review
No actionable issues found. Three independent reviewers traced the keys-only parent propagation path, synthetic app-theme key generation, direct lookup behavior, resource precedence, disconnected-element fallback, and the active regression test.
The fix is consistent with existing eager merged-resource behavior: Application.RequestedTheme already wins for AppThemeBinding.AppThemeResource, so resolving that synthetic key from the parent-chain Application closes the lazy lookup gap without changing ordinary dictionary lookup semantics.
Test coverage assessment: ParentSetResolvesAppThemeDynamicResourceWithoutListener is a discriminating regression test: without the fix, lookup returns no value and leaves AppTheme.Unspecified; with the fix, it resolves the parent application theme. Related theme and resource tests cover adjacent behavior.
Prior review status: The existing automated review also reported no findings; this review found no duplicate unresolved concerns.
Methodology: 3 independent reviewers with adversarial consensus.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@PureWeen — new AI review results are available based on this last commit:
9e531b3.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ⚠️ SKIPPED
No tests were detected in this PR.
Recommendation: Add tests to verify the fix using the write-tests-agent.
📱 UI Tests — Button,Label,Layout
Detected UI test categories: Button,Label,Layout
✅ Deep UI tests — 359 passed, 0 failed across 3 categories on platform-pool agent (replaces in-process counts above).
🧪 UI Test Execution Results (deep, platform pool)
| Category | Tests | Snapshot diffs |
|---|---|---|
Button |
71/73 ✓ | — |
Label |
96/98 ✓ | — |
Layout |
192/195 ✓ | — |
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs) |
📋 Pre-Flight — Context & Validation
Issue: #36794 - [ci-scan-net11] [CI] AppThemeTests.ParentSetResolvesAppThemeDynamicResourceWithoutListener fails on Windows Helix (Expected Light, Actual Unspecified)
PR: #36885 - [ci-fix-net11] Resolve app theme dynamic resource from parent Application
Platforms Affected: shared Controls resource lookup; issue observed on Windows and macOS Helix, requested try-fix platform android
Files Changed: 1 implementation, 0 test
Key Findings
- The issue is a deterministic net11.0 regression in the lazy parent resource propagation path:
GetMergedResourceKeysadvertises the syntheticAppThemeBinding.AppThemeResourcekey, but on-demand resolution did not previously produceApplication.RequestedTheme. - PR #36885 changes
ResourcesExtensions.TryGetResourceso the synthetic app-theme key resolves by walking the element parent chain toApplication. - Gate was already skipped before this phase because no tests were detected in the PR; the linked issue points to existing unit test
AppThemeTests.ParentSetResolvesAppThemeDynamicResourceWithoutListener. - Public PR/issue comment surfaces showed no prior blocking review findings;
ghwas unauthenticated, so CI status remained undetermined from this environment.
Code Review Summary
Verdict: NEEDS_DISCUSSION
Confidence: low
Errors: 0 | Warnings: 0 | Suggestions: 1
Key code review findings:
- ℹ
src/Controls/src/Core/ResourcesExtensions.cs:144— harmless redundant fallback walk whenAppThemeBinding.AppThemeResourcehas no parentApplication.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #36885 | Special-case AppThemeBinding.AppThemeResource in TryGetResource and resolve it from the parent Application.RequestedTheme. |
src/Controls/src/Core/ResourcesExtensions.cs |
Original PR |
🔬 Code Review — Deep Analysis
Code Review — PR #36885
Independent Assessment
What this changes: ResourcesExtensions.TryGetResource now resolves the synthetic AppThemeBinding.AppThemeResource key by walking the element parent chain to Application and returning Application.RequestedTheme.
Inferred motivation: The lazy keys-only resource propagation path can advertise the app-theme key via GetMergedResourceKeys, but TryGetResource previously searched only dictionaries, leaving app-theme dynamic resources unresolved.
Reconciliation with PR Narrative
Author claims: Fixes #36794 by making app-theme dynamic resources resolve from the parent Application during parent assignment.
Agreement/disagreement: Agrees. The code matches the claimed root cause and aligns TryGetResource with GetMergedResources, which already injects Application.RequestedTheme.
Prior Review Reconciliation
No prior ❌ Error findings found.
Queried all three surfaces via public REST fallback:
- Top-level reviews: Copilot bot and PureWeen reported no actionable findings.
- Inline review comments: none.
- PR issue comments: dogfood/AzP/review-command comments only.
Blast Radius Assessment
- Runs for all instances: No — only
TryGetResource(..., AppThemeBinding.AppThemeResource, ...). - Startup impact: Limited page/resource initialization impact; no unconditional startup code.
- Static/shared state: No new static/shared state.
- Infrastructure scope: Core resource lookup, so confidence is capped conservatively.
CI Status
- Required-check result: tool-unavailable/undetermined —
gh pr checks --requiredfailed because GitHub CLI is unauthenticated. - Public REST fallback:
maui-prbuild 1529988 still in progress/queued; several integration legs failed.- Blazor failures show
ASPDEPR011in generated Blazor templateProgram.cs. - Samples failures show
MAUIG2045compiled-binding sample XAML errors.
- Blazor failures show
- Classification: undetermined overall; observed failures appear unrelated to this one-line resource lookup change, but CI is not complete.
- Action taken: invoked
azdo-build-investigator; attemptedci-analysisbut unavailable; confidence capped low.
Findings
No ❌ Error or
💡 Suggestion — Avoid redundant lookup when no parent Application exists
src/Controls/src/Core/ResourcesExtensions.cs:144
If key == AppThemeBinding.AppThemeResource and no Application is found, the code falls through and walks the same parent chain again through normal dictionary lookup. This is harmless, but an explicit value = null; return false; after the special-case loop would avoid redundant work.
Failure-Mode Probing
- No
Applicationin parent chain: no null dereference; lookup falls through to existing behavior. - Manual dictionary entry for
AppThemeResource:Application.RequestedThemewins when an app parent exists, matchingGetMergedResources. - Theme changes after attach: existing
Application.OnParentResourcesChangedpropagation remains unchanged. - Null
element: handled by existing loop/fallback behavior.
Verdict: NEEDS_DISCUSSION
Confidence: low
Summary: The code change is sound and I found no blocking code issues. Verdict is not approval-ready only because required CI could not be authenticated via gh, public fallback shows incomplete/red CI, and the skill rules prohibit a ready verdict while CI is undetermined.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix-1 | Resolve AppThemeBinding.AppThemeResource in the keys-only OnResourcesChangedKeys resolver lambda instead of in general-purpose TryGetResource. |
✅ PASS | 1 file | Targeted to the lazy resource propagation path; keeps broad resource lookup semantics unchanged. |
| PR | PR #36885 | Special-case AppThemeBinding.AppThemeResource in ResourcesExtensions.TryGetResource and resolve it from the parent Application.RequestedTheme. |
1 file | Original PR; code review found it sound, but it has broader blast radius because TryGetResource is used by more callers. |
Candidate Details
try-fix-1 — Keys-only resolver special-case
Approach: The failure occurs when parent-set resource propagation uses RealParent.GetMergedResourceKeys() and then resolves only matching DynamicResource keys on demand. AppThemeBinding.AppThemeResource is synthetic and not stored in a ResourceDictionary, so the resolver lambda in Element.OnResourcesChangedKeys was taught to resolve that one synthetic key from the parent Application.RequestedTheme.
Why different from PR: PR #36885 changes ResourcesExtensions.TryGetResource, which affects all callers of general resource lookup. Candidate 1 leaves TryGetResource unchanged and scopes the synthetic-resource handling to the keys-only propagation resolver that triggers the failing scenario.
Diff: See CustomAgentLogsTmp/PRState/36885/PRAgent/try-fix/attempt-1/fix.diff and CustomAgentLogsTmp/PRState/36885/PRAgent/try-fix-1/content.md.
Test results: dotnet test src/Controls/tests/Core.UnitTests/Controls.Core.UnitTests.csproj --filter "FullyQualifiedName~AppThemeTests.ParentSetResolvesAppThemeDynamicResourceWithoutListener" --no-restore passed: 1 passed, 0 failed.
Failure analysis: N/A — candidate passed after a namespace qualification correction during the attempt. Self-review found 0 findings.
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 1 | Yes | Candidate 1: handle the synthetic app-theme resource in the keys-only resolver rather than broadening TryGetResource. |
| claude-opus-4.7 | 2 | Not run | Stop condition met: Candidate 1 passed the regression test and is demonstrably narrower than the PR fix. |
| gpt-5.3-codex | 2 | Not run | Stop condition met. |
| gpt-5.5 | 2 | Not run | Stop condition met. |
Exhausted: No — stopped because a meaningfully different candidate passed the targeted regression test and is narrower than the PR fix.
Selected Fix: Candidate #1 — It localizes synthetic AppThemeResource handling to the lazy keys-only propagation resolver, while PR #36885 changes general-purpose TryGetResource.
🏁 Report — Final Recommendation
Comparative Fix Report — PR #36885
Candidates compared
| Rank | Candidate | Regression result | Summary |
|---|---|---|---|
| 1 | try-fix-1 |
✅ PASS | Resolves AppThemeBinding.AppThemeResource inside the keys-only Element.OnResourcesChangedKeys resolver, directly in the path that regressed. This is the narrowest passing fix. |
| 2 | pr |
Resolves the synthetic app-theme key in general ResourcesExtensions.TryGetResource. The expert reviewer found the code sound, but the fix has broader reach because TryGetResource is used outside the lazy keys-only propagation path. |
|
| 3 | pr-plus-reviewer |
Same as pr; the expert reviewer produced no actionable feedback, so no sandbox changes were applied. |
No candidate failed the regression test. The only candidate with recorded regression-test evidence is try-fix-1, which passed AppThemeTests.ParentSetResolvesAppThemeDynamicResourceWithoutListener.
Analysis
The raw PR fix is correct: it teaches TryGetResource how to resolve the synthetic AppThemeBinding.AppThemeResource from the parent Application.RequestedTheme, matching the synthetic key already emitted by GetMergedResourceKeys() and full merged-resource snapshots.
However, try-fix-1 is more targeted. The observed failure is specifically in the lazy keys-only resource propagation path where Element.OnResourcesChangedKeys(IEnumerable<string> keys) receives keys from RealParent.GetMergedResourceKeys() and resolves only dynamic-resource matches on demand. Handling the synthetic app-theme resource in that resolver fixes the failing path without changing general-purpose TryGetResource behavior for every caller.
pr-plus-reviewer does not improve on pr because the expert reviewer found no actionable issues to apply.
Winning candidate
Winner: try-fix-1
try-fix-1 wins because it is the only candidate with recorded passing regression-test evidence and it has the narrowest behavioral blast radius. The PR fix is sound, but changing TryGetResource is a broader surface than necessary for this regression.
🧭 Next Steps — alternative fix proposed (try-fix-1)
Automated review — alternative fix proposed
The expert-reviewer evaluation compared the PR fix against automatically generated candidates and selected try-fix-1 as the strongest fix.
Why: try-fix-1 won because it passed the recorded regression test and fixes the synthetic AppThemeResource resolution in the lazy keys-only propagation path where the failure occurs. The PR fix is code-sound, but it changes general TryGetResource behavior and has broader blast radius.
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-1)
diff --git a/src/Controls/src/Core/Element/Element.cs b/src/Controls/src/Core/Element/Element.cs
index 35dde97ecb..05a822882d 100644
--- a/src/Controls/src/Core/Element/Element.cs
+++ b/src/Controls/src/Core/Element/Element.cs
@@ -878,6 +878,21 @@ namespace Microsoft.Maui.Controls
// not just return the first match from TryGetResource.
if (key.StartsWith(Style.StyleClassPrefix, StringComparison.Ordinal))
return this.GetMergedStyleClassResource(key);
+
+ // AppThemeResource is a synthetic resource not stored in any ResourceDictionary.
+ // Resolve it directly from the Application's RequestedTheme.
+ if (key == AppThemeBinding.AppThemeResource)
+ {
+ IElementDefinition current = this;
+ while (current != null)
+ {
+ if (current is Application app)
+ return app.RequestedTheme;
+ current = current.Parent;
+ }
+ return ApplicationModel.AppTheme.Unspecified;
+ }
+
return this.TryGetResource(key, out var v) ? v : null;
});
}
|
/azp run |
|
Current exact-base evidence from builds 1530284/1530350 confirms this is the active deterministic blocker. The existing |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
|
Independent full gate verification is now complete against merge base |
|
Replacement main build 1530412 no longer fails the AppTheme test. Its current Release Helix red is instead |
|
/azp run |
|
Azure Pipelines: Successfully started running 3 pipeline(s). |
|
Device build 1530466 is fully accounted for by exact-base build 1530285: both fail the same six |
|
Main build 1530464 is also fully classified: all build, pack, Helix, AOT, iOS, Android, WindowsTemplates, MultiProject, and general Build jobs passed. Samples recovered on automatic retry. The sole final red is |
|
UI build 1530465 completed after its long MacCatalyst retries. Shell, ViewBase/VisualState/Window, CollectionView retry, and Layout (2h31m) all passed. The four final red shards—Android API30 Shell, WinUI CV1, WinUI CollectionView, and Android Material3 API36—are all already completed red with the same job names in the exact |
|
@Redth @StephaneDelcroix — this base-fix PR is now ready for a human current-head review. Independent verification proves |
|
@mattleibow — this one-file net11.0 base fix is the critical path for six completed extensibility PRs. Independent verification: AppThemeTests is 12/13 without the fix (only |
kubaflo
left a comment
There was a problem hiding this comment.
Multi-model review (round 1) — code verified directly (15-line change, small enough to fully reason about) + CI classified against the net11.0 baseline.
✅ Code — LGTM
TryGetResource now special-cases the synthetic AppThemeBinding.AppThemeResource key ("__MAUI_ApplicationTheme__") by walking up to the Application and returning app.RequestedTheme. This is correct and closes a real inconsistency: GetMergedResources (:50) and GetMergedResourceKeys (:86) both inject this synthetic key, but the key is never stored in any real ResourceDictionary/SystemResources, so the old TryGetResource returned false for it — a lookup that the merged-resource views claimed would succeed. The fix makes them consistent, returning the same app.RequestedTheme value the merged view produces.
Verified:
- Blast radius is low — guarded by
key == AppThemeResource; every other key path is byte-for-byte unchanged, and the guard is an ordinal string compare (~O(1) for non-matches). out valueis assigned on thereturn truepath; if noApplicationis found the block falls through to the existing loop unchanged.- Precedence matches
GetMergedResources(Application wins for this key), and detached elements (no parent Application) fall through tofalseexactly as before.
CI — red, but confirmed pre-existing / branch-wide (not caused by this PR)
All three required checks are red, but the net11.0 base branch itself is red on the same legs:
maui-pr-devicetestsfails on every platform here — andrefs/heads/net11.0(build 1530597) and every other net11 PR (#36109, #35892, #36654, #36657, …) fail it identically. Branch-wide.maui-pr-uitests— same:refs/heads/net11.0(build 1530878) + preview7 + every PR fail it.maui-pr— fails only on the Integration-Blazor / Integration-Samples legs (macOS+windows), the known pre-existing net11 flake (same legs fail on unrelated PRs like #36888). A 15-line resource-resolution change cannot cause Blazor/Samples integration failures on all platforms.
So the red CI is inherited from a broadly-red net11.0 branch, not this change.
Verdict: code is correct and safe. Not marking approved only because required CI is red (branch-wide net11 breakage) — per policy I don't approve over red CI even when the failures are PR-unrelated. Once the net11 branch CI is repaired this is a clean merge. Deferring the merge call to the net11 owners (already pinged).
🤖 Reviewed with Opus 4.8 lead; code verified line-by-line, CI classified against the net11.0 baseline via AzDO build history.
kubaflo
left a comment
There was a problem hiding this comment.
Looks good but leaving for @StephaneDelcroix for the final decision
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!
Root cause
The .NET 11 lazy parent resource propagation path enumerates synthetic
AppThemeBinding.AppThemeResourcekeys throughGetMergedResourceKeys, but its on-demandTryGetResourcelookup only searched resource dictionaries. The synthetic key therefore remained unresolved and dynamic resources could retainAppTheme.Unspecifiedwhen a parent was assigned without an existing resource listener.Description of Change
Resolve
AppThemeBinding.AppThemeResourcedirectly from theApplicationfound in the element parent chain. This keeps the lazy keys-only path consistent with full merged-resource snapshots and preserves Application-wins semantics.Behavioral impact
App-theme dynamic resources now resolve to
Application.RequestedThemewhen parent propagation occurs, including views that did not already have a resource listener. Other resource lookup behavior is unchanged.Validation
AppThemeTests.ParentSetResolvesAppThemeDynamicResourceWithoutListener: Debug 1/1 passed; Release 1/1 passedDynamicResourceTests|ResourceDictionaryTests|AppThemeTests: Debug 72/72 passed; Release 72/72 passednet11.0with platform target frameworks disabledgit diff --checkpassedIssues Fixed
Fixes #36794