Browse Source

Implement support for DataTypeInheritFromAttribute

Max Katz 2 years ago
parent
commit
d8d2240ecb

+ 34 - 0
src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs

@@ -0,0 +1,34 @@
+using System;
+
+namespace Avalonia.Metadata;
+
+/// <summary>
+/// Hints the compiler how to resolve the compiled bindings data type for the collection-like controls' item specific properties.  
+/// </summary>
+/// <remarks>
+/// Typical example usage is a ListBox control, where DataTypeInheritFrom is defined on the ItemTemplate property,
+/// so template can try to inherit data type from the Items collection binding. 
+/// </remarks>
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
+public sealed class DataTypeInheritFromAttribute : Attribute
+{
+    /// <summary>
+    /// Initializes a new instance of the <see cref="DataTypeInheritFromAttribute"/> class.
+    /// </summary>
+    /// <param name="ancestorProperty">Defines property name which items' type should used on the target property</param>
+    public DataTypeInheritFromAttribute(string ancestorProperty)
+    {
+        AncestorProperty = ancestorProperty;
+    }
+    
+    /// <summary>
+    /// Defines property name which items' type should used on the target property.
+    /// </summary>
+    public string AncestorProperty { get; }
+    
+    /// <summary>
+    /// Defines ancestor type which should be used in a lookup for <see cref="AncestorProperty"/>.
+    /// If null, declaring type of the target property is used.
+    /// </summary>
+    public Type? AncestorType { get; set; }
+}

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -7,6 +7,7 @@ using Avalonia.Data;
 using System;
 using Avalonia.Controls.Utils;
 using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Metadata;
 using Avalonia.Reactive;
 
 namespace Avalonia.Controls
@@ -24,6 +25,7 @@ namespace Avalonia.Controls
         /// </summary>
         //TODO Binding
         [AssignBinding]
+        [DataTypeInheritFrom(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
         public virtual IBinding Binding
         {
             get

+ 2 - 0
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@@ -24,6 +24,7 @@ namespace Avalonia.Controls
                 (o, v) => o.CellTemplate = v);
 
         [Content]
+        [DataTypeInheritFrom(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
         public IDataTemplate CellTemplate
         {
             get { return _cellTemplate; }
@@ -50,6 +51,7 @@ namespace Avalonia.Controls
         /// <remarks>
         /// If this property is <see langword="null"/> the column is read-only.
         /// </remarks>
+        [DataTypeInheritFrom(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))]
         public IDataTemplate CellEditingTemplate
         {
             get => _cellEditingCellTemplate;

+ 1 - 0
src/Avalonia.Controls/ItemsControl.cs

@@ -168,6 +168,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the data template used to display the items in the control.
         /// </summary>
+        [DataTypeInheritFrom(nameof(Items))]
         public IDataTemplate? ItemTemplate
         {
             get { return GetValue(ItemTemplateProperty); }

+ 2 - 0
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -11,6 +11,7 @@ using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Logging;
 using Avalonia.LogicalTree;
+using Avalonia.Metadata;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
@@ -121,6 +122,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the template used to display each item.
         /// </summary>
+        [DataTypeInheritFrom(nameof(Items))]
         public IDataTemplate? ItemTemplate
         {
             get => GetValue(ItemTemplateProperty);

+ 37 - 23
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@@ -68,26 +68,41 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
 
                 // If there is no x:DataType directive,
                 // do more specialized inference
-                if (directiveDataContextTypeNode is null)
+                if (directiveDataContextTypeNode is null && inferredDataContextTypeNode is null)
                 {
-                    if (context.GetAvaloniaTypes().IDataTemplate.IsAssignableFrom(on.Type.GetClrType())
-                        && inferredDataContextTypeNode is null)
+                    // Infer data type from collection binding on a control that displays items.
+                    var property = context.ParentNodes().OfType<XamlPropertyAssignmentNode>().FirstOrDefault();
+                    var attributeType = context.GetAvaloniaTypes().DataTypeInheritFromAttribute;
+                    var attribute = property?.Property?.GetClrProperty().CustomAttributes
+                        .FirstOrDefault(a => a.Type == attributeType);
+    
+                    if (attribute is not null)
                     {
-                        // Infer data type from collection binding on a control that displays items.
-                        var parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();
+                        var propertyName = (string)attribute.Parameters.First();
+                        XamlAstConstructableObjectNode parentObject;
+                        if (attribute.Properties.TryGetValue("AncestorType", out var type)
+                            && type is IXamlType xamlType)
+                        {
+                            parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>()
+                                .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName);
+                        }
+                        else
+                        {
+                            parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();
+                        }
+                            
                         if (parentObject != null)
                         {
-                            var parentType = parentObject.Type.GetClrType();
-
-                            if (context.GetAvaloniaTypes().ItemsControl.IsDirectlyAssignableFrom(parentType)
-                                || context.GetAvaloniaTypes().ItemsRepeater.IsDirectlyAssignableFrom(parentType))
-                            {
-                                inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject);
-                            }
+                            inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName);
                         }
 
-                        if (inferredDataContextTypeNode is null)
+                        if (inferredDataContextTypeNode is null
+                            // Only for IDataTemplate, as we want to notify user as early as possible,
+                            // and IDataTemplate cannot inherit DataType from the parent implicitly.
+                            && context.GetAvaloniaTypes().IDataTemplate.IsAssignableFrom(on.Type.GetClrType()))
                         {
+                            // 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.
                             inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
                         }
                     }
@@ -98,18 +113,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
 
             return node;
         }
-
-        private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode parentObject)
+        
+        private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(
+            AstTransformationContext context, XamlAstConstructableObjectNode on,
+            XamlAstConstructableObjectNode parentObject, string propertyName)
         {
             var parentItemsValue = parentObject
                                             .Children.OfType<XamlPropertyAssignmentNode>()
-                                            .FirstOrDefault(pa => pa.Property.Name == "Items")
+                                            .FirstOrDefault(pa => pa.Property.Name == propertyName)
                                             ?.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);
+                return null;
             }
 
             IXamlType itemsCollectionType = null;
@@ -140,9 +155,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                     }
                 }
             }
