Explorar o código

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

Jeremy Koritzinsky %!s(int64=6) %!d(string=hai) anos
pai
achega
777693cee2

+ 1 - 1
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" />

+ 1 - 1
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());

+ 4 - 1
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;
         }
     }

+ 119 - 25
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);
+    }
 }

+ 0 - 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 - 0
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 - 0
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