From 0a94431153ad5cdf2671a5aafdccd2e6ea1422e8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 18 Dec 2021 19:55:38 +0300 Subject: [PATCH] Merge pull request #7201 from jkoritzinsky/relativesource-compiledbinding Support RelativeSource-based bindings without requiring an x:DataType property. --- .../AvaloniaXamlIlBindingPathTransformer.cs | 8 +- ...valoniaXamlIlDataContextTypeTransformer.cs | 22 ++-- .../XamlIlBindingPathHelper.cs | 101 ++++++++++-------- .../CompiledBindingExtensionTests.cs | 24 ++++- 4 files changed, 93 insertions(+), 62 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs index 4d8c940bbc..4d21a29eb9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -99,7 +99,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - if (startType == null) + Func startTypeResolver = startType is not null ? () => startType : () => { var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); if (parentDataContextNode is null) @@ -107,10 +107,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); } - startType = parentDataContextNode.DataContextType; - } + return parentDataContextNode.DataContextType; + }; - XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startTypeResolver, context.ParentNodes().OfType().First().Type.GetClrType()); } return node; 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 349143253e..24dd3f7771 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using XamlX; @@ -11,7 +12,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { class AvaloniaXamlIlDataContextTypeTransformer : IXamlAstTransformer { - private const string AvaloniaNs = "https://github.com/avaloniaui"; public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlDataContextTypeMetadataNode) @@ -120,7 +120,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType().FirstOrDefault(); if (parentItemsDataContext != null) { - itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, parentItemsDataContext.DataContextType); + itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, () => parentItemsDataContext.DataContextType, parentObject.Type.GetClrType()); } } } @@ -153,16 +153,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } else if (obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { - IXamlType startType; - var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); - if (parentDataContextNode is null) + Func startTypeResolver = () => { - throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", obj); - } + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", obj); + } - startType = parentDataContextNode.DataContextType; + return parentDataContextNode.DataContextType; + }; - var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startType); + var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startTypeResolver, on.Type.GetClrType()); return new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 1974dfe3bc..7f7f60ed94 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { static class XamlIlBindingPathHelper { - public static IXamlType UpdateCompiledBindingExtension(AstTransformationContext context, XamlAstConstructableObjectNode binding, IXamlType startType) + public static IXamlType UpdateCompiledBindingExtension(AstTransformationContext context, XamlAstConstructableObjectNode binding, Func startTypeResolver, IXamlType selfType) { IXamlType bindingResultType = null; if (binding.Arguments.Count > 0 && binding.Arguments[0] is ParsedBindingPathNode bindingPath) @@ -28,7 +28,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var transformed = TransformBindingPath( context, bindingPath, - startType, + startTypeResolver, + selfType, bindingPath.Path); bindingResultType = transformed.BindingResultType; @@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (bindingPathAssignment is null) { - return startType; + return startTypeResolver(); } if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) @@ -49,7 +50,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var transformed = TransformBindingPath( context, bindingPathNode, - startType, + startTypeResolver, + selfType, bindingPathNode.Path); bindingResultType = transformed.BindingResultType; @@ -64,13 +66,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return bindingResultType; } - private static IXamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, IXamlType startType, IEnumerable bindingExpression) + private static IXamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, Func startTypeResolver, IXamlType selfType, IEnumerable bindingExpression) { List transformNodes = new List(); List nodes = new List(); foreach (var astNode in bindingExpression) { - var targetType = nodes.Count == 0 ? startType : nodes[nodes.Count - 1].Type; + var targetTypeResolver = nodes.Count == 0 ? startTypeResolver : () => nodes[nodes.Count - 1].Type; switch (astNode) { case BindingExpressionGrammar.EmptyExpressionNode _: @@ -79,59 +81,66 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); break; case BindingExpressionGrammar.StreamNode _: - IXamlType observableType; - if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) { - observableType = targetType; - } - else - { - observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); - } + IXamlType targetType = targetTypeResolver(); + IXamlType observableType; + if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) + { + observableType = targetType; + } + else + { + observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + } - if (observableType != null) - { - nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); - break; - } - bool foundTask = false; - for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) - { - if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + if (observableType != null) { - foundTask = true; - nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); break; } + bool foundTask = false; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + { + foundTask = true; + nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + break; + } + } + if (foundTask) + { + break; + } + throw new XamlX.XamlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); } - if (foundTask) - { - break; - } - throw new XamlX.XamlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); case BindingExpressionGrammar.PropertyNameNode propName: - var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; - var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => - f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); - - if (avaloniaPropertyFieldMaybe != null) { - nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, - XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); - } - else - { - var clrProperty = GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName); + IXamlType targetType = targetTypeResolver(); + var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; + var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); - if (clrProperty is null) + if (avaloniaPropertyFieldMaybe != null) + { + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + } + else { - throw new XamlX.XamlParseException($"Unable to resolve property of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); + var clrProperty = GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName); + + if (clrProperty is null) + { + throw new XamlX.XamlParseException($"Unable to resolve property of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); + } + nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); } - nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); + break; } - break; case BindingExpressionGrammar.IndexerNode indexer: { + IXamlType targetType = targetTypeResolver(); if (targetType.IsArray) { nodes.Add(new XamlIlArrayIndexerPathElementNode(targetType, indexer.Arguments, lineInfo)); @@ -183,7 +192,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, context.GetAvaloniaTypes(), lineInfo))); break; case BindingExpressionGrammar.SelfNode _: - nodes.Add(new SelfPathElementNode(targetType)); + nodes.Add(new SelfPathElementNode(selfType)); break; case VisualAncestorBindingExpressionNode visualAncestor: nodes.Add(new FindVisualAncestorPathElementNode(visualAncestor.Type, visualAncestor.Level)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 8a61458030..15c6f5877f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -731,8 +731,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions + Title='foo'> "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); @@ -1042,6 +1041,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binds_To_Self_Without_DataType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.Equal(textBlock.Name, textBlock.Text); + } + } + void Throws(string type, Action cb) { try