diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs index 7944d8b569..24cded1d22 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -72,16 +72,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers v.Property is AvaloniaSyntheticCompiledBindingProperty prop && prop.Name == SyntheticCompiledBindingPropertyName.ElementName); - var sourceProperty = syntheticCompiledBindingProperties - .FirstOrDefault(v => - v.Property is AvaloniaSyntheticCompiledBindingProperty prop - && prop.Name == SyntheticCompiledBindingPropertyName.Source); - var relativeSourceProperty = syntheticCompiledBindingProperties .FirstOrDefault(v => v.Property is AvaloniaSyntheticCompiledBindingProperty prop && prop.Name == SyntheticCompiledBindingPropertyName.RelativeSource); + var sourceProperty = binding.Children.OfType() + .FirstOrDefault(v => + v.Property is XamlAstClrProperty prop + && prop.Name == "Source"); + if (elementNameProperty?.Values[0] is XamlAstTextNode elementName) { convertedNode = new BindingExpressionGrammar.NameNode { Name = elementName.Text }; @@ -91,14 +91,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlParseException($"Invalid ElementName '{elementNameProperty.Values[0]}'.", elementNameProperty.Values[0]); } - if (sourceProperty?.Values[0] != null) + if (sourceProperty != null && convertedNode != null) { - if (convertedNode != null) - { - throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); - } - - convertedNode = new RawSourceBindingExpressionNode(sourceProperty?.Values[0]); + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); } if (GetRelativeSourceObjectFromAssignment( @@ -223,10 +218,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { binding.Children.Remove(elementNameProperty); } - if (sourceProperty != null) - { - binding.Children.Remove(sourceProperty); - } if (relativeSourceProperty != null) { binding.Children.Remove(relativeSourceProperty); 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 fc32084687..4d8c940bbc 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using XamlX; using XamlX.Ast; using XamlX.Transform; using XamlX.TypeSystem; @@ -15,14 +13,102 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { if (node is XamlAstConstructableObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) { - IXamlType startType; - var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); - if (parentDataContextNode is null) + IXamlType startType = null; + var sourceProperty = binding.Children.OfType().FirstOrDefault(c => c.Property.Name == "Source"); + if ((sourceProperty?.Values.Count ?? 0) == 1) { - throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); + var sourceValue = sourceProperty.Values[0]; + switch (sourceValue) + { + case XamlAstTextNode textNode: + startType = textNode.Type?.GetClrType(); + break; + + case XamlMarkupExtensionNode extension: + startType = extension.Type?.GetClrType(); + + //let's try to infer StaticResource type from parent resources in xaml + if (extension.Value.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.MarkupExtensions.StaticResourceExtension" && + extension.Value is XamlAstConstructableObjectNode cn && + cn.Arguments.Count == 1 && cn.Arguments[0] is XamlAstTextNode keyNode) + { + bool matchProperty(IXamlAstNode node, IXamlType styledElementType, string propertyName) + { + return (node is XamlPropertyAssignmentNode p && + p.Property.DeclaringType == styledElementType && p.Property.Name == propertyName) + || + (node is XamlManipulationGroupNode m && m.Children.Count > 0 && + m.Children[0] is XamlPropertyAssignmentNode pm && + pm.Property.DeclaringType == styledElementType && pm.Property.Name == propertyName); + } + + string getResourceValue_xKey(XamlPropertyAssignmentNode node) + => node.Values.Count == 2 && node.Values[0] is XamlAstTextNode t ? t.Text : ""; + + IXamlType getResourceValue_Type(XamlPropertyAssignmentNode node, IXamlType xamlType) + => node.Values.Count == 2 ? node.Values[1].Type.GetClrType() : xamlType; + + IEnumerable getResourceValues(IXamlAstNode node) + { + if (node is XamlPropertyAssignmentNode propertyNode) + { + if (propertyNode.Values.Count == 1 && + propertyNode.Values[0] is XamlAstConstructableObjectNode obj && + obj.Type.GetClrType().FullName == "Avalonia.Controls.ResourceDictionary") + { + foreach (var r in obj.Children.SelectMany(c => getResourceValues(c))) + { + yield return r; + } + } + else + { + yield return propertyNode; + } + } + else if (node is XamlManipulationGroupNode m) + { + foreach (var r in m.Children.OfType()) + { + yield return r; + } + } + } + + string key = keyNode.Text; + + var styledElement = context.GetAvaloniaTypes().StyledElement; + var resource = context.ParentNodes() + .OfType() + .Where(o => styledElement.IsAssignableFrom(o.Type.GetClrType())) + .Select(o => o.Children.FirstOrDefault(p => matchProperty(p, styledElement, "Resources"))) + .Where(r => r != null) + .SelectMany(r => getResourceValues(r)) + .FirstOrDefault(r => getResourceValue_xKey(r) == key); + + if (resource != null) + { + startType = getResourceValue_Type(resource, startType); + } + } + break; + + case XamlStaticExtensionNode staticExtension: + startType = staticExtension.Type?.GetClrType(); + break; + } } - startType = parentDataContextNode.DataContextType; + if (startType == null) + { + 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.", binding); + } + + startType = parentDataContextNode.DataContextType; + } XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs index 154c6a235c..ba05347a52 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs @@ -25,11 +25,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return new AvaloniaSyntheticCompiledBindingProperty(node, SyntheticCompiledBindingPropertyName.RelativeSource); } - else if (prop.Name == "Source") - { - return new AvaloniaSyntheticCompiledBindingProperty(node, - SyntheticCompiledBindingPropertyName.Source); - } } return node; @@ -39,8 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers enum SyntheticCompiledBindingPropertyName { ElementName, - RelativeSource, - Source + RelativeSource } class AvaloniaSyntheticCompiledBindingProperty : XamlAstNode, IXamlAstPropertyReference diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index da39920eb3..17d2ea7ae9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -32,6 +32,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Priority = Priority, StringFormat = StringFormat, + Source = Source, DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) }; } @@ -52,6 +53,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { + if (Source != null) + { + return CreateSourceObserver( + Source, + Path.BuildExpression(enableDataValidation)); + } + if (Path.RawSource != null) { return CreateSourceObserver( @@ -77,5 +85,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions [ConstructorArgument("path")] public CompiledBindingPath Path { get; set; } + + public object Source { get; set; } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 1cf9e0877d..6f549a4ffa 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Markup.Data; +using Avalonia.Media; using Avalonia.UnitTests; using XamlX; using Xunit; @@ -536,7 +537,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } [Fact] - public void ResolvesSourceBindingLongForm() + public void Binds_To_Source() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -559,6 +560,124 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Binds_To_Source_StaticResource() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_StaticResource1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + test + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_StaticResource_In_ResourceDictionary() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_StaticResource_In_ResourceDictionary1() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + test + + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void Binds_To_Source_xStatic() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var contentControl = window.FindControl("contentControl"); + + Assert.Equal(Brushes.Red.Color, contentControl.Content); + } + } + [Fact] public void CompilesBindingWhenRequested() {