From ea4e15eb6e8c5ea55328171fd20c62085fad20dc Mon Sep 17 00:00:00 2001 From: Athari Date: Sun, 30 Nov 2025 12:17:02 +0300 Subject: [PATCH] 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);