diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index dc26ba4b31..09e6b27967 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -51,7 +51,7 @@
-
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 967b90edf7..710faa28f4 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -57,7 +57,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AddNameScopeRegistration(),
new AvaloniaXamlIlDataContextTypeTransformer(),
new AvaloniaXamlIlBindingPathTransformer(),
- new AvaloniaXamlIlNestedScopeMetadataRemover()
+ new AvaloniaXamlIlCompiledBindingsMetadataRemover()
);
Transformers.Add(new AvaloniaXamlIlMetadataRemover());
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlNestedScopeMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs
similarity index 65%
rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlNestedScopeMetadataRemover.cs
rename to src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs
index 6ee1d4959d..523079d2c9 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlNestedScopeMetadataRemover.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs
@@ -4,13 +4,16 @@ using XamlIl.Transform;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
- class AvaloniaXamlIlNestedScopeMetadataRemover : IXamlIlAstTransformer
+ class AvaloniaXamlIlCompiledBindingsMetadataRemover : IXamlIlAstTransformer
{
public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node)
{
if (node is NestedScopeMetadataNode nestedScope)
return nestedScope.Value;
+ if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType)
+ return dataContextType.Value;
+
return node;
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
index 413c84b3c5..3e4a2f7f94 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs
@@ -18,10 +18,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
private const string AvaloniaNs = "https://github.com/avaloniaui";
public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node)
{
+ if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlDataContextTypeMetadataNode)
+ {
+ // We've already resolved the data context type for this node.
+ return node;
+ }
+
if (node is XamlIlAstObjectNode on)
{
- AvaloniaXamlIlDataContextTypeMetadataNode calculatedDataContextTypeNode = null;
+ AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null;
AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null;
+ bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate);
for (int i = 0; i < on.Children.Count; ++i)
{
@@ -45,46 +52,123 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
}
}
- else if (child is XamlIlAstXamlPropertyValueNode pv
- && pv.Property is XamlIlAstNamePropertyReference pref
- && pref.Name == "DataContext"
- && pref.DeclaringType is XamlIlAstXmlTypeReference tref
- && tref.Name == "StyledElement"
- && tref.XmlNamespace == AvaloniaNs)
+ else if (child is XamlIlPropertyAssignmentNode pa)
{
- var bindingType = context.GetAvaloniaTypes().IBinding;
- if (!pv.Values[0].Type.GetClrType().GetAllInterfaces().Contains(bindingType))
+ if (pa.Property.Name == "DataContext"
+ && pa.Property.DeclaringType.Equals(context.GetAvaloniaTypes().StyledElement)
+ && pa.Values[0] is XamlIlMarkupExtensionNode ext
+ && ext.Value is XamlIlAstObjectNode obj)
{
- calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, pv.Values[0].Type.GetClrType());
+ inferredDataContextTypeNode = ParseDataContext(context, on, obj);
}
- else if(pv.Values[0].Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
- && pv.Values[0] is XamlIlAstObjectNode binding)
+ else if(isDataTemplate
+ && pa.Property.Name == "DataType"
+ && pa.Values[0] is XamlIlTypeExtensionNode dataTypeNode)
{
- IXamlIlType startType;
- var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault();
- if (parentDataContextNode is null)
- {
- throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding);
- }
-
- startType = parentDataContextNode.DataContextType;
+ inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataTypeNode.Value.GetClrType());
+ }
+ }
+ }
- var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType);
- calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType);
+ // If there is no x:DataContextType directive,
+ // do more specialized inference
+ if (directiveDataContextTypeNode is null)
+ {
+ if (isDataTemplate && inferredDataContextTypeNode is null)
+ {
+ // Infer data type from collection binding on a control that displays items.
+ var parentObject = context.ParentNodes().OfType().FirstOrDefault();
+ if (parentObject != null && context.GetAvaloniaTypes().IItemsPresenterHost.IsDirectlyAssignableFrom(parentObject.Type.GetClrType()))
+ {
+ inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject);
+ }
+ else
+ {
+ inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on);
}
}
}
- return directiveDataContextTypeNode ?? calculatedDataContextTypeNode ?? node;
+
+ return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node;
}
- // TODO: Add node for DataTemplate scope.
return node;
}
+
+ private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(XamlIlAstTransformationContext context, XamlIlAstObjectNode on, XamlIlAstObjectNode parentObject)
+ {
+ var parentItemsValue = parentObject
+ .Children.OfType()
+ .FirstOrDefault(pa => pa.Property.Name == "Items")
+ ?.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);
+ }
+
+ IXamlIlType itemsCollectionType = null;
+ if (context.GetAvaloniaTypes().IBinding.IsAssignableFrom(parentItemsValue.Type.GetClrType()))
+ {
+ if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)
+ && parentItemsValue is XamlIlMarkupExtensionNode ext && ext.Value is XamlIlAstObjectNode parentItemsBinding)
+ {
+ var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType().FirstOrDefault();
+ if (parentItemsDataContext != null)
+ {
+ itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, parentItemsDataContext.DataContextType);
+ }
+ }
+ }
+ else
+ {
+ itemsCollectionType = parentItemsValue.Type.GetClrType();
+ }
+
+ if (itemsCollectionType != null)
+ {
+ var elementType = itemsCollectionType
+ .GetAllInterfaces()
+ .FirstOrDefault(i =>
+ i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true)
+ .GenericArguments[0];
+ return new AvaloniaXamlIlDataContextTypeMetadataNode(on, elementType);
+ }
+ // 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);
+ }
+
+ private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(XamlIlAstTransformationContext context, XamlIlAstObjectNode on, XamlIlAstObjectNode obj)
+ {
+ var bindingType = context.GetAvaloniaTypes().IBinding;
+ if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()))
+ {
+ return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType());
+ }
+ else if (obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension))
+ {
+ IXamlIlType startType;
+ var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault();
+ if (parentDataContextNode is null)
+ {
+ throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", obj);
+ }
+
+ startType = parentDataContextNode.DataContextType;
+
+ var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startType);
+ return new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType);
+ }
+
+ return null;
+ }
}
class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase
{
- public IXamlIlType DataContextType { get; set; }
+ public virtual IXamlIlType DataContextType { get; }
public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlType targetType)
: base(value, value)
@@ -92,4 +176,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
DataContextType = targetType;
}
}
+
+ class AvaloniaXamlIlUninferrableDataContextMetadataNode : AvaloniaXamlIlDataContextTypeMetadataNode
+ {
+ public AvaloniaXamlIlUninferrableDataContextMetadataNode(IXamlIlAstValueNode value)
+ : base(value, null)
+ {
+ }
+
+ public override IXamlIlType DataContextType => throw new XamlIlTransformException("Unable to infer DataContext type for compiled bindings nested within this element.", Value);
+ }
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs
index ba3c3618b2..b76de82efb 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs
@@ -11,9 +11,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
if (node is AvaloniaXamlIlTargetTypeMetadataNode targetType)
return targetType.Value;
- if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType)
- return dataContextType.Value;
-
return node;
}
}
diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index d29e99f2c3..21d3b38d79 100644
--- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -35,6 +35,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlIlType CompiledBindingPathBuilder { get; }
public IXamlIlType CompiledBindingPath { get; }
public IXamlIlType CompiledBindingExtension { get; }
+ public IXamlIlType DataTemplate { get; }
+ public IXamlIlType IItemsPresenterHost { get; }
public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg)
{
@@ -85,6 +87,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder");
CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath");
CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension");
+ DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
+ IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost");
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
index 996b2b7ee9..ca1979659f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
@@ -5,9 +5,11 @@ using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
using Avalonia.Data.Core;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
+using XamlIl;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
@@ -189,6 +191,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
+ [Fact]
+ public void InfersCompiledBindingDataContextFromDataContextBinding()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ window.ApplyTemplate();
+
+ var dataContext = new TestDataContext
+ {
+ StringProperty = "A"
+ };
+
+ window.DataContext = dataContext;
+
+ Assert.Equal(dataContext.StringProperty, textBlock.Text);
+ }
+ }
+
[Fact]
public void ResolvesNonIntegerIndexerBindingCorrectly()
{
@@ -218,6 +249,123 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text);
}
}
+
+ [Fact]
+ public void InfersDataTemplateTypeFromDataTypeProperty()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var target = window.FindControl("target");
+
+ var dataContext = new TestDataContext();
+
+ dataContext.StringProperty = "Initial Value";
+
+ window.DataContext = dataContext;
+
+ window.ApplyTemplate();
+ target.ApplyTemplate();
+ ((ContentPresenter)target.Presenter).UpdateChild();
+
+ Assert.Equal(dataContext.StringProperty, ((TextBlock)target.Presenter.Child).Text);
+ }
+ }
+
+
+ [Fact]
+ public void ThrowsOnUninferrableLooseDataTemplateNoDataTypeWithCompiledBindingPath()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ Assert.Throws(() => loader.Load(xaml));
+ }
+ }
+
+ [Fact]
+ public void InfersDataTemplateTypeFromParentCollectionItemsType()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ var window = (Window)loader.Load(xaml);
+ var target = window.FindControl("target");
+
+ var dataContext = new TestDataContext();
+
+ dataContext.ListProperty.Add("Test");
+
+ window.DataContext = dataContext;
+
+ window.ApplyTemplate();
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content);
+ }
+ }
+
+ [Fact]
+ public void ThrowsOnUninferrableDataTemplateInItemsControlWithoutItemsBinding()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+";
+ var loader = new AvaloniaXamlLoader();
+ Assert.Throws(() => loader.Load(xaml));
+ }
+ }
}
public class TestDataContext