From 604a8b635f25add5f9309ca359f09eab6fe6d87a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:12:24 +0200 Subject: [PATCH 01/17] add some tests for cast in path expression --- .../CompiledBindingExtensionTests.cs | 90 ++++++++++++++++++- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 8a82ad048b..c651aabe0a 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -616,23 +616,105 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Throws(() => AvaloniaRuntimeXamlLoader.Load(xaml)); } } + + [Fact] + public void SupportCastToTypeInExpressionWithProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, contentControl.Content); + } + } + + [Fact] + public void SupportCastToTypeInExpressionWithProperty1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, contentControl.Content); + } + } + + [Fact] + public void SupportCastToTypeInExpressionWithProperty_DifferentTypeEvaluatesToNull() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, contentControl.Content); + + window.DataContext = "foo"; + + Assert.Equal(null, contentControl.Content); + } + } } public interface INonIntegerIndexer { - string this[string key] {get; set;} + string this[string key] { get; set; } } public interface INonIntegerIndexerDerived : INonIntegerIndexer - {} + { } public interface IHasProperty { - string StringProperty {get; set; } + string StringProperty { get; set; } } public interface IHasPropertyDerived : IHasProperty - {} + { } public class TestDataContext : IHasPropertyDerived { From 5500c2ed8c989f410986ee556e6b59d7aa3fa9c7 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:14:06 +0200 Subject: [PATCH 02/17] support parse cast in binding path expression --- .../Parsers/BindingExpressionGrammar.cs | 36 +++++++++++++++++++ .../Markup/Parsers/ExpressionParser.cs | 19 ++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 8e5631e198..7df53f430f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -46,6 +46,10 @@ namespace Avalonia.Markup.Parsers state = ParseIndexer(ref r, nodes); break; + case State.TypeCast: + state = ParseTypeCast(ref r, nodes); + break; + case State.ElementName: state = ParseElementName(ref r, nodes); mode = SourceMode.Control; @@ -124,6 +128,10 @@ namespace Avalonia.Markup.Parsers { return State.Indexer; } + else if (ParseOpenBrace(ref r)) + { + return State.TypeCast; + } return State.End; } @@ -186,6 +194,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } + private static State ParseTypeCast(ref CharacterReader r, List nodes) + { + var (ns, typeName) = ParseTypeName(ref r); + + nodes.Add(new TypeCastNode { Namespace = ns, TypeName = typeName }); + + if (ParseMemberAccessor(ref r)) + { + var identifier = r.ParseIdentifier(); + + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + } + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + return State.AfterMember; + } + private static State ParseElementName(ref CharacterReader r, List nodes) { var name = r.ParseIdentifier(); @@ -322,6 +351,7 @@ namespace Avalonia.Markup.Parsers BeforeMember, AttachedProperty, Indexer, + TypeCast, End, } @@ -383,5 +413,11 @@ namespace Avalonia.Markup.Parsers public string TypeName { get; set; } public int Level { get; set; } } + + public class TypeCastNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 1048148c1f..558130e23f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -59,6 +59,9 @@ namespace Avalonia.Markup.Parsers case BindingExpressionGrammar.NameNode elementName: nextNode = new ElementNameNode(_nameScope, elementName.Name); break; + case BindingExpressionGrammar.TypeCastNode typeCast: + nextNode = ParseTypeCastNode(typeCast); + break; } if (rootNode is null) { @@ -92,6 +95,22 @@ namespace Avalonia.Markup.Parsers return new FindAncestorNode(ancestorType, ancestorLevel); } + private TypeCastNode ParseTypeCastNode(BindingExpressionGrammar.TypeCastNode node) + { + Type castType = null; + if (!(node.Namespace is null) && !(node.TypeName is null)) + { + if (_typeResolver == null) + { + throw new InvalidOperationException("Cannot parse a binding path with a typed Cast without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); + } + + castType = _typeResolver(node.Namespace, node.TypeName); + } + + return new TypeCastNode(castType); + } + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { if (_typeResolver == null) From 4563c6cd241b2de78b7c88d9883f05c0f308d023 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:16:20 +0200 Subject: [PATCH 03/17] Compiled path support for cast --- src/Avalonia.Base/Data/Core/TypeCastNode.cs | 34 +++++++++++++ .../Avalonia.Markup.Xaml.csproj | 1 + .../CompiledBindings/CompiledBindingPath.cs | 51 +++++++++++++++++++ .../CompiledBindings/StrongTypeCastNode.cs | 18 +++++++ 4 files changed, 104 insertions(+) create mode 100644 src/Avalonia.Base/Data/Core/TypeCastNode.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs new file mode 100644 index 0000000000..476fd5527f --- /dev/null +++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Data.Core +{ + public class TypeCastNode : ExpressionNode + { + public override string Description => $"as {TargetType.FullName}"; + + public Type TargetType { get; } + + public TypeCastNode(Type type) + { + TargetType = type; + } + + protected virtual object Cast(object value) + { + return TargetType.IsInstanceOfType(value) ? value : null; + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.TryGetTarget(out object target)) + { + target = Cast(target); + reference = target == null ? NullReference : new WeakReference(target); + } + + base.StartListeningCore(reference); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 3e15d2f700..217da2d50d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index d627fe3cd3..f6636664c1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; @@ -53,6 +54,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings case IStronglyTypedStreamElement stream: node = new StreamNode(stream.CreatePlugin()); break; + case ITypeCastElement typeCast: + node = new StrongTypeCastNode(typeCast.Type, typeCast.Cast); + break; default: throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}"); } @@ -66,6 +70,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal object RawSource { get; } + + public override string ToString() + => string.Concat(_elements.Select(e => e.ToString())); } public class CompiledBindingPathBuilder @@ -126,6 +133,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return this; } + public CompiledBindingPathBuilder TypeCast() + { + _elements.Add(new TypeCastPathElement()); + return this; + } + public CompiledBindingPathBuilder SetRawSource(object rawSource) { _rawSource = rawSource; @@ -157,6 +170,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public IPropertyInfo Property { get; } public Func, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; } + + public override string ToString() + => $".{Property.Name}"; } internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement @@ -164,6 +180,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings IStreamPlugin CreatePlugin(); } + internal interface ITypeCastElement : ICompiledBindingPathElement + { + Type Type { get; } + + Func Cast { get; } + } + internal class TaskStreamPathElement : IStronglyTypedStreamElement { public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); @@ -181,6 +204,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement { public static readonly SelfPathElement Instance = new SelfPathElement(); + + public override string ToString() + => "$self"; } internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement @@ -193,6 +219,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public Type AncestorType { get; } public int Level { get; } + + public override string ToString() + => $"$parent[{AncestorType?.Name},{Level}]"; } internal class VisualAncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement @@ -217,6 +246,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public INameScope NameScope { get; } public string Name { get; } + + public override string ToString() + => $"#{Name}"; } internal class ArrayElementPathElement : ICompiledBindingPathElement @@ -229,5 +261,24 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings public int[] Indices { get; } public Type ElementType { get; } + public override string ToString() + => $"[{string.Join(",", Indices)}]"; + } + + internal class TypeCastPathElement : ITypeCastElement + { + private static object TryCast(object obj) + { + if (obj is T result) + return result; + return null; + } + + public Type Type => typeof(T); + + public Func Cast => TryCast; + + public override string ToString() + => $"({Type.FullName})"; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs new file mode 100644 index 0000000000..1252ec7eca --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs @@ -0,0 +1,18 @@ +using System; +using Avalonia.Data.Core; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public class StrongTypeCastNode : TypeCastNode + { + private Func _cast; + + public StrongTypeCastNode(Type type, Func cast) : base(type) + { + _cast = cast; + } + + protected override object Cast(object value) + => _cast(value); + } +} From 74bbdc193b97cf597c1104c892fbb30b7103b24e Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:21:58 +0200 Subject: [PATCH 04/17] actually compile cast in compiled binding --- .../XamlIlBindingPathHelper.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 03ec32b9cf..414ecc760a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -242,6 +242,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case RawSourceBindingExpressionNode rawSource: nodes.Add(new RawSourcePathElementNode(rawSource.RawSource)); break; + case BindingExpressionGrammar.TypeCastNode typeCastNode: + var castType = GetType(typeCastNode.Namespace, typeCastNode.TypeName); + + if (castType is null) + { + throw new XamlX.XamlParseException($"Unable to resolve cast to type {typeCastNode.Namespace}:{typeCastNode.TypeName} based on XAML tree.", lineInfo); + } + + nodes.Add(new TypeCastPathElementNode(castType)); + break; } } @@ -625,6 +635,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } + class TypeCastPathElementNode : IXamlIlBindingPathElementNode + { + public TypeCastPathElementNode(IXamlType ancestorType) + { + Type = ancestorType; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "TypeCast").MakeGenericMethod(new[] { Type })); + } + } + class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstEmitableNode { private readonly List _transformElements; From 0036bf22c4b4e5aac1874c12989a7270b5f3c018 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:25:06 +0200 Subject: [PATCH 05/17] add tests for reflection binding for casting in path --- .../MarkupExtensions/BindingExtensionTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index 9ea2cd643a..bbb68e7cdf 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -84,6 +84,54 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportCastToTypeInExpression() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, contentControl.Content); + } + } + + [Fact] + public void SupportCastToTypeInExpression_DifferentTypeEvaluatesToNull() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = "foo"; + + window.DataContext = dataContext; + + Assert.Equal(null, contentControl.Content); + } + } private class FooBar { public object Foo { get; } = null; From df95ba630dc92d6b32cb0ea7fb1992f2314f0318 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:28:01 +0200 Subject: [PATCH 06/17] add test for $parent as it look like not working for compiled binding --- .../CompiledBindingExtensionTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index c651aabe0a..a244f33384 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -601,6 +601,26 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportParentInPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + Assert.Equal("foo", contentControl.Content); + } + } + [Fact] public void ThrowsOnInvalidCompileBindingsDirective() { From ffc82bf7e441d058ca76f2a49a81c9a2a8ae11f6 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:30:55 +0200 Subject: [PATCH 07/17] try make $parent work with compiled binding --- .../XamlIlBindingPathHelper.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 414ecc760a..59ddfe2ef0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -198,20 +198,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case BindingExpressionGrammar.AncestorNode ancestor: if (ancestor.Namespace is null && ancestor.TypeName is null) { - var styledElementType = context.GetAvaloniaTypes().StyledElement; - var ancestorType = context - .ParentNodes() - .OfType() - .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) - .ElementAtOrDefault(ancestor.Level) - ?.Type.GetClrType(); - - if (ancestorType is null) + if (ancestor.Namespace is null && ancestor.TypeName is null) { - throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); - } + var styledElementType = context.GetAvaloniaTypes().StyledElement; + var ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .Skip(1) + .ElementAtOrDefault(ancestor.Level) + ?.Type.GetClrType(); + + if (ancestorType is null) + { + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); + } - nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); + nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); + } } else { From 8535ee690e81809eb69c737affebfb6a0e22b060 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:32:11 +0200 Subject: [PATCH 08/17] second attempt make $parent work in compiled binding --- .../CompilerExtensions/XamlIlBindingPathHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 59ddfe2ef0..3bea580da0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -436,7 +436,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { codeGen.Ldtype(Type) .Ldc_I4(_level) - .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor")); + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Ancestor")); } } From 9633300e5d10a188b3edeb6945a572a1e65d50da Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 18:33:57 +0200 Subject: [PATCH 09/17] add few more tests for cast in binding path --- .../CompiledBindingExtensionTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index a244f33384..dd89274517 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -637,6 +637,52 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportCastToTypeInExpression() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = new TestDataContext(); + + window.DataContext = dataContext; + + Assert.Equal(dataContext, contentControl.Content); + } + } + + [Fact] + public void SupportCastToTypeInExpression_DifferentTypeEvaluatesToNull() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var dataContext = "foo"; + + window.DataContext = dataContext; + + Assert.Equal(null, contentControl.Content); + } + } + [Fact] public void SupportCastToTypeInExpressionWithProperty() { From fa754bcaa2d62d253403f27f17b398292ee3962a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 19:01:38 +0200 Subject: [PATCH 10/17] add test for converter with parameter in compiled binding --- .../CompiledBindingExtensionTests.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index dd89274517..2f42f57434 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Globalization; using System.Reactive.Subjects; using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Markup.Data; using Avalonia.UnitTests; @@ -621,6 +623,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportConverterWithParameter() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = new TestDataContext() { StringProperty = "Foo" }; + + Assert.Equal("Foo+Bar", textBlock.Text); + } + } + [Fact] public void ThrowsOnInvalidCompileBindingsDirective() { @@ -782,6 +806,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public interface IHasPropertyDerived : IHasProperty { } + public class AppendConverter : IValueConverter + { + public static IValueConverter Instance { get; } = new AppendConverter(); + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => string.Format("{0}+{1}", value, parameter); + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + + } + public class TestDataContext : IHasPropertyDerived { public string StringProperty { get; set; } From 86492f5a75f8ade00ca03b24849554585c56e77b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 19:02:01 +0200 Subject: [PATCH 11/17] support converter parameter in compiled binding should fix #5030 --- .../MarkupExtensions/CompiledBindingExtension.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index aab733cb43..da39920eb3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -26,6 +26,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { Path = Path, Converter = Converter, + ConverterParameter = ConverterParameter, + TargetNullValue = TargetNullValue, FallbackValue = FallbackValue, Mode = Mode, Priority = Priority, From 979a659b49794400e513bdd3886640ff2f53d98d Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 13 Nov 2020 21:00:16 +0200 Subject: [PATCH 12/17] remove duplicate condition --- .../XamlIlBindingPathHelper.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 3bea580da0..1974dfe3bc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -198,24 +198,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case BindingExpressionGrammar.AncestorNode ancestor: if (ancestor.Namespace is null && ancestor.TypeName is null) { - if (ancestor.Namespace is null && ancestor.TypeName is null) + var styledElementType = context.GetAvaloniaTypes().StyledElement; + var ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .Skip(1) + .ElementAtOrDefault(ancestor.Level) + ?.Type.GetClrType(); + + if (ancestorType is null) { - var styledElementType = context.GetAvaloniaTypes().StyledElement; - var ancestorType = context - .ParentNodes() - .OfType() - .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) - .Skip(1) - .ElementAtOrDefault(ancestor.Level) - ?.Type.GetClrType(); - - if (ancestorType is null) - { - throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); - } - - nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); } + + nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); } else { @@ -622,10 +619,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private readonly IXamlAstValueNode _rawSource; public RawSourcePathElementNode(IXamlAstValueNode rawSource) - :base(rawSource) + : base(rawSource) { _rawSource = rawSource; - + } public IXamlType Type => _rawSource.Type.GetClrType(); From ebe1af51a93252c9e313cd56964ff001315883c7 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 14 Nov 2020 03:18:16 +0200 Subject: [PATCH 13/17] modify tests to c# style casting in binding path --- .../MarkupExtensions/BindingExtensionTests.cs | 4 ++-- .../MarkupExtensions/CompiledBindingExtensionTests.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs index bbb68e7cdf..20ed22e84f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs @@ -94,7 +94,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' > - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); @@ -120,7 +120,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' > - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 2f42f57434..22edcc5cb4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -671,7 +671,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions' x:DataType='local:TestDataContext'> - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); @@ -694,7 +694,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions' x:DataType='local:TestDataContext'> - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); @@ -717,7 +717,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions' x:DataType='local:TestDataContext'> - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); @@ -743,7 +743,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions' x:DataType='local:TestDataContext'> - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); @@ -769,7 +769,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions' x:DataType='local:TestDataContext'> - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var contentControl = window.FindControl("contentControl"); From 2c4e23be84530d4265b522411cf72d8c334c79e5 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 14 Nov 2020 03:28:12 +0200 Subject: [PATCH 14/17] add support for c# style casting in binding path --- .../Parsers/BindingExpressionGrammar.cs | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 7df53f430f..15db7a96e4 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -88,6 +88,11 @@ namespace Avalonia.Markup.Parsers } else if (ParseOpenBrace(ref r)) { + if (PeekOpenBrace(ref r)) + { + return State.TypeCast; + } + return State.AttachedProperty; } else if (PeekOpenBracket(ref r)) @@ -140,6 +145,11 @@ namespace Avalonia.Markup.Parsers { if (ParseOpenBrace(ref r)) { + if (PeekOpenBrace(ref r)) + { + return State.TypeCast; + } + return State.AttachedProperty; } else @@ -196,23 +206,30 @@ namespace Avalonia.Markup.Parsers private static State ParseTypeCast(ref CharacterReader r, List nodes) { + bool parseMemberBeforeAddCast = ParseOpenBrace(ref r); + var (ns, typeName) = ParseTypeName(ref r); - nodes.Add(new TypeCastNode { Namespace = ns, TypeName = typeName }); + var result = State.AfterMember; - if (ParseMemberAccessor(ref r)) + if (parseMemberBeforeAddCast) { - var identifier = r.ParseIdentifier(); + if (!ParseCloseBrace(ref r)) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } - nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + result = ParseBeforeMember(ref r, nodes); } + nodes.Add(new TypeCastNode { Namespace = ns, TypeName = typeName }); + if (r.End || !r.TakeIf(')')) { throw new ExpressionParseException(r.Position, "Expected ')'."); } - return State.AfterMember; + return result; } private static State ParseElementName(ref CharacterReader r, List nodes) @@ -317,11 +334,21 @@ namespace Avalonia.Markup.Parsers return !r.End && r.TakeIf('('); } + private static bool ParseCloseBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf(')'); + } + private static bool PeekOpenBracket(ref CharacterReader r) { return !r.End && r.Peek == '['; } + private static bool PeekOpenBrace(ref CharacterReader r) + { + return !r.End && r.Peek == '('; + } + private static bool ParseStreamOperator(ref CharacterReader r) { return !r.End && r.TakeIf('^'); @@ -373,9 +400,9 @@ namespace Avalonia.Markup.Parsers } } - public interface INode {} + public interface INode { } - public interface ITransformNode {} + public interface ITransformNode { } public class EmptyExpressionNode : INode { } @@ -396,11 +423,11 @@ namespace Avalonia.Markup.Parsers public IList Arguments { get; set; } } - public class NotNode : INode, ITransformNode {} + public class NotNode : INode, ITransformNode { } - public class StreamNode : INode {} + public class StreamNode : INode { } - public class SelfNode : INode {} + public class SelfNode : INode { } public class NameNode : INode { From bf2dfff8f6ba98f904a384c1441cb4a6886b9869 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 14 Nov 2020 17:42:56 +0200 Subject: [PATCH 15/17] add test for casting after indexer --- .../CompiledBindingExtensionTests.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 22edcc5cb4..50dfa2c7b0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -759,6 +759,36 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void SupportCastToTypeInExpressionWithPropertyIndexer() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + var data = new TestData() + { + StringProperty = "Foo" + }; + var dataContext = new TestDataContext + { + ObjectsArrayProperty = new object[] { data } + }; + + window.DataContext = dataContext; + + Assert.Equal(data.StringProperty, contentControl.Content); + } + } + [Fact] public void SupportCastToTypeInExpressionWithProperty_DifferentTypeEvaluatesToNull() { @@ -818,6 +848,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } + public class TestData + { + public string StringProperty { get; set; } + } + public class TestDataContext : IHasPropertyDerived { public string StringProperty { get; set; } @@ -830,6 +865,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public string[] ArrayProperty { get; set; } + public object[] ObjectsArrayProperty { get; set; } + public List ListProperty { get; set; } = new List(); public NonIntegerIndexer NonIntegerIndexerProperty { get; set; } = new NonIntegerIndexer(); From c16817aac524fcca112cd422daadc6744064050a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 14 Nov 2020 17:43:49 +0200 Subject: [PATCH 16/17] add support for casting after indexer in binding path --- .../Markup/Parsers/BindingExpressionGrammar.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 15db7a96e4..637802169d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -170,6 +170,12 @@ namespace Avalonia.Markup.Parsers { var (ns, owner) = ParseTypeName(ref r); + if(!r.End && r.TakeIf(')')) + { + nodes.Add(new TypeCastNode() { Namespace = ns, TypeName = owner }); + return State.AfterMember; + } + if (r.End || !r.TakeIf('.')) { throw new ExpressionParseException(r.Position, "Invalid attached property name."); @@ -220,6 +226,11 @@ namespace Avalonia.Markup.Parsers } result = ParseBeforeMember(ref r, nodes); + + if(r.Peek == '[') + { + result = ParseIndexer(ref r, nodes); + } } nodes.Add(new TypeCastNode { Namespace = ns, TypeName = typeName }); From d1cf6fc9bea043f053aae23e72932ea753ca1b64 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Sat, 14 Nov 2020 18:43:23 +0200 Subject: [PATCH 17/17] fix failing test --- .../Markup/Parsers/BindingExpressionGrammar.cs | 5 +++++ .../ExpressionObserverBuilderTests_AttachedProperty.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 637802169d..7c362e24cc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -183,6 +183,11 @@ namespace Avalonia.Markup.Parsers var name = r.ParseIdentifier(); + if (name.Length == 0) + { + throw new ExpressionParseException(r.Position, "Attached Property name expected after '.'."); + } + if (r.End || !r.TakeIf(')')) { throw new ExpressionParseException(r.Position, "Expected ')'."); diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs index 45cf28773f..7c48a975ef 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs @@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests.Parsers { var data = new Class1(); - Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner)", typeResolver: _typeResolver)); + Assert.Throws(() => ExpressionObserverBuilder.Build(data, "(Owner.)", typeResolver: _typeResolver)); } [Fact]