From d35241d5e84a55f1c4df46974ac742e6a2450229 Mon Sep 17 00:00:00 2001 From: Athari Date: Sun, 30 Nov 2025 08:26:34 +0300 Subject: [PATCH 1/3] Added support for Classes="{Binding StringValue}" (#18068) --- src/Avalonia.Base/ClassBindingManager.cs | 40 ++++++++++- src/Avalonia.Base/StyledElementExtensions.cs | 8 ++- .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlClassesPropertyResolver.cs | 2 +- ...XamlIlResolveClassesPropertyTransformer.cs | 71 +++++++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 12 +++- 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 51dac66bb0..7b15d3cfe1 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Avalonia.Data; using Avalonia.Reactive; @@ -12,7 +13,44 @@ namespace Avalonia private static readonly Dictionary s_RegisteredProperties = new Dictionary(); - public static IDisposable Bind(StyledElement target, string className, BindingBase source, object anchor) + public static readonly AttachedProperty ClassesProperty = + AvaloniaProperty.RegisterAttached( + "Classes", typeof(ClassBindingManager), ""); + + public static void SetClasses(StyledElement element, string value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(ClassesProperty, value); + } + + public static string GetClasses(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(ClassesProperty); + } + + static ClassBindingManager() + { + ClassesProperty.Changed.AddClassHandler(ClassesPropertyChanged); + } + + private static void ClassesPropertyChanged(StyledElement sender, AvaloniaPropertyChangedEventArgs e) + { + var newValue = e.GetNewValue() ?? ""; + var newValues = newValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var currentValues = sender.Classes.Where(c => !c.StartsWith(":", StringComparison.Ordinal)); + if (currentValues.SequenceEqual(newValues)) + return; + + sender.Classes.Replace(newValues); + } + + public static IDisposable BindClasses(StyledElement target, BindingBase source, object anchor) + { + return target.Bind(ClassesProperty, source); + } + + public static IDisposable BindClass(StyledElement target, string className, BindingBase source, object anchor) { var prop = GetClassProperty(className); return target.Bind(prop, source); diff --git a/src/Avalonia.Base/StyledElementExtensions.cs b/src/Avalonia.Base/StyledElementExtensions.cs index beea29d7f7..e9f7491ed2 100644 --- a/src/Avalonia.Base/StyledElementExtensions.cs +++ b/src/Avalonia.Base/StyledElementExtensions.cs @@ -6,8 +6,14 @@ namespace Avalonia { public static class StyledElementExtensions { + public static IDisposable BindClasses(this StyledElement target, BindingBase source, object anchor) => + ClassBindingManager.BindClasses(target, source, anchor); + + public static void SetClasses(this StyledElement target, string classNames) => + ClassBindingManager.SetClasses(target, classNames); + public static IDisposable BindClass(this StyledElement target, string className, BindingBase source, object anchor) => - ClassBindingManager.Bind(target, className, source, anchor); + ClassBindingManager.BindClass(target, className, source, anchor); public static AvaloniaProperty GetClassProperty(string className) => ClassBindingManager.GetClassProperty(className); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index e0225a24f7..f18911e3e3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -45,6 +45,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Targeted InsertBefore( new AvaloniaXamlIlResolveClassesPropertiesTransformer(), + new AvaloniaXamlIlResolveClassesPropertyTransformer(), new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); InsertAfter( diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs index 48118df40c..3988ef7802 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs @@ -86,7 +86,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .Ldloc(bloc.Local) // TODO: provide anchor? .Ldnull(); - emitter.EmitCall(_types.ClassesBindMethod, true); + emitter.EmitCall(_types.BindClassMethod, true); } public IXamlType TargetType => _types.StyledElement; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs new file mode 100644 index 0000000000..459662cc7b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveClassesPropertyTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + var types = context.GetAvaloniaTypes(); + if (node is XamlAstNamePropertyReference prop && + prop.Name == "Classes" && + prop.TargetType is XamlAstClrTypeReference targetRef && + prop.DeclaringType is XamlAstClrTypeReference declaringRef && + types.StyledElement.IsAssignableFrom(targetRef.Type) && + types.StyledElement.IsAssignableFrom(declaringRef.Type) + ) + { + return new XamlAstClrProperty(node, prop.Name, types.StyledElement, types.StyledElementClassesProperty.Getter) + { + Setters = { new ClassesStringSetter(types), new ClassesBindingSetter(types) } + }; + } + return node; + } + + abstract class ClassesSetter(AvaloniaXamlIlWellKnownTypes types, IXamlType parameter) + : IXamlEmitablePropertySetter + { + public abstract void Emit(IXamlILEmitter emitter); + + protected AvaloniaXamlIlWellKnownTypes Types { get; } = types; + public IXamlType TargetType => Types.StyledElement; + public PropertySetterBinderParameters BinderParameters { get; } = new() { AllowXNull = false }; + public IReadOnlyList Parameters { get; } = [parameter]; + public IReadOnlyList CustomAttributes { get; } = []; + } + + class ClassesStringSetter(AvaloniaXamlIlWellKnownTypes types) + : ClassesSetter(types, types.XamlIlTypes.String) + { + public override void Emit(IXamlILEmitter emitter) + { + using (var value = emitter.LocalsPool.GetLocal(Parameters[0])) + emitter + .Stloc(value.Local) + .Ldloc(value.Local); + emitter.EmitCall(Types.SetClassesMethod, true); + } + } + + class ClassesBindingSetter(AvaloniaXamlIlWellKnownTypes types) + : ClassesSetter(types, types.IBinding) + { + public override void Emit(IXamlILEmitter emitter) + { + using (var value = emitter.LocalsPool.GetLocal(Parameters[0])) + emitter + .Stloc(value.Local) + .Ldloc(value.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(Types.BindClassesMethod, true); + } + } + } +} 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 8659eb1299..7677b9e9e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -101,7 +101,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinition { get; } public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } - public IXamlMethod ClassesBindMethod { get; } + public IXamlMethod BindClassMethod { get; } + public IXamlMethod BindClassesMethod { get; } + public IXamlMethod SetClassesMethod { get; } public IXamlProperty StyledElementClassesProperty { get; } public IXamlType IBrush { get; } public IXamlType ImmutableSolidColorBrush { get; } @@ -298,10 +300,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes"); StyledElementClassesProperty = StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes)); - ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + BindClassMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") .GetMethod("BindClass", IDisposable, false, StyledElement, cfg.WellKnownTypes.String, BindingBase, cfg.WellKnownTypes.Object); + BindClassesMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + .GetMethod("BindClasses", IDisposable, false, StyledElement, + BindingBase, cfg.WellKnownTypes.Object); + SetClassesMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + .GetMethod("SetClasses", cfg.WellKnownTypes.Void, false, StyledElement, + cfg.WellKnownTypes.String); IBrush = cfg.TypeSystem.GetType("Avalonia.Media.IBrush"); ImmutableSolidColorBrush = cfg.TypeSystem.GetType("Avalonia.Media.Immutable.ImmutableSolidColorBrush"); From ea4e15eb6e8c5ea55328171fd20c62085fad20dc Mon Sep 17 00:00:00 2001 From: Athari Date: Sun, 30 Nov 2025 12:17:02 +0300 Subject: [PATCH 2/3] Fixed Classes="{Binding StringValue}" overwriting Classes.Foo="True" and Classes.Foo="{Binding}" (#18068) --- src/Avalonia.Base/ClassBindingManager.cs | 46 +++++++++++++++++-- src/Avalonia.Base/Controls/Classes.cs | 20 ++++++++ src/Avalonia.Base/StyledElementExtensions.cs | 3 ++ .../AvaloniaXamlIlClassesPropertyResolver.cs | 8 +--- ...XamlIlResolveClassesPropertyTransformer.cs | 4 +- .../AvaloniaXamlIlWellKnownTypes.cs | 7 ++- 6 files changed, 74 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 7b15d3cfe1..4fb0038163 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -17,6 +17,10 @@ namespace Avalonia AvaloniaProperty.RegisterAttached( "Classes", typeof(ClassBindingManager), ""); + public static readonly AttachedProperty?> BoundClassesProperty = + AvaloniaProperty.RegisterAttached?>( + "BoundClasses", typeof(ClassBindingManager)); + public static void SetClasses(StyledElement element, string value) { _ = element ?? throw new ArgumentNullException(nameof(element)); @@ -29,6 +33,18 @@ namespace Avalonia return element.GetValue(ClassesProperty); } + public static void SetBoundClasses(StyledElement element, HashSet? value) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + element.SetValue(BoundClassesProperty, value); + } + + public static HashSet? GetBoundClasses(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + return element.GetValue(BoundClassesProperty); + } + static ClassBindingManager() { ClassesProperty.Changed.AddClassHandler(ClassesPropertyChanged); @@ -36,13 +52,28 @@ namespace Avalonia private static void ClassesPropertyChanged(StyledElement sender, AvaloniaPropertyChangedEventArgs e) { + var boundClasses = GetBoundClasses(sender); + var newValue = e.GetNewValue() ?? ""; var newValues = newValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var currentValues = sender.Classes.Where(c => !c.StartsWith(":", StringComparison.Ordinal)); + var currentValues = sender.Classes + .Where(c => !c.StartsWith(":", StringComparison.Ordinal) && boundClasses?.Contains(c) != true) + .ToList(); if (currentValues.SequenceEqual(newValues)) return; - sender.Classes.Replace(newValues); + sender.Classes.Replace(currentValues, newValues); + } + + private static void AddBoundClass(StyledElement target, string className) + { + var boundClasses = GetBoundClasses(target); + if (boundClasses == null) + { + boundClasses = []; + SetBoundClasses(target, boundClasses); + } + boundClasses.Add(className); } public static IDisposable BindClasses(StyledElement target, BindingBase source, object anchor) @@ -50,8 +81,15 @@ namespace Avalonia return target.Bind(ClassesProperty, source); } + public static void SetClass(StyledElement target, string className, bool value) + { + AddBoundClass(target, className); + target.Classes.Set(className, value); + } + public static IDisposable BindClass(StyledElement target, string className, BindingBase source, object anchor) { + AddBoundClass(target, className); var prop = GetClassProperty(className); return target.Bind(prop, source); } @@ -63,8 +101,8 @@ namespace Avalonia var prop = AvaloniaProperty.Register(ClassPropertyPrefix + className); prop.Changed.Subscribe(args => { - var classes = ((StyledElement)args.Sender).Classes; - classes.Set(className, args.NewValue.GetValueOrDefault()); + var sender = (StyledElement)args.Sender; + SetClass(sender, className, args.NewValue.GetValueOrDefault()); }); return prop; diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs index 611cc23992..51497d51d0 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -280,6 +280,26 @@ namespace Avalonia.Controls NotifyChanged(); } + internal void Replace(IList? toRemove, IList toAdd) + { + foreach (var name in toAdd) + { + ThrowIfPseudoclass(name, "added"); + } + + if (toRemove != null) + { + foreach (var name in toRemove) + { + ThrowIfPseudoclass(name, "removed"); + } + base.RemoveAll(toRemove); + } + + base.AddRange(toAdd); + NotifyChanged(); + } + /// void IPseudoClasses.Add(string name) { diff --git a/src/Avalonia.Base/StyledElementExtensions.cs b/src/Avalonia.Base/StyledElementExtensions.cs index e9f7491ed2..d402dd6920 100644 --- a/src/Avalonia.Base/StyledElementExtensions.cs +++ b/src/Avalonia.Base/StyledElementExtensions.cs @@ -15,6 +15,9 @@ namespace Avalonia public static IDisposable BindClass(this StyledElement target, string className, BindingBase source, object anchor) => ClassBindingManager.BindClass(target, className, source, anchor); + public static void SetClass(this StyledElement target, string className, bool value) => + ClassBindingManager.SetClass(target, className, value); + public static AvaloniaProperty GetClassProperty(string className) => ClassBindingManager.GetClassProperty(className); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs index 3988ef7802..547e3b32d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs @@ -46,15 +46,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public void Emit(IXamlILEmitter emitter) { using (var value = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.Boolean)) - { emitter .Stloc(value.Local) - .EmitCall(_types.StyledElementClassesProperty.Getter!) .Ldstr(_className) - .Ldloc(value.Local) - .EmitCall(_types.Classes.GetMethod(new FindMethodMethodSignature("Set", - _types.XamlIlTypes.Void, _types.XamlIlTypes.String, _types.XamlIlTypes.Boolean))); - } + .Ldloc(value.Local); + emitter.EmitCall(_types.SetClassMethod); } public IXamlType TargetType => _types.StyledElement; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs index 459662cc7b..48fb4efce3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs @@ -49,12 +49,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers emitter .Stloc(value.Local) .Ldloc(value.Local); - emitter.EmitCall(Types.SetClassesMethod, true); + emitter.EmitCall(Types.SetClassesMethod); } } class ClassesBindingSetter(AvaloniaXamlIlWellKnownTypes types) - : ClassesSetter(types, types.IBinding) + : ClassesSetter(types, types.BindingBase) { public override void Emit(IXamlILEmitter emitter) { 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 7677b9e9e8..f2ba8848f1 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -102,6 +102,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } public IXamlMethod BindClassMethod { get; } + public IXamlMethod SetClassMethod { get; } public IXamlMethod BindClassesMethod { get; } public IXamlMethod SetClassesMethod { get; } public IXamlProperty StyledElementClassesProperty { get; } @@ -302,8 +303,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes)); BindClassMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") .GetMethod("BindClass", IDisposable, false, StyledElement, - cfg.WellKnownTypes.String, - BindingBase, cfg.WellKnownTypes.Object); + cfg.WellKnownTypes.String, BindingBase, cfg.WellKnownTypes.Object); + SetClassMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + .GetMethod("SetClass", cfg.WellKnownTypes.Void, false, StyledElement, + cfg.WellKnownTypes.String, cfg.WellKnownTypes.Boolean); BindClassesMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") .GetMethod("BindClasses", IDisposable, false, StyledElement, BindingBase, cfg.WellKnownTypes.Object); From 8f4a7863deee0bbc6b5c1c6ae235a3febcbcf785 Mon Sep 17 00:00:00 2001 From: Athari Date: Sun, 18 Jan 2026 15:04:15 +0300 Subject: [PATCH 3/3] Added tests for ClassBindingManager. Fixed already existing (IsClassesBindingProperty returning incorrect name) and newly added (Class.Foo="false" failing to override Classes="Foo Bar") bugs. Changed anchor to nullable object to closer reflect underlying binding API. --- src/Avalonia.Base/ClassBindingManager.cs | 25 +-- src/Avalonia.Base/StyledElementExtensions.cs | 4 +- .../ClassBindingManagerTests.cs | 142 +++++++++++++++++- 3 files changed, 155 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 4fb0038163..06421b7161 100644 --- a/src/Avalonia.Base/ClassBindingManager.cs +++ b/src/Avalonia.Base/ClassBindingManager.cs @@ -17,7 +17,7 @@ namespace Avalonia AvaloniaProperty.RegisterAttached( "Classes", typeof(ClassBindingManager), ""); - public static readonly AttachedProperty?> BoundClassesProperty = + private static readonly AttachedProperty?> BoundClassesProperty = AvaloniaProperty.RegisterAttached?>( "BoundClasses", typeof(ClassBindingManager)); @@ -33,13 +33,13 @@ namespace Avalonia return element.GetValue(ClassesProperty); } - public static void SetBoundClasses(StyledElement element, HashSet? value) + private static void SetBoundClasses(StyledElement element, HashSet? value) { _ = element ?? throw new ArgumentNullException(nameof(element)); element.SetValue(BoundClassesProperty, value); } - public static HashSet? GetBoundClasses(StyledElement element) + private static HashSet? GetBoundClasses(StyledElement element) { _ = element ?? throw new ArgumentNullException(nameof(element)); return element.GetValue(BoundClassesProperty); @@ -55,7 +55,9 @@ namespace Avalonia var boundClasses = GetBoundClasses(sender); var newValue = e.GetNewValue() ?? ""; - var newValues = newValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var newValues = newValue.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(c => boundClasses?.Contains(c) != true) + .ToList(); var currentValues = sender.Classes .Where(c => !c.StartsWith(":", StringComparison.Ordinal) && boundClasses?.Contains(c) != true) .ToList(); @@ -76,9 +78,9 @@ namespace Avalonia boundClasses.Add(className); } - public static IDisposable BindClasses(StyledElement target, BindingBase source, object anchor) + public static IDisposable BindClasses(StyledElement target, BindingBase source, object? anchor) { - return target.Bind(ClassesProperty, source); + return target.Bind(ClassesProperty, source, anchor); } public static void SetClass(StyledElement target, string className, bool value) @@ -87,11 +89,11 @@ namespace Avalonia target.Classes.Set(className, value); } - public static IDisposable BindClass(StyledElement target, string className, BindingBase source, object anchor) + public static IDisposable BindClass(StyledElement target, string className, BindingBase source, object? anchor) { AddBoundClass(target, className); var prop = GetClassProperty(className); - return target.Bind(prop, source); + return target.Bind(prop, source, anchor); } [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice", @@ -120,11 +122,10 @@ namespace Avalonia [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 = null; + if (property.Name?.StartsWith(ClassPropertyPrefix, StringComparison.OrdinalIgnoreCase) == true) { - classPropertyName = property.Name.Substring(ClassPropertyPrefix.Length + 1); + classPropertyName = property.Name.Substring(ClassPropertyPrefix.Length); return true; } return false; diff --git a/src/Avalonia.Base/StyledElementExtensions.cs b/src/Avalonia.Base/StyledElementExtensions.cs index d402dd6920..246788de48 100644 --- a/src/Avalonia.Base/StyledElementExtensions.cs +++ b/src/Avalonia.Base/StyledElementExtensions.cs @@ -6,13 +6,13 @@ namespace Avalonia { public static class StyledElementExtensions { - public static IDisposable BindClasses(this StyledElement target, BindingBase source, object anchor) => + public static IDisposable BindClasses(this StyledElement target, BindingBase source, object? anchor = null) => ClassBindingManager.BindClasses(target, source, anchor); public static void SetClasses(this StyledElement target, string classNames) => ClassBindingManager.SetClasses(target, classNames); - public static IDisposable BindClass(this StyledElement target, string className, BindingBase source, object anchor) => + public static IDisposable BindClass(this StyledElement target, string className, BindingBase source, object? anchor = null) => ClassBindingManager.BindClass(target, className, source, anchor); public static void SetClass(this StyledElement target, string className, bool value) => diff --git a/tests/Avalonia.Base.UnitTests/ClassBindingManagerTests.cs b/tests/Avalonia.Base.UnitTests/ClassBindingManagerTests.cs index 23f9882ba9..87f86b27f4 100644 --- a/tests/Avalonia.Base.UnitTests/ClassBindingManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/ClassBindingManagerTests.cs @@ -1,15 +1,17 @@ -using Xunit; +using Avalonia.Data; +using Avalonia.Controls; +using Xunit; namespace Avalonia.Base.UnitTests { public class ClassBindingManagerTests { - [Fact] public void GetClassProperty_Should_Return_Same_Instance_For_Same_Class() { var property1 = ClassBindingManager.GetClassProperty("Foo"); var property2 = ClassBindingManager.GetClassProperty("Foo"); + Assert.Same(property1, property2); } @@ -18,7 +20,143 @@ namespace Avalonia.Base.UnitTests { var property1 = ClassBindingManager.GetClassProperty("Foo"); var property2 = ClassBindingManager.GetClassProperty("Bar"); + Assert.NotSame(property1, property2); } + + [Fact] + public void SetClass_Should_Add_Class() + { + var target = new StyledElement(); + + ClassBindingManager.SetClass(target, "Foo", true); + + Assert.Contains("Foo", target.Classes); + } + + [Fact] + public void SetClass_Should_Remove_Added_Class() + { + var target = new StyledElement(); + + ClassBindingManager.SetClass(target, "Foo", true); + ClassBindingManager.SetClass(target, "Foo", false); + + Assert.DoesNotContain("Foo", target.Classes); + } + + [Fact] + public void SetClasses_Should_Add_Classes() + { + var target = new StyledElement(); + + ClassBindingManager.SetClasses(target, "Foo Bar"); + + Assert.Contains("Foo", target.Classes); + Assert.Contains("Bar", target.Classes); + } + + [Fact] + public void SetClasses_Should_Remove_Added_Classes() + { + var target = new StyledElement(); + + ClassBindingManager.SetClasses(target, "Foo Bar"); + ClassBindingManager.SetClasses(target, ""); + + Assert.Empty(target.Classes); + } + + [Fact] + public void SetClasses_Should_Keep_PseudoClasses() + { + var target = new StyledElement(); + ((IPseudoClasses)target.Classes).Add(":Baz"); + + ClassBindingManager.SetClasses(target, "Foo"); + + Assert.Equal(new[] { ":Baz", "Foo" }, target.Classes); + } + + [Fact] + public void SetClass_Should_Override_SetClasses_Adding() + { + var target = new StyledElement(); + + ClassBindingManager.SetClasses(target, "Foo Bar"); + ClassBindingManager.SetClass(target, "Foo", false); + + Assert.Contains("Bar", target.Classes); + Assert.DoesNotContain("Foo", target.Classes); + } + + [Fact] + public void SetClass_Should_Override_SetClasses_Adding_When_Set_Before() + { + var target = new StyledElement(); + + ClassBindingManager.SetClass(target, "Foo", false); + ClassBindingManager.SetClasses(target, "Foo Bar"); + + Assert.Contains("Bar", target.Classes); + Assert.DoesNotContain("Foo", target.Classes); + } + + [Fact] + public void SetClass_Should_Override_SetClasses_Removing() + { + var target = new StyledElement(); + + ClassBindingManager.SetClasses(target, "Bar"); + ClassBindingManager.SetClass(target, "Foo", true); + + Assert.Contains("Foo", target.Classes); + Assert.Contains("Bar", target.Classes); + } + + [Fact] + public void SetClass_Should_Override_SetClasses_Removing_When_Set_Before() + { + var target = new StyledElement(); + + ClassBindingManager.SetClass(target, "Foo", true); + ClassBindingManager.SetClasses(target, "Bar"); + + Assert.Contains("Foo", target.Classes); + Assert.Contains("Bar", target.Classes); + } + + [Fact] + public void BindClass_Should_Update_Classes() + { + var target = new StyledElement(); + + using var d = ClassBindingManager.BindClass(target, "Bar", new Binding { Source = true }, null); + + Assert.Contains("Bar", target.Classes); + } + + [Fact] + public void BindClasses_Should_Update_Classes() + { + var target = new StyledElement(); + + using var d = ClassBindingManager.BindClasses(target, new Binding { Source = "Foo Bar" }, null); + + Assert.Equal("Foo Bar", ClassBindingManager.GetClasses(target)); + Assert.Contains("Foo", target.Classes); + Assert.Contains("Bar", target.Classes); + } + + [Fact] + public void IsClassesBindingProperty_Should_Detect_Classes_Properties() + { + var prop = ClassBindingManager.GetClassProperty("Foo"); + + var result = ClassBindingManager.IsClassesBindingProperty(prop, out var name); + + Assert.True(result); + Assert.Equal("Foo", name); + } } }