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); + } } }