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