-            // 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);
+            
+            return null;
         }
 
         private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode obj)
@@ -208,6 +222,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         {
         }
 
-        public override IXamlType DataContextType => throw new XamlTransformException("Unable to infer DataContext type for compiled bindings nested within this element.", Value);
+        public override IXamlType DataContextType => throw new XamlTransformException("Unable to infer DataContext type for compiled bindings nested within this element. Please set x:DataType on the Binding or parent.", Value);
     }
 }

+ 2 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -30,6 +30,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         public IXamlType AssignBindingAttribute { get; }
         public IXamlType DependsOnAttribute { get; }
         public IXamlType DataTypeAttribute { get; }
+        public IXamlType DataTypeInheritFromAttribute { get; }
         public IXamlType MarkupExtensionOptionAttribute { get; }
         public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
         public IXamlType OnExtensionType { get; }
@@ -135,6 +136,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute");
             DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute");
             DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute");
+            DataTypeInheritFromAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeInheritFromAttribute");
             MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
             MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
             OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");

+ 4 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@@ -37,6 +37,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 bindingResultType = transformed.BindingResultType;
                 binding.Arguments[0] = transformed;
             }
+            if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlIlBindingPathNode alreadyTransformed)
+            {
+                bindingResultType = alreadyTransformed.BindingResultType;
+            }
             else
             {
                 var bindingPathAssignment = binding.Children.OfType<XamlPropertyAssignmentNode>()

+ 119 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.ComponentModel;
@@ -7,6 +8,7 @@ using System.Linq;
 using System.Reactive.Subjects;
 using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
+using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
@@ -550,6 +552,98 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
                 Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content);
             }
         }
