Browse Source

Implement support for DataTypeInheritFromAttribute

pull/10121/head
Max Katz 3 years ago
parent
commit
d8d2240ecb
  1. 34
      src/Avalonia.Base/Metadata/DataTypeInheritFromAttribute.cs
  2. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  3. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  4. 1
      src/Avalonia.Controls/ItemsControl.cs
  5. 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  6. 60
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  7. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  8. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  9. 119
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

34
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
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
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
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
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);

60
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
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
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
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; }
}
}

Loading…
Cancel
Save