Browse Source

feat: Allow use of (Classes.`classname`) syntax on Style and ControlThe… (#16938)

* feat: Allow use of (Classes.`classname`) syntax on Style and ControlTheme Setter elements

* feat: Add Selector validation

* test: Check binding Classes in Setter
pull/17211/head
workgroupengineering 2 years ago
committed by GitHub
parent
commit
2d288847f7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 30
      src/Avalonia.Base/ClassBindingManager.cs
  2. 7
      src/Avalonia.Base/StyledElementExtensions.cs
  3. 3
      src/Avalonia.Base/Styling/Setter.cs
  4. 24
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs
  5. 8
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  6. 65
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs
  7. 13
      tests/Avalonia.Markup.Xaml.UnitTests/TestViewModel.cs
  8. 90
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs
  9. 98
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

30
src/Avalonia.Base/ClassBindingManager.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
using Avalonia.Reactive;
@ -7,13 +8,13 @@ namespace Avalonia
{
internal static class ClassBindingManager
{
private const string ClassPropertyPrefix = "__AvaloniaReserved::Classes::";
private static readonly Dictionary<string, AvaloniaProperty> s_RegisteredProperties =
new Dictionary<string, AvaloniaProperty>();
public static IDisposable Bind(StyledElement target, string className, IBinding source, object anchor)
{
if (!s_RegisteredProperties.TryGetValue(className, out var prop))
s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
var prop = GetClassProperty(className);
return target.Bind(prop, source);
}
@ -21,14 +22,33 @@ namespace Avalonia
Justification = "Classes.attr binding feature is implemented using intermediate avalonia properties for each class")]
private static AvaloniaProperty RegisterClassProxyProperty(string className)
{
var prop = AvaloniaProperty.Register<StyledElement, bool>("__AvaloniaReserved::Classes::" + className);
var prop = AvaloniaProperty.Register<StyledElement, bool>(ClassPropertyPrefix + className);
prop.Changed.Subscribe(args =>
{
var classes = ((StyledElement)args.Sender).Classes;
classes.Set(className, args.NewValue.GetValueOrDefault());
});
return prop;
}
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static AvaloniaProperty GetClassProperty(string className) =>
s_RegisteredProperties.TryGetValue(ClassPropertyPrefix + className, out var property)
? property
: s_RegisteredProperties[className] = RegisterClassProxyProperty(className);
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public static bool IsClassesBindingProperty(AvaloniaProperty property, [NotNullWhen(true)] out string? classPropertyName)
{
classPropertyName = default;
if(property.Name?.StartsWith(ClassPropertyPrefix, StringComparison.OrdinalIgnoreCase) == true)
{
classPropertyName = property.Name.Substring(ClassPropertyPrefix.Length + 1);
return true;
}
return false;
}
}
}

7
src/Avalonia.Base/StyledElementExtensions.cs

@ -1,4 +1,5 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Data;
namespace Avalonia
@ -7,5 +8,11 @@ namespace Avalonia
{
public static IDisposable BindClass(this StyledElement target, string className, IBinding source, object anchor) =>
ClassBindingManager.Bind(target, className, source, anchor);
public static AvaloniaProperty GetClassProperty(string className) =>
ClassBindingManager.GetClassProperty(className);
internal static bool IsClassesBindingProperty(this AvaloniaProperty property, [NotNullWhen(true)] out string? classPropertyName) =>
ClassBindingManager.IsClassesBindingProperty(property, out classPropertyName);
}
}

3
src/Avalonia.Base/Styling/Setter.cs

@ -75,6 +75,9 @@ namespace Avalonia.Styling
if (Property.IsDirect && instance.HasActivator)
throw new InvalidOperationException(
$"Cannot set direct property '{Property}' in '{instance.Source}' because the style has an activator.");
if (Property.IsClassesBindingProperty(out var classPropertyName) && instance.HasActivator)
throw new InvalidOperationException(
$"Cannot set Class Binding property '(Classes.{classPropertyName})' in '{instance.Source}' because the style has an activator.");
if (Value is IBinding2 binding)
return SetBinding((StyleInstance)instance, ao, binding);

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

@ -60,6 +60,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName,
new XamlAstClrTypeReference(on, targetType, false), property.Values[0]);
if (avaloniaPropertyNode is IXamlIlAvaloniaClassPropertyNode && HasComplexActivator(styleParent!))
{
throw new XamlStyleTransformException($"Cannot set Classes Binding property '{propertyName}' because the style has an activator."
, node);
}
property.Values = new List<IXamlAstValueNode> {avaloniaPropertyNode};
}
@ -137,6 +143,24 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
return node;
// Check that the style has selector activator and complexity
bool HasComplexActivator(AvaloniaXamlIlTargetTypeMetadataNode style)
{
if (style.Value is XamlAstObjectNode valueNode &&
valueNode.Children
.FirstOrDefault(n => n is XamlAstXamlPropertyValueNode
{
Property: XamlAstClrProperty{ Name : "Selector" }
}) is XamlAstXamlPropertyValueNode { Values.Count : >= 1 } selectorNone
)
{
return selectorNone.Values.Count > 1 ||
(selectorNone.Values[0] is not XamlIlTypeSelector);
}
return false;
}
}
class SetterValueProperty : XamlAstClrProperty

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

