Browse Source

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 <joaosantaremdacruz@tecnico.ulisboa.pt>
pull/21248/head
João Cruz 6 months ago
parent
commit
dfa80f8e81
  1. 11
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  2. 25
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  3. 180
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

11
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. // Notify the user that we were unable to infer the data context type if they use a compiled binding.
inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
} }
if (inferredDataContextTypeNode is null
&& directiveDataContextTypeNode is null
&& !context.ParentNodes().OfType<XamlAstConstructableObjectNode>().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; return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node;

25
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -349,18 +349,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
break; break;
case BindingExpressionGrammar.NameNode elementName: case BindingExpressionGrammar.NameNode elementName:
IXamlType? elementType = null, dataType = null; IXamlType? elementType = null, dataType = null;
(elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default;
foreach (var deferredContent in context.ParentNodes().OfType<NestedScopeMetadataNode>()) foreach (var deferredContent in context.ParentNodes().OfType<NestedScopeMetadataNode>())
{ {
if (!(elementType is null))
{
break;
}
(elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default; (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default;
if (!(elementType is null)) if (!(elementType is null))
{ {
break; break;
} }
} }
if (elementType is null)
{
(elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default;
}
if (elementType is null) 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. // Ignore name registrations, if we are inside of the nested namescope.
if (_childScopesStack.Count == 0) 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) && registration.Name is XamlAstTextNode text && text.Text == Name)
{ {
TargetType = registration.TargetType; TargetType = registration.TargetType;
} DataContextType = _stack
// We are visiting nodes top to bottom. .OfType<AvaloniaXamlIlDataContextTypeMetadataNode>()
// If we have already found target type by its name, .FirstOrDefault()
// it means all next nodes will be below, and not applicable for data context inheritance. ?.DataContextType;
else if (TargetType is null && node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata)
{
DataContextType = dataContextTypeMetadata.DataContextType;
} }
} }
return node; return node;

180
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Input;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
@ -425,6 +426,54 @@ namespace Avalonia.Markup.Xaml.UnitTests
Assert.Equal((IEnumerable<string>)["a", "b", "c"], parsed.MyProp.Select(x => x.Value)); Assert.Equal((IEnumerable<string>)["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<TextBlock>(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<ListBox>(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<TextBlock>(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<ListBox>(parsed.Content);
Assert.NotNull(listBox.ItemTemplate);
}
}
} }
public class XamlIlBugTestsEventHandlerCodeBehind : Window 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(@"
<UserControl x:Class='Avalonia.Markup.Xaml.UnitTests.XamlIlCompiledBindingRootControl'
xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
>
<TextBlock Text='{CompiledBinding Greeting}'/>
</UserControl>
", typeof(XamlIlTests).Assembly, this);
}
}
public class XamlIlCompiledBindingRootControlWithMismatchedDataContext : UserControl
{
public string Greeting => "Hello";
public XamlIlCompiledBindingRootControlWithMismatchedDataContext()
{
DataContext = new object();
AvaloniaRuntimeXamlLoader.Load(@"
<UserControl x:Class='Avalonia.Markup.Xaml.UnitTests.XamlIlCompiledBindingRootControlWithMismatchedDataContext'
xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
>
<TextBlock Text='{CompiledBinding Greeting}'/>
</UserControl>
", typeof(XamlIlTests).Assembly, this);
}
}
public class XamlIlCompiledBindingTemplateControl : UserControl
{
public IReadOnlyList<XamlIlCompiledBindingTemplateItem> Items { get; } =
[new XamlIlCompiledBindingTemplateItem { Name = "Item" }];
public ICommand ButtonCommand { get; } = new XamlIlCompiledBindingNoOpCommand();
public XamlIlCompiledBindingTemplateControl()
{
DataContext = this;
AvaloniaRuntimeXamlLoader.Load(@"
<UserControl x:Class='Avalonia.Markup.Xaml.UnitTests.XamlIlCompiledBindingTemplateControl'
xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:XamlIlCompiledBindingTemplateControl'
>
<ListBox Name='ListBoxRoot' ItemsSource='{CompiledBinding Items}'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:XamlIlCompiledBindingTemplateItem'>
<StackPanel>
<TextBlock Text='{CompiledBinding Name}' />
<Button Command='{CompiledBinding #ListBoxRoot.DataContext.ButtonCommand}'
CommandParameter='{CompiledBinding .}' />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
", typeof(XamlIlTests).Assembly, this);
}
}
public class XamlIlCompiledBindingNestedTemplateControl : UserControl
{
public IReadOnlyList<XamlIlCompiledBindingOuterItem> Items { get; } =
[new XamlIlCompiledBindingOuterItem()];
public ICommand ButtonCommand { get; } = new XamlIlCompiledBindingNoOpCommand();
public XamlIlCompiledBindingNestedTemplateControl()
{
DataContext = this;
AvaloniaRuntimeXamlLoader.Load(@"
<UserControl x:Class='Avalonia.Markup.Xaml.UnitTests.XamlIlCompiledBindingNestedTemplateControl'
xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:XamlIlCompiledBindingNestedTemplateControl'
>
<ListBox Name='ListBoxRoot' ItemsSource='{CompiledBinding Items}'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:XamlIlCompiledBindingOuterItem'>
<ListBox ItemsSource='{CompiledBinding InnerItems}'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:XamlIlCompiledBindingTemplateItem'>
<Button Command='{CompiledBinding #ListBoxRoot.DataContext.ButtonCommand}'
CommandParameter='{CompiledBinding .}' />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
", typeof(XamlIlTests).Assembly, this);
}
}
public class XamlIlCompiledBindingOuterItem
{
public IReadOnlyList<XamlIlCompiledBindingTemplateItem> InnerItems { get; } =
[new XamlIlCompiledBindingTemplateItem { Name = "Inner" }];
}
public class XamlIlCompiledBindingTemplateItem
{
public string Name { get; set; } = string.Empty;
}
public class XamlIlCompiledBindingNoOpCommand : ICommand
{
public event EventHandler? CanExecuteChanged
{
add { }
remove { }
}
public bool CanExecute(object? parameter) => true;
public void Execute(object? parameter)
{
}
}
} }

Loading…
Cancel
Save