From d8d2240ecbd8204502c483054e5d1e4f47ae8164 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 29 Jan 2023 03:04:51 -0500 Subject: [PATCH] Implement support for DataTypeInheritFromAttribute --- .../Metadata/DataTypeInheritFromAttribute.cs | 34 +++++ .../DataGridBoundColumn.cs | 2 + .../DataGridTemplateColumn.cs | 2 + src/Avalonia.Controls/ItemsControl.cs | 1 + .../Repeater/ItemsRepeater.cs | 2 + ...valoniaXamlIlDataContextTypeTransformer.cs | 60 +++++---- .../AvaloniaXamlIlWellKnownTypes.cs | 2 + .../XamlIlBindingPathHelper.cs | 4 + .../CompiledBindingExtensionTests.cs | 119 ++++++++++++++++++ 9 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs diff --git a/src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs new file mode 100644 index 0000000000..6bd967769a --- /dev/null +++ b/src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Hints the compiler how to resolve the compiled bindings data type for the collection-like controls' item specific properties. +/// +/// +/// 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. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class DataTypeInheritFromAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Defines property name which items' type should used on the target property + public DataTypeInheritFromAttribute(string ancestorProperty) + { + AncestorProperty = ancestorProperty; + } + + /// + /// Defines property name which items' type should used on the target property. + /// + public string AncestorProperty { get; } + + /// + /// Defines ancestor type which should be used in a lookup for . + /// If null, declaring type of the target property is used. + /// + public Type? AncestorType { get; set; } +} diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 8f532b9803..2365e0ab5b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/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 /// //TODO Binding [AssignBinding] + [DataTypeInheritFrom(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] public virtual IBinding Binding { get diff --git a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs index 516e9cf6c2..6cf4c881b3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs +++ b/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 /// /// If this property is the column is read-only. /// + [DataTypeInheritFrom(nameof(DataGrid.Items), AncestorType = typeof(DataGrid))] public IDataTemplate CellEditingTemplate { get => _cellEditingCellTemplate; diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index db49da85e8..5ea3fbb98e 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -168,6 +168,7 @@ namespace Avalonia.Controls /// /// Gets or sets the data template used to display the items in the control. /// + [DataTypeInheritFrom(nameof(Items))] public IDataTemplate? ItemTemplate { get { return GetValue(ItemTemplateProperty); } diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 4de6a5188d..bfd667d530 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/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 /// /// Gets or sets the template used to display each item. /// + [DataTypeInheritFrom(nameof(Items))] public IDataTemplate? ItemTemplate { get => GetValue(ItemTemplateProperty); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 574d46e737..18af6d5a39 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/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().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().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() + .FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName); + } + else + { + parentObject = context.ParentNodes().OfType().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() - .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); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 0b61316603..6b36343852 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/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"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index ae29dcf9cb..fb825cf636 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/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() diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 16b8bb3f91..ba4b083e0d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/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(@" + + + + + + + + + + + + +"); + var target = window.FindControl("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(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(@" + + + + + + + + + + + + +"); + var target = window.FindControl("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(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 ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(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 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; } + } }