diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index ecb5863043..8632f16aa9 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/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 s_RegisteredProperties = new Dictionary(); - + 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("__AvaloniaReserved::Classes::" + className); + var prop = AvaloniaProperty.Register(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; + } } } diff --git a/src/Avalonia.Base/StyledElementExtensions.cs b/src/Avalonia.Base/StyledElementExtensions.cs index d530dae8d4..e68664a503 100644 --- a/src/Avalonia.Base/StyledElementExtensions.cs +++ b/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); } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index 98efff55ee..f2f01945e2 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/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); 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 6bd00ed0ba..68a5321bf2 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/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 {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 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 679938e504..a59785cbbf 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/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 + ); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 3ecd855980..7fd8d987b9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/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; using IXamlIlAstEmitableNode = XamlX.Emit.IXamlAstEmitableNode; +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; 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 + { + 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 Parameters { get; } + public IXamlAstTypeReference Type => _type; + + public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); + + public XamlILNodeEmitResult Emit(XamlEmitContextWithLocals context, IXamlILEmitter emitter) + { + using (var loc = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.String)) + { + emitter + .Ldstr(_className); + emitter.EmitCall(_method, false); + } + return XamlILNodeEmitResult.Type(0, _returnType); + } + } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/TestViewModel.cs b/tests/Avalonia.Markup.Xaml.UnitTests/TestViewModel.cs index 18d1944dff..f7840011f1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/TestViewModel.cs +++ b/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(); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs index 6c2ee2bb08..566fb48c4f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlThemeTests.cs +++ b/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 = $$$""" + + + + + + + + + + +