@ -130,6 +130,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType IReadOnlyListOfT { get; }
public IXamlType ControlTemplate { get; }
public IXamlType EventHandlerT { get; }
public IXamlMethod GetClassProperty { get; }
sealed internal class InteractivityWellKnownTypes
{
@ -327,6 +328,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IReadOnlyListOfT = cfg.TypeSystem.GetType("System.Collections.Generic.IReadOnlyList`1");
EventHandlerT = cfg.TypeSystem.GetType("System.EventHandler`1");
Interactivity = new InteractivityWellKnownTypes(cfg);
GetClassProperty = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions")
.GetMethod(name: "GetClassProperty",
returnType: AvaloniaProperty,
allowDowncast:false,
cfg.WellKnownTypes.String
);
}
}

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

@ -5,14 +5,13 @@ using Avalonia.Markup.Xaml.Parsers;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using Avalonia.Utilities;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.Transform.Transformers;
using XamlX.TypeSystem;
using XamlX.Emit;
using XamlX.IL;
using XamlIlEmitContext = XamlX.Emit.XamlEmitContext<XamlX.IL.IXamlILEmitter, XamlX.IL.XamlILNodeEmitResult>;
using IXamlIlAstEmitableNode = XamlX.Emit.IXamlAstEmitableNode<XamlX.IL.IXamlILEmitter, XamlX.IL.XamlILNodeEmitResult>;
using XamlIlEmitContext = XamlX.Emit.XamlEmitContext<XamlX.IL.IXamlILEmitter, XamlX.IL.XamlILNodeEmitResult>;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
@ -69,6 +68,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
if(parsedPropertyName.owner == null)
forgedReference = new XamlAstNamePropertyReference(lineInfo, selectorTypeReference,
propertyName, selectorTypeReference);
else if (string.IsNullOrWhiteSpace(parsedPropertyName.ns)
&& string.Equals(parsedPropertyName.owner, "Classes", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrWhiteSpace(parsedPropertyName.name)
)
{
return new XamlIlAvaloniaClassProperty(context.GetAvaloniaTypes(), parsedPropertyName.name, lineInfo);
}
else
{
var xmlOwner = parsedPropertyName.ns;
@ -124,7 +130,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
IXamlType AvaloniaPropertyType { get; }
}
// Marker interface, used to identify whether the Avalonia property represents Classes
interface IXamlIlAvaloniaClassPropertyNode : IXamlIlAvaloniaPropertyNode
{
}
class XamlIlAvaloniaPropertyNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode
{
public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property, IXamlType propertyType) : base(lineInfo)
@ -433,4 +445,47 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
}
sealed class XamlIlAvaloniaClassProperty : XamlAstClrProperty,
IXamlIlAvaloniaClassPropertyNode,
IXamlAstValueNode,
IXamlAstLocalsEmitableNode<IXamlILEmitter, XamlILNodeEmitResult>
{
private readonly IXamlMethod _method;
private readonly AvaloniaXamlIlWellKnownTypes _types;
private readonly string _className;
private readonly IXamlAstTypeReference _type;
private readonly IXamlType _returnType;
public XamlIlAvaloniaClassProperty(AvaloniaXamlIlWellKnownTypes types,
string className,
IXamlLineInfo lineInfo) : base(lineInfo, className, types.Classes, null, null, null)
{
Parameters = [types.XamlIlTypes.String];
_method = types.GetClassProperty;
AvaloniaPropertyType = types.XamlIlTypes.Boolean;
_types = types;
_returnType = _types.AvaloniaPropertyT.MakeGenericType(types.XamlIlTypes.Boolean);
_type = new XamlAstClrTypeReference(this, _returnType, false);
_className = className;
Setters = [];
}
public IXamlType AvaloniaPropertyType { get; }
public IReadOnlyList<IXamlType> Parameters { get; }
public IXamlAstTypeReference Type => _type;
public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters();
public XamlILNodeEmitResult Emit(XamlEmitContextWithLocals<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter emitter)
{
using (var loc = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.String))
{
emitter
.Ldstr(_className);
emitter.EmitCall(_method, false);
}
return XamlILNodeEmitResult.Type(0, _returnType);
}
}
}

13
tests/Avalonia.Markup.Xaml.UnitTests/TestViewModel.cs

@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.UnitTests
private string _string;
private int _integer;
private TestViewModel _child;
private bool _boolean;
public int Integer
{
@ -37,5 +38,15 @@ namespace Avalonia.Markup.Xaml.UnitTests
RaisePropertyChanged();
}
}
public bool Boolean
{
get => _boolean;
set
{
_boolean = value;
RaisePropertyChanged();
}
}
}
}
}

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

