Skip to content

Commit fbf559d

Browse files
maxkatz6grokys
andauthored
Fix TemplateBinding and allow custom attributes in XamlValueConverter (#14612)
* Add InheritDataTypeFromAttribute and use it in TemplateBinding * Add failing tests for TemplateBinding depending on a scope * Update XamlX and RoslynTypeSystem * Add missing interface implementations * Improve errors readability in XamlAvaloniaPropertyHelper * Use more specific TryGetCorrectlyTypedValue overloads * Finally, respect InheritDataTypeFromAttribute in the AvaloniaProperty parser * Add some docs * Output better exception * Update XamlX * Add missing docs * Add attribute to well known types * Add Correctly_Resolve_TemplateBinding_In_Theme_Detached_Template test and fix ColorPicker usage --------- Co-authored-by: Steven Kirk <[email protected]>
1 parent 8744c64 commit fbf559d

13 files changed

+249
-24
lines changed

src/Avalonia.Base/Data/TemplateBinding.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Avalonia.Data.Converters;
66
using Avalonia.Data.Core;
77
using Avalonia.Logging;
8+
using Avalonia.Metadata;
89
using Avalonia.Styling;
910

1011
namespace Avalonia.Data
@@ -26,7 +27,7 @@ public TemplateBinding()
2627
{
2728
}
2829

29-
public TemplateBinding(AvaloniaProperty property)
30+
public TemplateBinding([InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)] AvaloniaProperty property)
3031
: base(BindingPriority.Template)
3132
{
3233
Property = property;
@@ -64,6 +65,7 @@ public TemplateBinding(AvaloniaProperty property)
6465
/// <summary>
6566
/// Gets or sets the name of the source property on the templated parent.
6667
/// </summary>
68+
[InheritDataTypeFrom(InheritDataTypeFromScopeKind.ControlTemplate)]
6769
public AvaloniaProperty? Property { get; set; }
6870

6971
/// <inheritdoc/>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
3+
namespace Avalonia.Metadata;
4+
5+
/// <summary>
6+
/// Represents the kind of scope from which a data type can be inherited. Used in resolving target for AvaloniaProperty.
7+
/// </summary>
8+
public enum InheritDataTypeFromScopeKind
9+
{
10+
/// <summary>
11+
/// Indicates that the data type should be inherited from a style.
12+
/// </summary>
13+
Style = 1,
14+
15+
/// <summary>
16+
/// Indicates that the data type should be inherited from a control template.
17+
/// </summary>
18+
ControlTemplate,
19+
}
20+
21+
/// <summary>
22+
/// Attribute that instructs the compiler to resolve the data type using specific scope hints, such as Style or ControlTemplate.
23+
/// </summary>
24+
/// <remarks>
25+
/// This attribute is used to configure markup extensions like TemplateBinding to properly parse AvaloniaProperty values,
26+
/// targeting a specific scope data type.
27+
/// </remarks>
28+
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
29+
public sealed class InheritDataTypeFromAttribute : Attribute
30+
{
31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="InheritDataTypeFromAttribute"/> class with the specified scope kind.
33+
/// </summary>
34+
/// <param name="scopeKind">The kind of scope from which to inherit the data type.</param>
35+
public InheritDataTypeFromAttribute(InheritDataTypeFromScopeKind scopeKind)
36+
{
37+
ScopeKind = scopeKind;
38+
}
39+
40+
/// <summary>
41+
/// Gets the kind of scope from which the data type should be inherited.
42+
/// </summary>
43+
public InheritDataTypeFromScopeKind ScopeKind { get; }
44+
}

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,31 @@ public static bool CustomValueConverter(AstTransformationContext context,
263263
{
264264
return true;
265265
}
266-
266+
267267
if (type.FullName == "Avalonia.AvaloniaProperty")
268268
{
269-
var scope = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>().FirstOrDefault();
269+
var attrType = context.GetAvaloniaTypes().InheritDataTypeFromAttribute;
270+
var scopeKind = customAttributes?
271+
.FirstOrDefault(a => a.Type.Equals(attrType))?.Parameters
272+
.FirstOrDefault() switch
273+
{
274+
1 => AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style,
275+
2 => AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate,
276+
_ => (AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes?)null
277+
};
278+
279+
var scope = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
280+
.FirstOrDefault(s => scopeKind.HasValue ? s.ScopeType == scopeKind : true);
270281
if (scope == null)
271-
throw new XamlX.XamlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node);
282+
{
283+
#if NET6_0_OR_GREATER
284+
var isScopeDefined = Enum.IsDefined<AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes>(scopeKind ?? default);
285+
#else
286+
var isScopeDefined = Enum.IsDefined(typeof(AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes), scopeKind ?? default);
287+
#endif
288+
var scopeKindStr = isScopeDefined ? scopeKind!.Value.ToString() : "parent";
289+
throw new XamlX.XamlLoadException($"Unable to find the {scopeKindStr} scope for AvaloniaProperty lookup", node);
290+
}
272291

273292
result = XamlIlAvaloniaPropertyHelper.CreateNode(context, text, scope.TargetType, node );
274293
return true;

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode nod
2222

2323
IXamlAstTypeReference targetType;
2424

25-
var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control");
25+
var templatableBaseType = context.GetAvaloniaTypes().Control;
2626

2727
targetType = tt?.Values.FirstOrDefault() switch
2828
{
@@ -49,7 +49,7 @@ class AvaloniaXamlIlTargetTypeMetadataNode : XamlValueWithSideEffectNodeBase
4949

5050
public enum ScopeTypes
5151
{
52-
Style,
52+
Style = 1,
5353
ControlTemplate,
5454
Transitions
5555
}

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ XamlIlSelectorNode Create(IEnumerable<SelectorGrammar.ISyntax> syntax,
8686

8787
if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context,
8888
new XamlAstTextNode(node, property.Value, type: context.Configuration.WellKnownTypes.String),
89-
targetProperty.PropertyType, out var typedValue))
89+
targetProperty, out var typedValue))
9090
throw new XamlTransformException(
9191
$"Cannot convert '{property.Value}' to '{targetProperty.PropertyType.GetFqn()}",
9292
node);

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode nod
2929
IXamlType targetType = null;
3030
IXamlLineInfo lineInfo = null;
3131

