diff --git a/src/Avalonia.Base/Data/TemplateBinding.cs b/src/Avalonia.Base/Data/TemplateBinding.cs index db878620b4..fd3c0f5b62 100644 --- a/src/Avalonia.Base/Data/TemplateBinding.cs +++ b/src/Avalonia.Base/Data/TemplateBinding.cs @@ -5,6 +5,7 @@ using System.Globalization; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.Logging; +using Avalonia.Metadata; using Avalonia.Styling; namespace Avalonia.Data @@ -26,7 +27,7 @@ namespace Avalonia.Data { } - public TemplateBinding(AvaloniaProperty property) + public TemplateBinding([InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)] AvaloniaProperty property) : base(BindingPriority.Template) { Property = property; @@ -64,6 +65,7 @@ namespace Avalonia.Data /// /// Gets or sets the name of the source property on the templated parent. /// + [InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)] public AvaloniaProperty? Property { get; set; } /// diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs new file mode 100644 index 0000000000..4a57667f82 --- /dev/null +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromAttribute.cs @@ -0,0 +1,44 @@ +using System; + +namespace Avalonia.Metadata; + +/// +/// Represents the kind of scope from which a data type can be inherited. Used in resolving target for AvaloniaProperty. +/// +public enum InheritDataTypeFromScopeKind +{ + /// + /// Indicates that the data type should be inherited from a style. + /// + Style = 1, + + /// + /// Indicates that the data type should be inherited from a control template. + /// + ControlTemplate, +} + +/// +/// Attribute that instructs the compiler to resolve the data type using specific scope hints, such as Style or ControlTemplate. +/// +/// +/// This attribute is used to configure markup extensions like TemplateBinding to properly parse AvaloniaProperty values, +/// targeting a specific scope data type. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] +public sealed class InheritDataTypeFromAttribute : Attribute +{ + /// + /// Initializes a new instance of the class with the specified scope kind. + /// + /// The kind of scope from which to inherit the data type. + public InheritDataTypeFromAttribute(InheritDataTypeFromScopeKind scopeKind) + { + ScopeKind = scopeKind; + } + + /// + /// Gets the kind of scope from which the data type should be inherited. + /// + public InheritDataTypeFromScopeKind ScopeKind { get; } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 5cb5316b1c..53fa0a2a4d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -263,12 +263,31 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { return true; } - + if (type.FullName == "Avalonia.AvaloniaProperty") { - var scope = context.ParentNodes().OfType().FirstOrDefault(); + var attrType = context.GetAvaloniaTypes().InheritDataTypeFromAttribute; + var scopeKind = customAttributes? + .FirstOrDefault(a => a.Type.Equals(attrType))?.Parameters + .FirstOrDefault() switch + { + 1 => AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style, + 2 => AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate, + _ => (AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes?)null + }; + + var scope = context.ParentNodes().OfType() + .FirstOrDefault(s => scopeKind.HasValue ? s.ScopeType == scopeKind : true); if (scope == null) - throw new XamlX.XamlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node); + { +#if NET6_0_OR_GREATER + var isScopeDefined = Enum.IsDefined(scopeKind ?? default); +#else + var isScopeDefined = Enum.IsDefined(typeof(AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes), scopeKind ?? default); +#endif + var scopeKindStr = isScopeDefined ? scopeKind!.Value.ToString() : "parent"; + throw new XamlX.XamlLoadException($"Unable to find the {scopeKindStr} scope for AvaloniaProperty lookup", node); + } result = XamlIlAvaloniaPropertyHelper.CreateNode(context, text, scope.TargetType, node ); return true; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index afe4654df9..d19fa977ee 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -22,7 +22,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IXamlAstTypeReference targetType; - var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control"); + var templatableBaseType = context.GetAvaloniaTypes().Control; targetType = tt?.Values.FirstOrDefault() switch { @@ -49,7 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public enum ScopeTypes { - Style, + Style = 1, ControlTemplate, Transitions } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index a8718524a5..619940e208 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -86,7 +86,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, new XamlAstTextNode(node, property.Value, type: context.Configuration.WellKnownTypes.String), - targetProperty.PropertyType, out var typedValue)) + targetProperty, out var typedValue)) throw new XamlTransformException( $"Cannot convert '{property.Value}' to '{targetProperty.PropertyType.GetFqn()}", node); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 01612bff27..4557335bdf 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -29,6 +29,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IXamlType targetType = null; IXamlLineInfo lineInfo = null; + var avaloniaTypes = context.GetAvaloniaTypes(); + var styleParent = context.ParentNodes() .OfType() .FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); @@ -46,17 +48,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } IXamlType propType = null; + IXamlIlAvaloniaPropertyNode avaloniaPropertyNode = null; var property = @on.Children.OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); if (property != null) { - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlStyleTransformException("Setter.Property must be a string", node); + avaloniaPropertyNode = property.Values.OfType().FirstOrDefault(); + if (avaloniaPropertyNode is null) + { + var propertyName = property.Values.OfType().FirstOrDefault()?.Text; + if (propertyName == null) + throw new XamlStyleTransformException("Setter.Property must be a string.", node); + + avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]); + + property.Values = new List {avaloniaPropertyNode}; + } - var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]); - property.Values = new List {avaloniaPropertyNode}; propType = avaloniaPropertyNode.AvaloniaPropertyType; } else @@ -83,7 +92,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers valueProperty.Values[0]); valueProperty.Property = new SetterValueProperty(valueProperty.Property, - on.Type.GetClrType(), propType, context.GetAvaloniaTypes()); + on.Type.GetClrType(), propType, avaloniaTypes); + } + + // Handling a very specific case, when ITemplate value is used inside of Setter.Value, + // Which then is materialized for a specific control, and usually would set TemplatedParent. + // Note: this code is not always valid, as TemplatedParent might not be set, + // but we have better validation in runtime for TemplatedBinding. + // See Correctly_Resolve_TemplateBinding_In_Theme_Detached_Template test. + if (!avaloniaTypes.ITemplateOfControl.IsAssignableFrom(propType) + && on.Children.OfType()?.FirstOrDefault() is { } valueObj + && avaloniaTypes.ITemplateOfControl.IsAssignableFrom(valueObj?.Type.GetClrType())) + { + on.Children[on.Children.IndexOf(valueObj)] = new AvaloniaXamlIlTargetTypeMetadataNode(valueObj, + new XamlAstClrTypeReference(on, targetType, false), + AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate); } return node; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index c056f2b3f5..651bd80576 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using XamlX; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 9bad038d42..02201b8109 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -34,6 +34,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType DependsOnAttribute { get; } public IXamlType DataTypeAttribute { get; } public IXamlType InheritDataTypeFromItemsAttribute { get; } + public IXamlType InheritDataTypeFromAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } public IXamlType AvaloniaListAttribute { get; } @@ -60,6 +61,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType DataTemplate { get; } public IXamlType IDataTemplate { get; } + public IXamlType ITemplateOfControl { get; } + public IXamlType Control { get; } public IXamlType ItemsControl { get; } public IXamlType ReflectionBindingExtension { get; } @@ -196,6 +199,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute"); DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute"); InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); + InheritDataTypeFromAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute"); @@ -240,6 +244,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension"); DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate"); + Control = cfg.TypeSystem.GetType("Avalonia.Controls.Control"); + ITemplateOfControl = cfg.TypeSystem.GetType("Avalonia.Controls.ITemplate`1").MakeGenericType(Control); ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl"); ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 387d153018..2ec19a1aa4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -88,12 +88,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return new XamlIlAvaloniaPropertyFieldNode(context.GetAvaloniaTypes(), lineInfo, found); } - var clrProperty = - ((XamlAstClrProperty)new PropertyReferenceResolver().Transform(context, - forgedReference)); - return new XamlIlAvaloniaPropertyNode(lineInfo, - context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"), - clrProperty); + var clrProperty = (XamlAstClrProperty)new PropertyReferenceResolver().Transform(context, forgedReference); + var avaloniaPropertyBaseType = context.GetAvaloniaTypes().AvaloniaProperty; + + // PropertyReferenceResolver.Transform failed resolving property, return empty stub from here: + if (clrProperty.DeclaringType == XamlPseudoType.Unknown) + { + return new XamlIlAvaloniaPropertyNode(lineInfo, avaloniaPropertyBaseType, clrProperty, XamlPseudoType.Unknown); + } + + return new XamlIlAvaloniaPropertyNode(lineInfo, avaloniaPropertyBaseType, clrProperty); } public static IXamlType GetAvaloniaPropertyType(IXamlField field, @@ -124,12 +128,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions class XamlIlAvaloniaPropertyNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { - public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property) : base(lineInfo) + public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property, IXamlType propertyType) : base(lineInfo) { Type = new XamlAstClrTypeReference(this, type, false); Property = property; - AvaloniaPropertyType = Property.Getter?.ReturnType - ?? Property.Setters.First().Parameters[0]; + AvaloniaPropertyType = propertyType; + } + + public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property) + : this(lineInfo, type, property, GetPropertyType(property)) + { } public XamlAstClrProperty Property { get; } @@ -143,6 +151,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } public IXamlType AvaloniaPropertyType { get; } + + private static IXamlType GetPropertyType(XamlAstClrProperty property) => + property.Getter?.ReturnType + ?? property.Setters.FirstOrDefault()?.Parameters[0] + ?? throw new InvalidOperationException( + $"Unable to resolve \"{property.DeclaringType.Name}.{property.Name}\" property type. There is no setter or getter."); } class XamlIlAvaloniaPropertyFieldNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 21a3f4eae2..0adb26973d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -239,7 +239,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { var textNode = new XamlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex], type: context.Configuration.WellKnownTypes.String); if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode, - param, out var converted)) + property.CustomAttributes, param, out var converted)) throw new XamlX.XamlTransformException( $"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}", textNode); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs index 66ad6b6d6a..6c2ee2bb08 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs @@ -1,5 +1,10 @@ using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; @@ -103,6 +108,72 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Equal(Brushes.Red, border.Background); } } + + [Fact] + public void Correctly_Resolve_TemplateBinding_In_Nested_Style() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" + + + + + + + +"; + + var theme = (ControlTheme)AvaloniaRuntimeXamlLoader.Load(xaml); + var style = Assert.IsType"; + + var style = (Style)AvaloniaRuntimeXamlLoader.Load(xaml); + var setter = Assert.IsType(Assert.Single(style.Setters)); + + Assert.Equal(TestTemplatedControl.TestDataProperty, (setter.Value as TemplateBinding)?.Property); + } + } + + [Fact] + public void Fails_To_Resolve_TemplateBinding_In_Style_Without_Template_Metadata() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = $@" +"; + + var exception = Assert.ThrowsAny(() => AvaloniaRuntimeXamlLoader.Load(xaml)); + Assert.Contains("ControlTemplate", exception.Message); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs index 0c862bb66a..b8d7d6502e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TestTemplatedControl.cs @@ -4,5 +4,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { public class TestTemplatedControl : TemplatedControl { + public static readonly StyledProperty TestDataProperty = + AvaloniaProperty.Register(nameof(TestData)); + + public object TestData + { + get => GetValue(TestDataProperty); + set => SetValue(TestDataProperty, value); + } } }