+        
+        [Fact]
+        public void InfersDataTemplateTypeFromParentDataGridItemsType()
+        {
+            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'>
+    <local:DataGridLikeControl Items='{CompiledBinding ListProperty}' Name='target'>
+        <local:DataGridLikeControl.Columns>
+            <local:DataGridLikeColumn Binding='{CompiledBinding Length}'>
+                <local:DataGridLikeColumn.Template>
+                    <DataTemplate>
+                        <TextBlock Text='{CompiledBinding Length}' />
+                    </DataTemplate>
+                </local:DataGridLikeColumn.Template>
+            </local:DataGridLikeColumn>
+        </local:DataGridLikeControl.Columns>
+    </local:DataGridLikeControl>
+</Window>");
+                var target = window.FindControl<DataGridLikeControl>("target");
+                var column = target!.Columns.Single();
+
+                var dataContext = new TestDataContext();
+
+                dataContext.ListProperty.Add("Test");
+
+                window.DataContext = dataContext;
+
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+
+                // Assert DataGridLikeColumn.Binding data type.
+                var compiledPath = ((CompiledBindingExtension)column.Binding).Path;
+                var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
+                Assert.Equal(typeof(int), node.Property.PropertyType);
+                
+                // Assert DataGridLikeColumn.Template data type by evaluating the template.
+                var firstItem = dataContext.ListProperty[0];
+                var textBlockFromTemplate = (TextBlock)column.Template.Build(firstItem);
+                textBlockFromTemplate.DataContext = firstItem;
+                Assert.Equal(firstItem.Length.ToString(), textBlockFromTemplate.Text);
+            }
+        }
+        
+        [Fact]
+        public void ExplicitDataTypeStillWorksOnDataGridLikeControls()
+        {
+            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'>
+    <local:DataGridLikeControl Name='target'>
+        <local:DataGridLikeControl.Columns>
+            <local:DataGridLikeColumn Binding='{CompiledBinding Length}' x:DataType='x:String'>
+                <local:DataGridLikeColumn.Template>
+                    <DataTemplate x:DataType='x:String'>
+                        <TextBlock Text='{CompiledBinding Length}' />
+                    </DataTemplate>
+                </local:DataGridLikeColumn.Template>
+            </local:DataGridLikeColumn>
+        </local:DataGridLikeControl.Columns>
+    </local:DataGridLikeControl>
+</Window>");
+                var target = window.FindControl<DataGridLikeControl>("target");
+                var column = target!.Columns.Single();
+
+                var dataContext = new TestDataContext();
+                dataContext.ListProperty.Add("Test");
+                target.Items = dataContext.ListProperty;
+
+                window.ApplyTemplate();
+                target.ApplyTemplate();
+
+                // Assert DataGridLikeColumn.Binding data type.
+                var compiledPath = ((CompiledBindingExtension)column.Binding).Path;
+                var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
+                Assert.Equal(typeof(int), node.Property.PropertyType);
+                
+                // Assert DataGridLikeColumn.Template data type by evaluating the template.
+                var firstItem = dataContext.ListProperty[0];
+                var textBlockFromTemplate = (TextBlock)column.Template.Build(firstItem);
+                textBlockFromTemplate.DataContext = firstItem;
+                Assert.Equal(firstItem.Length.ToString(), textBlockFromTemplate.Text);
+            }
+        }
 
         [Fact]
         public void ThrowsOnUninferrableDataTemplateInItemsControlWithoutItemsBinding()
@@ -1835,4 +1929,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
     {
         [AssignBinding] public IBinding X { get; set; }
     }
+
+    public class DataGridLikeControl : Control
+    {
+        public static readonly DirectProperty<DataGridLikeControl, IEnumerable?> ItemsProperty =
+            ItemsControl.ItemsProperty.AddOwner<DataGridLikeControl>(o => o.Items, (o, v) => o.Items = v);
+
+        private IEnumerable _items;
+        public IEnumerable Items
+        {
+            get { return _items; }
+            set { SetAndRaise(ItemsProperty, ref _items, value); }
+        }
+
+        public AvaloniaList<DataGridLikeColumn> Columns { get; } = new();
+    }
+
+    public class DataGridLikeColumn
+    {
+        [AssignBinding]
+        [DataTypeInheritFrom(nameof(DataGridLikeControl.Items), AncestorType = typeof(DataGridLikeControl))]
+        public IBinding Binding { get; set; }
+        
+        [DataTypeInheritFrom(nameof(DataGridLikeControl.Items), AncestorType = typeof(DataGridLikeControl))]
+        public IDataTemplate Template { get; set; }
+    }
 }