32+
var avaloniaTypes = context.GetAvaloniaTypes();
33+
3234
var styleParent = context.ParentNodes()
3335
.OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
3436
.FirstOrDefault(x => x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style);
@@ -46,17 +48,24 @@ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode nod
4648
}
4749

4850
IXamlType propType = null;
51+
IXamlIlAvaloniaPropertyNode avaloniaPropertyNode = null;
4952
var property = @on.Children.OfType<XamlAstXamlPropertyValueNode>()
5053
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property");
5154
if (property != null)
5255
{
53-
var propertyName = property.Values.OfType<XamlAstTextNode>().FirstOrDefault()?.Text;
54-
if (propertyName == null)
55-
throw new XamlStyleTransformException("Setter.Property must be a string", node);
56+
avaloniaPropertyNode = property.Values.OfType<IXamlIlAvaloniaPropertyNode>().FirstOrDefault();
57+
if (avaloniaPropertyNode is null)
58+
{
59+
var propertyName = property.Values.OfType<XamlAstTextNode>().FirstOrDefault()?.Text;
60+
if (propertyName == null)
61+
throw new XamlStyleTransformException("Setter.Property must be a string.", node);
62+
63+
avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
64+
new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]);
65+
66+
property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode};
67+
}
5668

57-
var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
58-
new XamlAstClrTypeReference(lineInfo, targetType, false), property.Values[0]);
59-
property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode};
6069
propType = avaloniaPropertyNode.AvaloniaPropertyType;
6170
}
6271
else
@@ -83,7 +92,21 @@ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode nod
8392
valueProperty.Values[0]);
8493

