Browse Source

Implement smart inference for DataTemplates. Fix calculated DataContext types for DataContext bindings.

pull/3001/head
Jeremy Koritzinsky 7 years ago
parent
commit
777693cee2
  1. 2
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  2. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  3. 5
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs
  4. 144
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  5. 3
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs
  6. 4
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  7. 148
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

2
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -51,7 +51,7 @@
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlDataContextTypeTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlDesignPropertiesTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlNestedScopeMetadataRemover.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlCompiledBindingsMetadataRemover.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlMetadataRemover.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlPropertyPathTransformer.cs" />
<Compile Include="XamlIl\CompilerExtensions\Transformers\AvaloniaXamlIlRootObjectScopeTransformer.cs" />

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -57,7 +57,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AddNameScopeRegistration(),
new AvaloniaXamlIlDataContextTypeTransformer(),
new AvaloniaXamlIlBindingPathTransformer(),
new AvaloniaXamlIlNestedScopeMetadataRemover()
new AvaloniaXamlIlCompiledBindingsMetadataRemover()
);
Transformers.Add(new AvaloniaXamlIlMetadataRemover());

5
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlNestedScopeMetadataRemover.cs → src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs

@ -4,13 +4,16 @@ using XamlIl.Transform;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
class AvaloniaXamlIlNestedScopeMetadataRemover : IXamlIlAstTransformer
class AvaloniaXamlIlCompiledBindingsMetadataRemover : IXamlIlAstTransformer
{
public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node)
{
if (node is NestedScopeMetadataNode nestedScope)
return nestedScope.Value;
if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType)
return dataContextType.Value;
return node;
}
}

144
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -18,10 +18,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
private const string AvaloniaNs = "https://github.com/avaloniaui";
public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node)
{
if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlDataContextTypeMetadataNode)
{
// We've already resolved the data context type for this node.
return node;
}
if (node is XamlIlAstObjectNode on)
{
AvaloniaXamlIlDataContextTypeMetadataNode calculatedDataContextTypeNode = null;
AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null;
AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null;
bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate);
for (int i = 0; i < on.Children.Count; ++i)
{
@ -45,46 +52,123 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
}
else if (child is XamlIlAstXamlPropertyValueNode pv
&& pv.Property is XamlIlAstNamePropertyReference pref
&& pref.Name == "DataContext"
&& pref.DeclaringType is XamlIlAstXmlTypeReference tref
&& tref.Name == "StyledElement"
&& tref.XmlNamespace == AvaloniaNs)
else if (child is XamlIlPropertyAssignmentNode pa)
{
var bindingType = context.GetAvaloniaTypes().IBinding;
if (!pv.Values[0].Type.GetClrType().GetAllInterfaces().Contains(bindingType))
if (pa.Property.Name == "DataContext"
&& pa.Property.DeclaringType.Equals(context.GetAvaloniaTypes().StyledElement)
&& pa.Values[0] is XamlIlMarkupExtensionNode ext
&& ext.Value is XamlIlAstObjectNode obj)
{
calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, pv.Values[0].Type.GetClrType());
inferredDataContextTypeNode = ParseDataContext(context, on, obj);
}
else if(pv.Values[0].Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
&& pv.Values[0] is XamlIlAstObjectNode binding)
else if(isDataTemplate
&& pa.Property.Name == "DataType"
&& pa.Values[0] is XamlIlTypeExtensionNode dataTypeNode)
{
IXamlIlType startType;
var parentDataContextNode = context.ParentNodes().OfType<AvaloniaXamlIlDataContextTypeMetadataNode>().FirstOrDefault();
if (parentDataContextNode is null)
{
throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding);
}
startType = parentDataContextNode.DataContextType;
inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataTypeNode.Value.GetClrType());
}
}
}
var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType);
calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType);
// If there is no x:DataContextType directive,
// do more specialized inference
if (directiveDataContextTypeNode is null)
{
if (isDataTemplate && inferredDataContextTypeNode is null)
{
// Infer data type from collection binding on a control that displays items.
var parentObject = context.ParentNodes().OfType<XamlIlAstObjectNode>().FirstOrDefault();
if (parentObject != null && context.GetAvaloniaTypes().IItemsPresenterHost.IsDirectlyAssignableFrom(parentObject.Type.GetClrType()))
{
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject);
}
else
{
inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
}
}
}
return directiveDataContextTypeNode ?? calculatedDataContextTypeNode ?? node;
return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node;
}
// TODO: Add node for DataTemplate scope.
return node;
}
private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(XamlIlAstTransformationContext context, XamlIlAstObjectNode on, XamlIlAstObjectNode parentObject)
{
var parentItemsValue = parentObject
.Children.OfType<XamlIlPropertyAssignmentNode>()
.FirstOrDefault(pa => pa.Property.Name == "Items")
?.Values[0];
if (parentItemsValue is null)
{
// We can't infer the collection type and the currently calculated type is definitely wrong.
// Notify the user that we were unable to infer the data context type if they use a compiled binding.
return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
}
IXamlIlType itemsCollectionType = null;
if (context.GetAvaloniaTypes().IBinding.IsAssignableFrom(parentItemsValue.Type.GetClrType()))
{
if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
&& parentItemsValue is XamlIlMarkupExtensionNode ext && ext.Value is XamlIlAstObjectNode parentItemsBinding)
{
var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType<AvaloniaXamlIlDataContextTypeMetadataNode>().FirstOrDefault();
if (parentItemsDataContext != null)
{
itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, parentItemsDataContext.DataContextType);
}
}
}
else
{
itemsCollectionType = parentItemsValue.Type.GetClrType();
}
if (itemsCollectionType != null)
{
var elementType = itemsCollectionType
.GetAllInterfaces()
.FirstOrDefault(i =>
i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true)
.GenericArguments[0];
return new AvaloniaXamlIlDataContextTypeMetadataNode(on, elementType);
}
// We can't infer the collection type and the currently calculated type is definitely wrong.
// Notify the user that we were unable to infer the data context type if they use a compiled binding.
return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
}
private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(XamlIlAstTransformationContext context, XamlIlAstObjectNode on, XamlIlAstObjectNode obj)
{
var bindingType = context.GetAvaloniaTypes().IBinding;
if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()))
{
return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType());
}
else if (obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension))
{
IXamlIlType startType;
var parentDataContextNode = context.ParentNodes().OfType<AvaloniaXamlIlDataContextTypeMetadataNode>().FirstOrDefault();
if (parentDataContextNode is null)
{
throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", obj);
}
startType = parentDataContextNode.DataContextType;
var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startType);
return new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType);
}
return null;
}
}
class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase
{
public IXamlIlType DataContextType { get; set; }
public virtual IXamlIlType DataContextType { get; }
public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlType targetType)
: base(value, value)
@ -92,4 +176,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
DataContextType = targetType;
}
}
class AvaloniaXamlIlUninferrableDataContextMetadataNode : AvaloniaXamlIlDataContextTypeMetadataNode
{
public AvaloniaXamlIlUninferrableDataContextMetadataNode(IXamlIlAstValueNode value)
: base(value, null)
{
}
public override IXamlIlType DataContextType => throw new XamlIlTransformException("Unable to infer DataContext type for compiled bindings nested within this element.", Value);
}
}

