Browse Source

Merge pull request #10121 from AvaloniaUI/generic-datacontext-resolver

Implement support for InheritDataTypeFromItemsAttribute
pull/10127/head
Max Katz 4 years ago
committed by GitHub
parent
commit
ee3e7a83b3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      samples/ControlCatalog/Pages/DataGridPage.xaml
  2. 13
      samples/ControlCatalog/Pages/DataGridPage.xaml.cs
  3. 34
      src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs
  4. 2
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  5. 2
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  6. 1
      src/Avalonia.Controls/ItemsControl.cs
  7. 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  8. 66
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
  9. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  10. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  11. 119
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

22
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -1,7 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:local="using:ControlCatalog.Models"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.DataGridPage">
xmlns:pages="clr-namespace:ControlCatalog.Pages"
x:Class="ControlCatalog.Pages.DataGridPage"
x:DataType="pages:DataGridPage">
<UserControl.Resources>
<local:GDPValueConverter x:Key="GDPConverter" />
<DataTemplate x:Key="Demo.DataTemplates.CountryHeader" x:DataType="local:Country">
@ -33,7 +35,7 @@
<DataGrid.Columns>
<!-- Using HeaderTemplate -->
<DataGridTextColumn Header="Country" HeaderTemplate="{StaticResource Demo.DataTemplates.CountryHeader}" Binding="{Binding Name}" Width="6*" x:DataType="local:Country" />
<DataGridTextColumn Header="Region" Binding="{CompiledBinding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Region" Binding="{Binding Region}" Width="4*" x:DataType="local:Country" />
<DataGridTextColumn Header="Population" Binding="{Binding Population}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="Area" Binding="{Binding Area}" Width="3*" x:DataType="local:Country" />
<DataGridTextColumn Header="GDP" Binding="{Binding GDP}" Width="3*"
@ -90,19 +92,21 @@
</TabItem>
<TabItem x:Name="EditableTab" Header="Editable">
<Grid RowDefinitions="*,Auto">
<DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0">
<!-- Example of columns inheriting the data type from the Items source -->
<DataGrid Name="dataGridEdit" Margin="12" Grid.Row="0"
Items="{Binding DataGrid3Source}">
<DataGrid.Columns>
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" x:DataType="local:Person" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" x:DataType="local:Person" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" x:DataType="local:Person" />
<DataGridTemplateColumn Header="Age" >
<DataGridTextColumn Header="First Name" Binding="{Binding FirstName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridTextColumn Header="Last" Binding="{Binding LastName}" Width="2*" FontSize="{Binding #FontSizeSlider.Value, Mode=OneWay}" />
<DataGridCheckBoxColumn Header="Is Banned" Binding="{Binding IsBanned}" Width="*" IsThreeState="{Binding #IsThreeStateCheckBox.IsChecked, Mode=OneWay}" />
<DataGridTemplateColumn Header="Age">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="local:Person">
<DataTemplate>
<TextBlock Text="{Binding Age, StringFormat='{}{0} years'}" VerticalAlignment="Center" HorizontalAlignment="Center" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
<DataGridTemplateColumn.CellEditingTemplate>
<DataTemplate DataType="local:Person">
<DataTemplate>
<NumericUpDown Value="{Binding Age}" FormatString="N0" HorizontalAlignment="Stretch" Minimum="0" Maximum="120" TemplateApplied="NumericUpDown_OnTemplateApplied" />
</DataTemplate>
</DataGridTemplateColumn.CellEditingTemplate>

13
samples/ControlCatalog/Pages/DataGridPage.xaml.cs

@ -1,5 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
@ -48,20 +49,22 @@ namespace ControlCatalog.Pages
var dg3 = this.Get<DataGrid>("dataGridEdit");
dg3.IsReadOnly = false;
var items = new List<Person>
var list = new ObservableCollection<Person>
{
new Person { FirstName = "John", LastName = "Doe" , Age = 30},
new Person { FirstName = "Elizabeth", LastName = "Thomas", IsBanned = true , Age = 40 },
new Person { FirstName = "Zack", LastName = "Ward" , Age = 50 }
};
var collectionView3 = new DataGridCollectionView(items);
dg3.Items = collectionView3;
DataGrid3Source = list;
var addButton = this.Get<Button>("btnAdd");
addButton.Click += (a, b) => collectionView3.AddNew();
addButton.Click += (a, b) => list.Add(new Person());
DataContext = this;
}
public IEnumerable<Person> DataGrid3Source { get; }
private void Dg1_LoadingRow(object? sender, DataGridRowEventArgs e)
{
e.Row.Header = e.Row.GetIndex() + 1;

34
src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs

@ -0,0 +1,34 @@
using System;
namespace Avalonia.Metadata;
/// <summary>
/// Instructs the compiler to resolve the compiled bindings data type for the item-specific properties of collection-like controls.
/// </summary>
/// <remarks>
/// A typical usage example is a ListBox control, where <see cref="InheritDataTypeFromItemsAttribute"/> is defined on the ItemTemplate property,
/// allowing the template to inherit the data type from the Items collection binding.
/// </remarks>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class InheritDataTypeFromItemsAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="InheritDataTypeFromItemsAttribute"/> class.
/// </summary>
/// <param name="ancestorItemsProperty">The name of the property whose item type should be used on the target property.</param>
public InheritDataTypeFromItemsAttribute(string ancestorItemsProperty)
{
AncestorItemsProperty = ancestorItemsProperty;
}
/// <summary>
/// The name of the property whose item type should be used on the target property.
/// </summary>
public string AncestorItemsProperty { get; }
/// <summary>
/// The ancestor type to be used in a lookup for the <see cref="AncestorProperty"/>.
/// If null, the 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]
[InheritDataTypeFromItems(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]
[InheritDataTypeFromItems(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>
[InheritDataTypeFromItems(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>
[InheritDataTypeFromItems(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>
[InheritDataTypeFromItems(nameof(Items))]
public IDataTemplate? ItemTemplate
{
get => GetValue(ItemTemplateProperty);

66
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs

@ -68,29 +68,44 @@ 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().InheritDataTypeFromItemsAttribute;
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();
if (parentObject != null)
var propertyName = (string)attribute.Parameters.First();
XamlAstConstructableObjectNode parentObject;
if (attribute.Properties.TryGetValue("AncestorType", out var type)
&& type is IXamlType xamlType)
{
var parentType = parentObject.Type.GetClrType();
if (context.GetAvaloniaTypes().ItemsControl.IsDirectlyAssignableFrom(parentType)
|| context.GetAvaloniaTypes().ItemsRepeater.IsDirectlyAssignableFrom(parentType))
{
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject);
}
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>()
.FirstOrDefault(n => n.Type.GetClrType().FullName == xamlType.FullName);
}
if (inferredDataContextTypeNode is null)
else
{
parentObject = context.ParentNodes().OfType<XamlAstConstructableObjectNode>().FirstOrDefault();
}
if (parentObject != null)
{
inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject, propertyName);
}
}
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);
}
}
return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node;
@ -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 InheritDataTypeFromItemsAttribute { 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");
InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute");
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;
}
else 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]
[InheritDataTypeFromItems(nameof(DataGridLikeControl.Items), AncestorType = typeof(DataGridLikeControl))]
public IBinding Binding { get; set; }
[InheritDataTypeFromItems(nameof(DataGridLikeControl.Items), AncestorType = typeof(DataGridLikeControl))]
public IDataTemplate Template { get; set; }
}
}

Loading…
Cancel
Save