8594
valueProperty.Property = new SetterValueProperty(valueProperty.Property,
86-
on.Type.GetClrType(), propType, context.GetAvaloniaTypes());
95+
on.Type.GetClrType(), propType, avaloniaTypes);
96+
}
97+
98+
// Handling a very specific case, when ITemplate value is used inside of Setter.Value,
99+
// Which then is materialized for a specific control, and usually would set TemplatedParent.
100+
// Note: this code is not always valid, as TemplatedParent might not be set,
101+
// but we have better validation in runtime for TemplatedBinding.
102+
// See Correctly_Resolve_TemplateBinding_In_Theme_Detached_Template test.
103+
if (!avaloniaTypes.ITemplateOfControl.IsAssignableFrom(propType)
104+
&& on.Children.OfType<XamlAstObjectNode>()?.FirstOrDefault() is { } valueObj
105+
&& avaloniaTypes.ITemplateOfControl.IsAssignableFrom(valueObj?.Type.GetClrType()))
106+
{
107+
on.Children[on.Children.IndexOf(valueObj)] = new AvaloniaXamlIlTargetTypeMetadataNode(valueObj,
108+
new XamlAstClrTypeReference(on, targetType, false),
109+
AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
87110
}
88111

89112
return node;

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using XamlX;

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ sealed class AvaloniaXamlIlWellKnownTypes
3434
public IXamlType DependsOnAttribute { get; }
3535
public IXamlType DataTypeAttribute { get; }
3636
public IXamlType InheritDataTypeFromItemsAttribute { get; }
37+
public IXamlType InheritDataTypeFromAttribute { get; }
3738
public IXamlType MarkupExtensionOptionAttribute { get; }
3839
public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
3940
public IXamlType AvaloniaListAttribute { get; }
@@ -60,6 +61,8 @@ sealed class AvaloniaXamlIlWellKnownTypes
6061

6162
public IXamlType DataTemplate { get; }
6263
public IXamlType IDataTemplate { get; }
64+
public IXamlType ITemplateOfControl { get; }
65+
public IXamlType Control { get; }
6366
public IXamlType ItemsControl { get; }
6467
public IXamlType ReflectionBindingExtension { get; }
6568

@@ -196,6 +199,7 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
196199
DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute");
197200
DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute");
198201
InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute");
202+
InheritDataTypeFromAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromAttribute");
199203
MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
200204
MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
201205
AvaloniaListAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.AvaloniaListAttribute");
@@ -240,6 +244,8 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
240244
ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension");
241245
DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate");
242246
IDataTemplate = cfg.TypeSystem.GetType("Avalonia.Controls.Templates.IDataTemplate");
247+
Control = cfg.TypeSystem.GetType("Avalonia.Controls.Control");
248+
ITemplateOfControl = cfg.TypeSystem.GetType("Avalonia.Controls.ITemplate`1").MakeGenericType(Control);
243249
ItemsControl = cfg.TypeSystem.GetType("Avalonia.Controls.ItemsControl");
244250
ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension");
245251
RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource");

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,16 @@ public static IXamlIlAvaloniaPropertyNode CreateNode(AstTransformationContext co
8888
return new XamlIlAvaloniaPropertyFieldNode(context.GetAvaloniaTypes(), lineInfo, found);
8989
}
9090

91-
var clrProperty =
92-
((XamlAstClrProperty)new PropertyReferenceResolver().Transform(context,
93-
forgedReference));
94-
return new XamlIlAvaloniaPropertyNode(lineInfo,
95-
context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"),
96-
clrProperty);
91+
var clrProperty = (XamlAstClrProperty)new PropertyReferenceResolver().Transform(context, forgedReference);
92+
var avaloniaPropertyBaseType = context.GetAvaloniaTypes().AvaloniaProperty;
93+
94+
// PropertyReferenceResolver.Transform failed resolving property, return empty stub from here:
95+
if (clrProperty.DeclaringType == XamlPseudoType.Unknown)
96+
{
97+
return new XamlIlAvaloniaPropertyNode(lineInfo, avaloniaPropertyBaseType, clrProperty, XamlPseudoType.Unknown);
98+
}
99+
100+
return new XamlIlAvaloniaPropertyNode(lineInfo, avaloniaPropertyBaseType, clrProperty);
97101
}
98102

99103
public static IXamlType GetAvaloniaPropertyType(IXamlField field,
@@ -124,12 +128,16 @@ interface IXamlIlAvaloniaPropertyNode : IXamlAstValueNode
124128

125129
class XamlIlAvaloniaPropertyNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode
126130
{
127-
public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property) : base(lineInfo)
131+
public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property, IXamlType propertyType) : base(lineInfo)
128132
{
129133
Type = new XamlAstClrTypeReference(this, type, false);
130134
Property = property;
131-
AvaloniaPropertyType = Property.Getter?.ReturnType
132-
?? Property.Setters.First().Parameters[0];
135+
AvaloniaPropertyType = propertyType;
136+
}
137+
138+
public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property)
139+
: this(lineInfo, type, property, GetPropertyType(property))
140+
{
133141
}
134142

135143
public XamlAstClrProperty Property { get; }
@@ -143,6 +151,12 @@ public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeG
143151
}
144152