3
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs

@ -11,9 +11,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (node is AvaloniaXamlIlTargetTypeMetadataNode targetType)
return targetType.Value;
if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType)
return dataContextType.Value;
return node;
}
}

4
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -35,6 +35,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlIlType CompiledBindingPathBuilder { get; }
public IXamlIlType CompiledBindingPath { get; }
public IXamlIlType CompiledBindingExtension { get; }
public IXamlIlType DataTemplate { get; }
public IXamlIlType IItemsPresenterHost { get; }
public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg)
{
@ -85,6 +87,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath");
CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension");
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost");
}
}

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

@ -5,9 +5,11 @@ using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Data.Core;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using XamlIl;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
@ -189,6 +191,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void InfersCompiledBindingDataContextFromDataContextBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<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:DataContextType='local:TestDataContext'>
<TextBlock DataContext='{CompiledBinding StringProperty}' Text='{CompiledBinding}' Name='textBlock' />
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.ApplyTemplate();
var dataContext = new TestDataContext
{
StringProperty = "A"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void ResolvesNonIntegerIndexerBindingCorrectly()
{
@ -218,6 +249,123 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text);
}
}
[Fact]
public void InfersDataTemplateTypeFromDataTypeProperty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<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:DataContextType='local:TestDataContext'>
<Window.DataTemplates>
<DataTemplate DataType='{x:Type x:String}'>
<TextBlock Text='{CompiledBinding}' Name='textBlock' />
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='{CompiledBinding StringProperty}' />
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
var dataContext = new TestDataContext();
dataContext.StringProperty = "Initial Value";
window.DataContext = dataContext;
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Equal(dataContext.StringProperty, ((TextBlock)target.Presenter.Child).Text);
}
}
[Fact]
public void ThrowsOnUninferrableLooseDataTemplateNoDataTypeWithCompiledBindingPath()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<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:DataContextType='local:TestDataContext'>
<Window.DataTemplates>
<DataTemplate>
<TextBlock Text='{CompiledBinding StringProperty}' Name='textBlock' />
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='{CompiledBinding}' />
</Window>";
var loader = new AvaloniaXamlLoader();
Assert.Throws<XamlIlTransformException>(() => loader.Load(xaml));
}
}
[Fact]
public void InfersDataTemplateTypeFromParentCollectionItemsType()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<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:DataContextType='local:TestDataContext'>
<ItemsControl Items='{CompiledBinding ListProperty}' Name='target'>
<ItemsControl.DataTemplates>
<DataTemplate>
<TextBlock Text='{CompiledBinding}' Name='textBlock' />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var target = window.FindControl<ItemsControl>("target");
var dataContext = new TestDataContext();
dataContext.ListProperty.Add("Test");
window.DataContext = dataContext;
window.ApplyTemplate();
target.ApplyTemplate();
target.Presenter.ApplyTemplate();
Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content);
}
}
[Fact]
public void ThrowsOnUninferrableDataTemplateInItemsControlWithoutItemsBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<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:DataContextType='local:TestDataContext'>
<ItemsControl Name='target'>
<ItemsControl.DataTemplates>
<DataTemplate>
<TextBlock Text='{CompiledBinding Property}' Name='textBlock' />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Window>";
var loader = new AvaloniaXamlLoader();
Assert.Throws<XamlIlTransformException>(() => loader.Load(xaml));
}
}
}
public class TestDataContext

Loading…
Cancel
Save