From dfa80f8e8107baeda13056045ef7852caf15d5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Cruz?= Date: Mon, 23 Mar 2026 19:49:36 +0000 Subject: [PATCH] Fix #20625: fix compiled binding DataContext inference in ItemTemplate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...valoniaXamlIlDataContextTypeTransformer.cs | 11 ++ .../XamlIlBindingPathHelper.cs | 25 +-- .../Xaml/XamlIlTests.cs | 180 ++++++++++++++++++ 3 files changed, 204 insertions(+), 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index a11dea95e4..0e2ccd0dfb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -118,6 +118,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers // Notify the user that we were unable to infer the data context type if they use a compiled binding. inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); } + + if (inferredDataContextTypeNode is null + && directiveDataContextTypeNode is null + && !context.ParentNodes().OfType().Any()) + { + // Root nodes without an explicit x:DataType still need a deterministic + // starting type for compiled bindings. Falling back to the root CLR type + // allows expressions to compile and avoids stale inference from unrelated + // branches, while runtime DataContext semantics remain unchanged. + inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, on.Type.GetClrType()); + } } return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node; 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..78a1b34e36 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_Fallback_To_Root_Type_Without_Explicit_DataType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = new XamlIlCompiledBindingRootControl(); + + var textBlock = Assert.IsType(parsed.Content); + Assert.Equal("Hello", textBlock.Text); + } + } + + [Fact] + public void Compiled_Binding_Should_Resolve_Named_Root_DataContext_In_ItemTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = new XamlIlCompiledBindingTemplateControl(); + + var listBox = Assert.IsType(parsed.Content); + Assert.NotNull(listBox.ItemTemplate); + } + } + + [Fact] + public void Compiled_Binding_Root_Fallback_Should_Not_Crash_With_Incompatible_Runtime_DataContext() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = new XamlIlCompiledBindingRootControlWithMismatchedDataContext(); + + var textBlock = Assert.IsType(parsed.Content); + Assert.Null(textBlock.Text); + } + } + + [Fact] + public void Compiled_Binding_Should_Resolve_Root_Command_From_Nested_ItemTemplate_Namescope() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = new XamlIlCompiledBindingNestedTemplateControl(); + + var listBox = Assert.IsType(parsed.Content); + Assert.NotNull(listBox.ItemTemplate); + } + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window @@ -555,4 +604,135 @@ namespace Avalonia.Markup.Xaml.UnitTests } } + + public class XamlIlCompiledBindingRootControl : UserControl + { + public string Greeting => "Hello"; + + public XamlIlCompiledBindingRootControl() + { + DataContext = this; + AvaloniaRuntimeXamlLoader.Load(@" + + + +", typeof(XamlIlTests).Assembly, this); + } + } + + public class XamlIlCompiledBindingRootControlWithMismatchedDataContext : UserControl + { + public string Greeting => "Hello"; + + public XamlIlCompiledBindingRootControlWithMismatchedDataContext() + { + DataContext = new object(); + AvaloniaRuntimeXamlLoader.Load(@" + + + +", typeof(XamlIlTests).Assembly, this); + } + } + + public class XamlIlCompiledBindingTemplateControl : UserControl + { + public IReadOnlyList Items { get; } = + [new XamlIlCompiledBindingTemplateItem { Name = "Item" }]; + + public ICommand ButtonCommand { get; } = new XamlIlCompiledBindingNoOpCommand(); + + public XamlIlCompiledBindingTemplateControl() + { + DataContext = this; + AvaloniaRuntimeXamlLoader.Load(@" + + + + + + +