Fix for RelativeSource AncestorType bindings not generating compiled bindings under AOT#34408
Fix for RelativeSource AncestorType bindings not generating compiled bindings under AOT#34408BagavathiPerumal wants to merge 2 commits intodotnet:mainfrom
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 34408Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 34408" |
There was a problem hiding this comment.
Pull request overview
This PR fixes a .NET MAUI XAML SourceGen limitation where {RelativeSource AncestorType=...} bindings inside templates were incorrectly routed to string-path Binding(...) creation, which is not trim-safe under AOT. The update enables SourceGen to produce trim-safe TypedBinding when the ancestor type is known at compile time, while preserving the existing fallback behavior for other RelativeSource modes.
Changes:
- Update
KnownMarkups.ProvideValueForBindingExtensionto prefer generating a compiledTypedBindingwhen aRelativeSourcehas a resolvableAncestorType. - Retain the guard that prevents compiling other
RelativeSourcemodes (e.g.,Self,TemplatedParent, and untyped ancestor lookups) using ambientx:DataType. - Add new XAML unit tests (issue #34056) covering both the fixed ancestor-type scenario and the protected
{RelativeSource Self}scenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/Controls/src/SourceGen/KnownMarkups.cs | Refines SourceGen binding compilation logic to allow compiled bindings for RelativeSource AncestorType when the type is already resolved, avoiding trim-unsafe string-path bindings under AOT. |
| src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml | Adds a minimal repro XAML page covering both RelativeSource AncestorType in a DataTemplate and {RelativeSource Self} in a DataTemplate. |
| src/Controls/tests/Xaml.UnitTests/Issues/Maui34056.xaml.cs | Adds unit tests validating the generated binding types across Runtime/XamlC/SourceGen inflators. |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
/azp run maui-pr-uitests |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
@simonrozsival @StephaneDelcroix could you please review this one? |
|
|
2 similar comments
|
|
|
|
kubaflo
left a comment
There was a problem hiding this comment.
Could you please resolve conflicts?
|
|
…iveSource AncestorType (trim-safe under AOT), while preserving the string-based Binding fallback for other RelativeSource modes where the source type is resolved at runtime.
bb08fe6 to
97c3680
Compare
@kubaflo, I have resolved the merge conflicts in this PR. |
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34408 | Add TryGetRelativeSourceAncestorType to detect ElementNode AncestorType and reuse already-resolved ITypeSymbol from context.Types for trim-safe TypedBinding |
✅ PASSED (Gate) | KnownMarkups.cs, Maui34056.xaml, Maui34056.xaml.cs |
Does not handle ValueNode AncestorType shorthand syntax |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Pre-register AncestorType on RelativeSource node in context.Types[markupNode] during ProvideValueForRelativeSourceExtension; trivial 10-line TryGetResolvedSourceType lookup in binding step |
✅ PASS | 1 file (KnownMarkups.cs) | Handles both ElementNode (x:Type) and ValueNode shorthand — addresses reviewer gap |
| 2 | try-fix (claude-sonnet-4.6) | Extend HasExplicitBindingSource gate itself with out ITypeSymbol? ancestorType parameter + TryResolveRelativeSourceAncestorType helper; gate returns false + resolved type when AncestorType is resolvable |
✅ PASS | 1 file (KnownMarkups.cs) | Gate IS the resolver; handles both x:Type and ValueNode shorthand |
| 3 | try-fix (gpt-5.3-codex) | Narrow explicit-source gate: allow compiled binding when binding has binding-level x:DataType AND Source is RelativeSource with AncestorType property present |
✅ PASS | 1 file (KnownMarkups.cs) | Minimal gate-refinement; uses binding-level x:DataType as authoritative source type |
| 4 | try-fix (gpt-5.4) | Source-policy narrowing: refactor gate into source policies; allow AncestorType RelativeSource with binding-local x:DataType; block x:Reference and non-ancestor RelativeSource | ✅ PASS | 1 file (KnownMarkups.cs) | Policy-based classification; avoids type symbol resolution entirely |
| PR | PR #34408 | Add TryGetRelativeSourceAncestorType: detect ElementNode AncestorType, reuse ITypeSymbol from context.Types for trim-safe TypedBinding |
✅ PASSED (Gate) | 3 files | Missing ValueNode shorthand support per reviewer |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | NO NEW IDEAS | All major approaches covered |
| claude-sonnet-4.6 | 2 | NO NEW IDEAS | All major approaches covered |
| gpt-5.3-codex | 2 | NO NEW IDEAS | All major approaches covered |
| gpt-5.4 | 2 | NO NEW IDEAS | All major approaches covered |
Exhausted: Yes
Selected Fix: Candidate #1 (claude-opus-4.6) — Pre-register AncestorType in ProvideValueForRelativeSourceExtension via context.Types[markupNode]. Simplest approach (fewest lines), handles both ElementNode and ValueNode shorthand (addresses reviewer gap), co-locates type registration with type resolution, minimal invasiveness.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Issue #34056, SourceGen/AOT trim-safety, 1 impl + 2 test files |
| Gate | ✅ PASSED | Android — tests fail without fix, pass with fix |
| Try-Fix | ✅ COMPLETE | 4 attempts, 4 passing |
| Report | ✅ COMPLETE |
Summary
PR #34408 fixes a real and important bug: {RelativeSource AncestorType=...} bindings under MauiXamlInflator + AOT fell through to the [RequiresUnreferencedCode] Binding(string,...) constructor and got trimmed at runtime. The fix is functionally correct for the primary scenario (tested) and the Gate confirms it.
However, an unresolved reviewer concern from @simonrozsival (line 708, KnownMarkups.cs) identifies a gap the fix does not address: when AncestorType is written in shorthand syntax — Source={RelativeSource AncestorType={local:PageViewModel}} — the ancestorTypeNode is a ValueNode rather than an ElementNode, and TryGetRelativeSourceAncestorType returns false, silently falling through to the string-based Binding fallback. The PR has no test for this variant.
Try-Fix exploration found a simpler alternative (Candidate #1) that resolves this gap with fewer lines of code.
Root Cause
KnownMarkups.cs::ProvideValueForBindingExtension used HasExplicitBindingSource to gate ALL RelativeSource bindings (including AncestorType) away from the TypedBinding path. This was correct for Self/TemplatedParent/FindAncestor without a type (runtime-resolved sources) but overly broad for AncestorType where the source type IS known at compile time and already registered in context.Types.
Fix Quality
PR's fix (TryGetRelativeSourceAncestorType):
- ✅ Passes tests for
{x:Type ...}syntax - ✅ Correctly gates
Self/TemplatedParentaway - ❌ Only handles
ElementNode(x:Type extension) — does NOT handleValueNodeshorthand syntax{local:SomeVM} - ❌ Unresolved reviewer concern from @simonrozsival is blocking
- ❌ 50-line method when the problem can be solved in ~10 lines
Candidate #1 (Try-Fix, claude-opus-4.6) — Recommended Alternative:
- ✅ Passes all tests (6/6)
- ✅ Handles BOTH
ElementNode({x:Type ...}) ANDValueNodeshorthand syntax - ✅ Addresses the reviewer's unresolved concern
- ✅ ~27 lines changed vs PR's ~44 lines
- ✅ Co-locates type registration with type resolution (pre-registers
ancestorTypeSymbolon the RelativeSource node viacontext.Types[markupNode]inProvideValueForRelativeSourceExtension; thenProvideValueForBindingExtensiondoes a singlecontext.Types.TryGetValue(sourceElementNode, out sourceType)lookup)
Recommended Changes
-
Address the ValueNode shorthand gap — Either adopt Candidate [Draft] Readme WIP #1's pre-registration approach (
context.Types[markupNode] = ancestorTypeSymbolinProvideValueForRelativeSourceExtension) or extendTryGetRelativeSourceAncestorTypeto also handleValueNodeviacontext.Compilation.GetTypeByMetadataName(...). -
Add a test for the shorthand syntax — Add a scenario to
Maui34056.xamlthat usesSource={RelativeSource AncestorType={local:Maui34056PageViewModel}}(without explicitx:Type) to prevent regression. -
Test class naming (minor) — Consider renaming
Maui34056TeststoTestsper convention for XAML unit test nested classes (low priority — author declined but it's per-convention).
Fix Comparison
| Criterion | PR's Fix | Candidate #1 |
|---|---|---|
| Tests pass | ✅ | ✅ |
Handles {x:Type T} AncestorType |
✅ | ✅ |
Handles {local:T} shorthand AncestorType |
❌ | ✅ |
| Lines changed | ~44 | ~27 |
| Reviewer concern addressed | ❌ | ✅ |
| Code co-location | New method in binding step | Registration in RelativeSource step |
Selected Fix: PR (functionally correct for tested cases, but gaps remain) — recommend adopting Candidate #1 approach or addressing gap before merge.
I’ve reviewed the AI summary comments. Only minor concerns were raised, specifically regarding the test class name. As with similar cases we’ve addressed before, the name Maui34056Tests is intentional. Because the file resides in a flat Issues/ folder with many other test files, including the issue number in the class name provides immediate clarity when examining stack traces, test runner output, or search results. Using a generic class name such as Tests would create ambiguity in this context. Therefore, retaining the existing class name is preferable. In addition, based on the review concern regarding AncestorType={local:Maui34056PageViewModel}, this concern is not valid. According to the documentation, AncestorType must reference a UI element type via {x:Type ...}, and ViewModel types are not valid ancestors in the visual tree. Hence, the suggested syntax cannot work and does not require changes. |
| // The AncestorType is typically an x:Type extension (ElementNode). | ||
| // ProvideValueForRelativeSourceExtension already resolved this type | ||
| // and registered it in context.Types — just look it up. | ||
| if (ancestorTypeNode is ElementNode typeExtNode) |
There was a problem hiding this comment.
Source={RelativeSource AncestorType={local:Maui34056PageViewModel}} is also valid syntax and in that case I believe ancestorTypeNode would be a ValueNode with a string value, although I might be wrong. We should have a test case for this scenario too and make sure it works too.
There was a problem hiding this comment.
@simonrozsival, I investigated the syntax {local:Maui34056PageViewModel} and confirmed that it is not valid XAML.
The XAML compiler throws MAUIG1001: "Invalid RelativeSource Mode '(none)'" at build time because {local:...} is not a markup extension and cannot return a System.Type.
Based on official documentation, the AncestorType property must always receive a valid Type. As documented:
“The AncestorType property must be set to a Type… otherwise a XamlParseException is thrown.”
Therefore, the only valid syntax is:
{x:Type local:Maui34056PageViewModel}
This valid form is already handled by the current fix, so no additional code change or test is required for this scenario.
Documentation: https://learn.microsoft.com/en-us/dotnet/maui/fundamentals/data-binding/relative-bindings?view=net-maui-10.0#bind-to-an-ancestor
simonrozsival
left a comment
There was a problem hiding this comment.
Thanks for the PR! This is a step in the right direction for compiled bindings. I hope we can build on this and build comprehensive binding source type inference in .NET 11 (#34696).
🚦 Gate — Test Verification📊 Expand Full Gate —
|
| # | Type | Test Name | Filter |
|---|---|---|---|
| 1 | XamlUnitTest | Maui34056 | Maui34056 |
Verification
| Step | Expected | Actual | Result |
|---|---|---|---|
| Without fix | FAIL | FAIL | ✅ |
| With fix | PASS | PASS | ✅ |
Details
- 📋 Error: Assert.IsType() Failure: Value is not the exact type
Expected: typeof(Microsoft.Maui.Controls.Internals.TypedBinding<Maui34056PageViewModel, ICommand>)
Actual: typeof(Microsoft.Maui.Controls.Binding...
Fix Files Reverted
eng/pipelines/ci-copilot.ymlsrc/Controls/src/SourceGen/KnownMarkups.cs
Base Branch: main | Merge Base: 720a9d4
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
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 issue occurs because KnownMarkups.cs::ProvideValueForBindingExtension had a hasRelativeSource gate that routed all RelativeSource bindings to new Binding(string, ...) — a [RequiresUnreferencedCode] constructor that gets trimmed under AOT Release builds. The gate was necessary to prevent using parent-scope x:DataType as the source type for {RelativeSource Self} bindings. However, it was too broad: when AncestorType={x:Type PageViewModel} is explicitly set, the source type is known at compile time, and a trim-safe TypedBinding should have been generated instead.
Fix Description:
The fix involves restructuring ProvideValueForBindingExtension with a two-step source type resolution:
TryGetRelativeSourceAncestorType (new method): looks up the AncestorType's already-resolved ITypeSymbol from the generator's context.Types dictionary. When AncestorType is present and resolvable, it produces a trim-safe TypedBinding<AncestorType, TProperty>.
HasExplicitBindingSource (renamed from HasRelativeSourceBinding): guards all other explicit source scenarios — RelativeSource without a resolvable AncestorType (Self, TemplatedParent, FindAncestor without a type) and x:Reference bindings. These bindings fall through to the string-based Binding fallback, since x:DataType does not describe the actual binding source in either case.
This enables compiled bindings precisely where the source type is known at compile time, while keeping the correct fallback for all other RelativeSource modes and x:Reference bindings. Zero new type resolution is needed — it reuses the ITypeSymbol already resolved by the SourceGen pipeline.
Issues Fixed
Fixes #34056
Tested the behaviour in the following platforms
Note: NativeAOT scenarios are applicable only to iOS and Mac Catalyst platforms as per the .NET MAUI documentation.
Output Screenshot
34056-BeforeFix.mov
34056-AfterFix.mov