Browse Source

Try to infer DataContext type from the $parent and #named compiled binding path parts (#17204)

* Try to infer DataContext type from #named binding nodes

* Try to infer DataContext type from $parent binding nodes

* Use new syntax in the repo (Rider still marks it as an error)

* Add tests ensuring type casing still works

* Fix $parent regression

* Make new tests StringSyntax compatible
pull/17259/head
Max Katz 1 year ago
committed by GitHub
parent
commit
be7bb765fd
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 3
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  2. 104
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  3. 156
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

3
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -177,7 +177,8 @@
<MultiBinding Converter="{x:Static BoolConverters.And}"> <MultiBinding Converter="{x:Static BoolConverters.And}">
<MultiBinding Converter="{x:Static BoolConverters.Or}" > <MultiBinding Converter="{x:Static BoolConverters.Or}" >
<Binding Path="IsActive" /> <Binding Path="IsActive" />
<Binding Path="#Main.((vm:ControlDetailsViewModel)DataContext).ShowInactiveFrames" /> <!-- Rider marks it as an error, because it doesn't know about new binding rules yet. -->
<Binding Path="#Main.DataContext.ShowInactiveFrames" />
</MultiBinding> </MultiBinding>
<Binding Path="IsVisible" /> <Binding Path="IsVisible" />
</MultiBinding> </MultiBinding>

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

