From 837e1aa91dd34bbd9e4d4c932a18878be2a4d753 Mon Sep 17 00:00:00 2001 From: JoaoCruz Date: Fri, 15 May 2026 15:05:05 +0100 Subject: [PATCH] Fix #20625: fix compiled binding DataContext inference in ItemTemplate (#21248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix #20625: fix compiled binding DataContext inference in ItemTemplate The Sandbox repro failed with AVLN2100/AVLN2000 when compiled bindings were used with ItemTemplate and name-based DataContext access. Root causes: - No fallback DataContext type was inferred for the root object when x:DataType was absent. - Name-scope lookup could capture stale DataContext metadata from an unrelated traversal branch, causing #ListBoxRoot.DataContext to incorrectly resolve to the item type. Fixes: - Add root-level DataContext fallback inference in AvaloniaXamlIlDataContextTypeTransformer. - Update ScopeRegistrationFinder to resolve DataContext from the current ancestor stack when the matching name is found. - Preserve first-match semantics and prefer root namescope lookup before deferred scopes in name binding resolution. - Add and expand unit tests for ItemTemplate and nested namescope scenarios, including mismatched runtime DataContext behavior. Signed-off-by: João Cruz * Fix #20625: simplify compiled binding regression tests Address review feedback to reduce test complexity in XamlIlTests. This change removes per-test UserControl helper classes and command scaffolding that were not required to validate the compiler behavior. Tests now use inline XAML in each [Fact] and shared lightweight mock types for root and item data. The compiled binding scenarios are still covered, including root fallback inference and named DataContext resolution with ItemTemplate and nested namescopes, but with less boilerplate and better readability. * Fix #20625: remove automatic DataContext fallback to root type Address review feedback by removing the automatic fallback inference to the root type in AvaloniaXamlIlDataContextTypeTransformer, as it is not necessary to fix the issue and is not desired behavior. Also removes the unit tests that validated this specific fallback. --------- Signed-off-by: João Cruz Co-authored-by: Julien Lebosquain --- .../XamlIlBindingPathHelper.cs | 25 ++++---- .../Xaml/XamlIlTests.cs | 62 +++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index b366e98856..3bc9f0dd46 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -349,18 +349,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions break; case BindingExpressionGrammar.NameNode elementName: IXamlType? elementType = null, dataType = null; + (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default; + foreach (var deferredContent in context.ParentNodes().OfType()) { + if (!(elementType is null)) + { + break; + } + (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default; if (!(elementType is null)) { break; } } - if (elementType is null) - { - (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default; - } if (elementType is null) { @@ -506,17 +509,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Ignore name registrations, if we are inside of the nested namescope. if (_childScopesStack.Count == 0) { - if (node is AvaloniaNameScopeRegistrationXamlIlNode registration + if (TargetType is null + && node is AvaloniaNameScopeRegistrationXamlIlNode registration && registration.Name is XamlAstTextNode text && text.Text == Name) { TargetType = registration.TargetType; - } - // We are visiting nodes top to bottom. - // If we have already found target type by its name, - // it means all next nodes will be below, and not applicable for data context inheritance. - else if (TargetType is null && node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata) - { - DataContextType = dataContextTypeMetadata.DataContextType; + DataContextType = _stack + .OfType() + .FirstOrDefault() + ?.DataContextType; } } return node; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 9118378dbd..e921ad5461 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; +using System.Windows.Input; using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; @@ -425,6 +426,54 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal((IEnumerable)["a", "b", "c"], parsed.MyProp.Select(x => x.Value)); } } + + [Fact] + public void Compiled_Binding_Should_Resolve_Named_Root_DataContext_In_ItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = (ListBox)AvaloniaRuntimeXamlLoader.Parse(@" + + + + + + +"); + Assert.NotNull(parsed.ItemTemplate); + } + } + + [Fact] + public void Compiled_Binding_Should_Resolve_Root_Command_From_Nested_ItemTemplate_Namescope() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = (ListBox)AvaloniaRuntimeXamlLoader.Parse(@" + + + + + + + + + + + + +"); + Assert.NotNull(parsed.ItemTemplate); + } + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window @@ -555,4 +604,17 @@ namespace Avalonia.Markup.Xaml.UnitTests } } + + public class CompiledBindingRootMock : UserControl + { + public string Greeting => "Hello"; + public string RootProperty => "RootValue"; + public IReadOnlyList Items { get; } = [new() { Name = "Outer", InnerItems = [new() { Name = "Inner" }] }]; + } + + public class CompiledBindingItemMock + { + public string Name { get; set; } = string.Empty; + public IReadOnlyList InnerItems { get; set; } = Array.Empty(); + } }