Browse Source

Fix #20625: fix compiled binding DataContext inference in ItemTemplate (#21248)

* 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>

* 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 <joaosantaremdacruz@tecnico.ulisboa.pt>
Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
pull/21371/head
JoaoCruz 1 week ago
committed by GitHub
parent
commit
837e1aa91d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 25
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  2. 62
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs

25
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<NestedScopeMetadataNode>())
{
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<AvaloniaXamlIlDataContextTypeMetadataNode>()
.FirstOrDefault()
?.DataContextType;
}
}
return node;

62
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<string>)["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(@"
<ListBox Name='ListBoxRoot' ItemsSource='{CompiledBinding Items}'
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:CompiledBindingRootMock'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:CompiledBindingItemMock'>
<TextBlock Text='{CompiledBinding #ListBoxRoot.DataContext.RootProperty}' />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>");
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(@"
<ListBox Name='ListBoxRoot' ItemsSource='{CompiledBinding Items}'
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:CompiledBindingRootMock'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:CompiledBindingItemMock'>
<ListBox ItemsSource='{CompiledBinding InnerItems}'>
<ListBox.ItemTemplate>
<DataTemplate x:DataType='local:CompiledBindingItemMock'>
<TextBlock Text='{CompiledBinding #ListBoxRoot.DataContext.RootProperty}' />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>");
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<CompiledBindingItemMock> Items { get; } = [new() { Name = "Outer", InnerItems = [new() { Name = "Inner" }] }];
}
public class CompiledBindingItemMock
{
public string Name { get; set; } = string.Empty;
public IReadOnlyList<CompiledBindingItemMock> InnerItems { get; set; } = Array.Empty<CompiledBindingItemMock>();
}
}

Loading…
Cancel
Save