@ -197,8 +197,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
if (avaloniaPropertyFieldMaybe != null) if (avaloniaPropertyFieldMaybe != null)
{ {
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, var isDataContextProperty = avaloniaPropertyFieldMaybe.Name == "DataContextProperty" && Equals(avaloniaPropertyFieldMaybe.DeclaringType, context.GetAvaloniaTypes().StyledElement);
XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); var propertyType = isDataContextProperty
? (nodes.LastOrDefault() as IXamlIlBindingPathNodeWithDataContextType)?.DataContextType
: null;
propertyType ??= XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo);
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, propertyType));
} }
else if (GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName) is IXamlProperty clrProperty) else if (GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName) is IXamlProperty clrProperty)
{ {
@ -297,34 +302,55 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
nodes.Add(new TemplatedParentPathElementNode(templatedParentType.GetClrType())); nodes.Add(new TemplatedParentPathElementNode(templatedParentType.GetClrType()));
break; break;
case BindingExpressionGrammar.AncestorNode ancestor: case BindingExpressionGrammar.AncestorNode ancestor:
if (ancestor.Namespace is null && ancestor.TypeName is null) var styledElement = context.GetAvaloniaTypes().StyledElement;
var ancestorTypeFilter = !(ancestor.Namespace is null && ancestor.TypeName is null) ? GetType(ancestor.Namespace, ancestor.TypeName) : null;
var ancestorNode = context
.ParentNodes()
.OfType<XamlAstConstructableObjectNode>()
.Where(x => styledElement.IsAssignableFrom(x.Type.GetClrType()))
.Skip(1)
.Where(x => ancestorTypeFilter is not null
? ancestorTypeFilter.IsAssignableFrom(x.Type.GetClrType()) : true)
.ElementAtOrDefault(ancestor.Level);
IXamlType? dataContextType = null;
if (ancestorNode is not null)
{ {
var styledElementType = context.GetAvaloniaTypes().StyledElement; var isSkipping = true;
var ancestorType = context foreach (var node in context.ParentNodes())
.ParentNodes()
.OfType<XamlAstConstructableObjectNode>()
.Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType()))
.Skip(1)
.ElementAtOrDefault(ancestor.Level)
?.Type.GetClrType();
if (ancestorType is null)
{ {
throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); if (node == ancestorNode)
isSkipping = false;
if (node is AvaloniaNameScopeRegistrationXamlIlNode)
break;
if (!isSkipping && node is AvaloniaXamlIlDataContextTypeMetadataNode metadataNode)
{
dataContextType = metadataNode.DataContextType;
break;
}
} }
nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level));
} }
else
// We need actual ancestor for a correct DataContextType,
// but since in current design bindings do a double-work by enumerating the tree,
// we want to keep original ancestor type filter, if it was present.
var bindingAncestorType = ancestorTypeFilter is not null
? ancestorTypeFilter
: ancestorNode?.Type.GetClrType();
if (bindingAncestorType is null)
{ {
nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); throw new XamlX.XamlTransformException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo);
} }
nodes.Add(new FindAncestorPathElementNode(bindingAncestorType, ancestor.Level, dataContextType));
break; break;
case BindingExpressionGrammar.NameNode elementName: case BindingExpressionGrammar.NameNode elementName:
IXamlType? elementType = null; IXamlType? elementType = null, dataType = null;
foreach (var deferredContent in context.ParentNodes().OfType<NestedScopeMetadataNode>()) foreach (var deferredContent in context.ParentNodes().OfType<NestedScopeMetadataNode>())
{ {
elementType = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name); (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name) ?? default;
if (!(elementType is null)) if (!(elementType is null))
{ {
break; break;
@ -332,14 +358,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
if (elementType is null) if (elementType is null)
{ {
elementType = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name); (elementType, dataType) = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name) ?? default;
} }
if (elementType is null) if (elementType is null)
{ {
throw new XamlX.XamlTransformException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo); throw new XamlX.XamlTransformException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo);
} }
nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType, dataType));
break; break;
case BindingExpressionGrammar.TypeCastNode typeCastNode: case BindingExpressionGrammar.TypeCastNode typeCastNode:
var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName); var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName);
@ -420,8 +446,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
string Name { get; } string Name { get; }
IXamlType? TargetType { get; set; } IXamlType? TargetType { get; set; }
IXamlType? DataContextType { get; set; }
public static IXamlType? GetTargetType(IXamlAstNode namescopeRoot, string name) public static (IXamlType Target, IXamlType? DataContextType)? GetTargetType(IXamlAstNode namescopeRoot, string name)
{ {
// If we start from the nested scope - skip it. // If we start from the nested scope - skip it.
if (namescopeRoot is NestedScopeMetadataNode scope) if (namescopeRoot is NestedScopeMetadataNode scope)
@ -431,7 +458,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var finder = new ScopeRegistrationFinder(name); var finder = new ScopeRegistrationFinder(name);
namescopeRoot.Visit(finder); namescopeRoot.Visit(finder);
return finder.TargetType; return finder.TargetType is not null ? (finder.TargetType, DataType: finder.DataContextType) : null;
} }
void IXamlAstVisitor.Pop() void IXamlAstVisitor.Pop()
@ -455,12 +482,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
// 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 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) if (_childScopesStack.Count == 0)
{ {
if (registration.Name is XamlAstTextNode text && text.Text == Name) if (node is AvaloniaNameScopeRegistrationXamlIlNode registration
&& registration.Name is XamlAstTextNode text && text.Text == Name)
{ {
TargetType = registration.TargetType; 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;
}
} }
return node; return node;
} }
@ -473,6 +508,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen); void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen);
} }
interface IXamlIlBindingPathNodeWithDataContextType
{
IXamlType? DataContextType { get; }
}
class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode
{ {
public XamlIlNotPathElementNode(IXamlType boolType) public XamlIlNotPathElementNode(IXamlType boolType)
@ -533,17 +573,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
} }
class FindAncestorPathElementNode : IXamlIlBindingPathElementNode class FindAncestorPathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType
{ {
private readonly int _level; private readonly int _level;
public FindAncestorPathElementNode(IXamlType ancestorType, int level) public FindAncestorPathElementNode(IXamlType ancestorType, int level, IXamlType? dataContextType)
{ {
Type = ancestorType; Type = ancestorType;
_level = level; _level = level;
DataContextType = dataContextType;
} }
public IXamlType Type { get; } public IXamlType Type { get; }
public IXamlType? DataContextType { get; }
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{ {
@ -573,17 +615,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
} }
class ElementNamePathElementNode : IXamlIlBindingPathElementNode class ElementNamePathElementNode : IXamlIlBindingPathElementNode, IXamlIlBindingPathNodeWithDataContextType
{ {
private readonly string _name; private readonly string _name;
public ElementNamePathElementNode(string name, IXamlType elementType) public ElementNamePathElementNode(string name, IXamlType elementType, IXamlType? dataType)
{ {
_name = name; _name = name;
Type = elementType; Type = elementType;
DataContextType = dataType;
} }
public IXamlType Type { get; } public IXamlType Type { get; }
public IXamlType? DataContextType { get; }
public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen)
{ {

156
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -1282,6 +1282,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void SupportsParentInPathWithTypeAndLevelFilter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border x:Name='p2'>
<Border x:Name='p1'>
<Button x:Name='p0'>
<TextBlock x:Name='textBlock' Text='{CompiledBinding $parent[Control;1].Name}' />
</Button>
</Border>
</Border>
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
Assert.Equal("p1", textBlock.Text);
}
}
[Fact] [Fact]
public void SupportConverterWithParameter() public void SupportConverterWithParameter()
{ {
@ -2175,6 +2197,140 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void ResolvesElementNameDataContextTypeBasedOnContext()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
x:Name='MyWindow'>
<TextBlock Text='{CompiledBinding ElementName=MyWindow, Path=DataContext.StringProperty}' Name='textBlock' />
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void ResolvesElementNameDataContextTypeBasedOnContextShortSyntax()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
x:Name='MyWindow'>
<TextBlock Text='{CompiledBinding #MyWindow.DataContext.StringProperty}' Name='textBlock' />
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void TypeCastWorksWithElementNameDataContext()
{
// By default, DataContext will infer DataType from the XAML context, which will be local:TestDataContext here.
// But developer should be able to re-define this type via type casing, if they know better.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
x:Name='MyWindow'>
<Panel>
<TextBlock Text='{CompiledBinding $parent.((Button)DataContext).Tag}' Name='textBlock' />
</Panel>
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var panelDataContext = new Button { Tag = "foo" };
((Panel)window.Content!).DataContext = panelDataContext;
Assert.Equal(panelDataContext.Tag, textBlock.Text);
}
}
[Fact]
public void ResolvesParentDataContextTypeBasedOnContext()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
x:Name='MyWindow'>
<Panel>
<TextBlock Text='{CompiledBinding $parent[Panel].DataContext.StringProperty}' Name='textBlock' />
</Panel>
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void ResolvesParentDataContextTypeBasedOnContextShortSyntax()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = (Window)AvaloniaRuntimeXamlLoader.Load(@"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
x:Name='MyWindow'>
<Panel>
<TextBlock Text='{CompiledBinding $parent.DataContext.StringProperty}' Name='textBlock' />
</Panel>
</Window>");
var textBlock = window.GetControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
static void Throws(string type, Action cb) static void Throws(string type, Action cb)
{ {
try try

Loading…
Cancel
Save