145153
public IXamlType AvaloniaPropertyType { get; }
154+
155+
private static IXamlType GetPropertyType(XamlAstClrProperty property) =>
156+
property.Getter?.ReturnType
157+
?? property.Setters.FirstOrDefault()?.Parameters[0]
158+
?? throw new InvalidOperationException(
159+
$"Unable to resolve \"{property.DeclaringType.Name}.{property.Name}\" property type. There is no setter or getter.");
146160
}
147161

148162
class XamlIlAvaloniaPropertyFieldNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode

src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ private static XamlIlBindingPathNode TransformBindingPath(AstTransformationConte
239239
{
240240
var textNode = new XamlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex], type: context.Configuration.WellKnownTypes.String);
241241
if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode,
242-
param, out var converted))
242+
property.CustomAttributes, param, out var converted))
243243
throw new XamlX.XamlTransformException(
244244
$"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}",
245245
textNode);

tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
using Avalonia.Controls;
2+
using Avalonia.Controls.Presenters;
3+
using Avalonia.Controls.Primitives;
4+
using Avalonia.Data;
5+
using Avalonia.Markup.Xaml.Templates;
26
using Avalonia.Media;
7+
using Avalonia.Styling;
38
using Avalonia.UnitTests;
49
using Avalonia.VisualTree;
510
using Xunit;
@@ -103,6 +108,72 @@ public void ControlTheme_Can_Be_Set_In_Style()
103108
Assert.Equal(Brushes.Red, border.Background);
104109
}
105110
}
111+
112+
[Fact]
113+
public void Correctly_Resolve_TemplateBinding_In_Nested_Style()
114+
{
115+
using (UnitTestApplication.Start(TestServices.StyledWindow))
116+
{
117+
var xaml = $@"
118+
<ControlTheme xmlns='https://github.com/avaloniaui'
119+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
120+
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
121+
TargetType='u:TestTemplatedControl'>
122+
<Setter Property='Template'>
123+
<ControlTemplate>
124+
<Border/>
125+
</ControlTemplate>
126+
</Setter>
127+
<Style Selector='^ /template/ Border'>
128+
<Setter Property='Tag' Value='{{TemplateBinding TestData}}'/>
129+
</Style>
130+
</ControlTheme>";
131+
132+
var theme = (ControlTheme)AvaloniaRuntimeXamlLoader.Load(xaml);
133+
var style = Assert.IsType<Style>(Assert.Single(theme.Children));
134+
var setter = Assert.IsType<Setter>(Assert.Single(style.Setters));
135+
136+
Assert.Equal(TestTemplatedControl.TestDataProperty, (setter.Value as TemplateBinding)?.Property);
137+
}
138+
}
139+
140+
[Fact]
141+
public void Correctly_Resolve_TemplateBinding_In_Theme_Detached_Template()
142+
{
143+
using (UnitTestApplication.Start(TestServices.StyledWindow))
144+
{
145+
var window = (Window)AvaloniaRuntimeXamlLoader.Load($@"
146+
<Window xmlns='https://github.com/avaloniaui'
147+
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
148+
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
149+
<Window.Resources>
150+
<ControlTheme x:Key='MyTheme' TargetType='ContentControl'>
151+
<Setter Property='CornerRadius' Value='10, 0, 0, 10' />
152+
<Setter Property='Content'>
153+
<Template>
154+
<Border CornerRadius='{{TemplateBinding CornerRadius}}'/>
155+
</Template>
156+
</Setter>
157+
<Setter Property='Template'>
158+
<ControlTemplate>
159+
<Button Content='{{TemplateBinding Content}}'
160+
ContentTemplate='{{TemplateBinding ContentTemplate}}' />
161+
</ControlTemplate>
162+
</Setter>
163+
</ControlTheme>
164+
</Window.Resources>
165+
166+
<ContentControl Theme='{{StaticResource MyTheme}}' />
167+
</Window>");
168+
var control = Assert.IsType<ContentControl>(window.Content);
169+
170+
window.Show();
171+
172+
var border = Assert.IsType<Border>(control.Content);
173+
174+
Assert.Equal(new CornerRadius(10, 0, 0, 10), border.CornerRadius);
175+
}
176+
}
106177

107178
private const string ControlThemeXaml = @"
108179
<ControlTheme x:Key='MyTheme' TargetType='u:TestTemplatedControl'>

0 commit comments

Comments
 (0)