@ -1,3 +1,4 @@
using System.Xml;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -175,6 +176,95 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Can_Use_Classes_In_Setter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<Window.Resources>
<ControlTheme x:Key='MyTheme' TargetType='ContentControl'>
<Setter Property='CornerRadius' Value='10, 0, 0, 10' />
<Setter Property='(Classes.Banned)' Value='true'/>
<Setter Property='Content'>
<Template>
<Border CornerRadius='{TemplateBinding CornerRadius}'/>
</Template>
</Setter>
<Setter Property='Template'>
<ControlTemplate>
<Button Content='{TemplateBinding Content}'
ContentTemplate='{TemplateBinding ContentTemplate}' />
</ControlTemplate>
</Setter>
<Style Selector='^.Banned'>
<Setter Property="TextBlock.TextDecorations" Value="Strikethrough"/>
</Style>
</ControlTheme>
</Window.Resources>
<ContentControl Theme='{StaticResource MyTheme}' />
</Window>
""";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var control = Assert.IsType<ContentControl>(window.Content);
Assert.Same(TextDecorations.Strikethrough,control.GetValue(TextBlock.TextDecorationsProperty));
}
}
[Fact]
public void Can_Binding_Classes_In_Setter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $$$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
xmlns:vm='using:Avalonia.Markup.Xaml.UnitTests'>
<Window.Resources>
<ControlTheme x:Key='MyTheme' TargetType='ContentControl' x:DataType='vm:TestViewModel'>
<Setter Property='CornerRadius' Value='10, 0, 0, 10' />
<Setter Property='(Classes.Banned)' Value='{Binding Boolean}'/>
<Setter Property='Content'>
<Template>
<Border CornerRadius='{TemplateBinding CornerRadius}'/>
</Template>
</Setter>
<Setter Property='Template'>
<ControlTemplate>
<Button Content='{TemplateBinding Content}'
ContentTemplate='{TemplateBinding ContentTemplate}' />
</ControlTemplate>
</Setter>
<Style Selector='^.Banned'>
<Setter Property="TextBlock.TextDecorations" Value="Strikethrough"/>
</Style>
</ControlTheme>
</Window.Resources>
<Window.DataContext>
<vm:TestViewModel/>
</Window.DataContext>
<ContentControl Theme='{StaticResource MyTheme}' />
</Window>
""";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
window.ApplyTemplate();
var vm = window.DataContext as TestViewModel;
Assert.NotNull(vm);
var control = Assert.IsType<ContentControl>(window.Content);
Assert.Null(control.GetValue(TextBlock.TextDecorationsProperty));
vm.Boolean = true;
Assert.Same(TextDecorations.Strikethrough, control.GetValue(TextBlock.TextDecorationsProperty));
}
}
private const string ControlThemeXaml = @"
<ControlTheme x:Key='MyTheme' TargetType='u:TestTemplatedControl'>
<Setter Property='Template'>

98
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@ -2,11 +2,9 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Xml;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml.Styling;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling;
@ -640,5 +638,101 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Contains("ControlTemplate", exception.Message);
}
}
[Fact]
public void Can_Use_Classes_In_Setter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<Window.Styles>
<Style Selector="Border">
<Setter Property="(Classes.Banned)" Value='true'/>
<Style Selector="^.Banned">
<Setter Property='Background' Value='Red'/>
</Style>
</Style>
</Window.Styles>
<Border/>
</Window>
""";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = window.Content as Border;
Assert.NotNull(border);
Assert.Equal(Brushes.Red, border.Background);
}
}
[Fact]
public void Can_Binding_Classes_In_Setter()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $$"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'
xmlns:vm='using:Avalonia.Markup.Xaml.UnitTests'
>
<Window.Styles>
<Style Selector="Border" x:DataType='vm:TestViewModel'>
<Setter Property="(Classes.Banned)" Value='{Binding Boolean}'/>
<Style Selector="^.Banned">
<Setter Property='Background' Value='Red'/>
</Style>
</Style>
</Window.Styles>
<Window.DataContext>
<vm:TestViewModel/>
</Window.DataContext>
<Border/>
</Window>
""";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
window.ApplyTemplate();
var vm = window.DataContext as TestViewModel;
Assert.NotNull(vm);
var border = window.Content as Border;
Assert.NotNull(border);
Assert.Null(border.Background);
vm.Boolean = true;
Assert.Equal(Brushes.Red, border.Background);
}
}
[Fact]
public void Fails_Use_Classes_In_Setter_When_Selector_Is_Complex()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = $"""
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:u='using:Avalonia.Markup.Xaml.UnitTests.Xaml'>
<Window.Styles>
<Style Selector="Border:pointover">
<Setter Property="(Classes.Banned)" Value='true'/>
<Style Selector="^.Banned">
<Setter Property='Background' Value='Red'/>
</Style>
</Style>
</Window.Styles>
<Border/>
</Window>
""";
var exception = Assert.ThrowsAny<XmlException>(() => AvaloniaRuntimeXamlLoader.Load(xaml));
Assert.Equal ("Cannot set Classes Binding property '(Classes.Banned)' because the style has an activator. Line 6, position 14.", exception.Message);
}
}
}
}

Loading…
Cancel
Save