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