diff --git a/src/Irihi.Avalonia.Shared.Public/Helpers/ClassHelper.cs b/src/Irihi.Avalonia.Shared.Public/Helpers/ClassHelper.cs new file mode 100644 index 0000000..5848470 --- /dev/null +++ b/src/Irihi.Avalonia.Shared.Public/Helpers/ClassHelper.cs @@ -0,0 +1,65 @@ +using System.Collections.Specialized; +using Avalonia; +using Avalonia.Collections; + +namespace Irihi.Avalonia.Shared.Helpers; + +public class ClassHelper +{ + public static readonly AttachedProperty ClassesProperty = + AvaloniaProperty.RegisterAttached("Classes"); + + public static readonly AttachedProperty ClassSourceProperty = + AvaloniaProperty.RegisterAttached("ClassSource"); + + static ClassHelper() + { + ClassesProperty.Changed.AddClassHandler(OnClassesChanged); + ClassSourceProperty.Changed.AddClassHandler(OnClassSourceChanged); + } + + private static void OnClassSourceChanged(StyledElement arg1, AvaloniaPropertyChangedEventArgs arg2) + { + if (arg2.NewValue is not StyledElement styledElement) return; + arg1.Classes.Clear(); + var nonPseudoClasses = styledElement.Classes.Where(c => !c.StartsWith(":")); + arg1.Classes.AddRange(nonPseudoClasses); + styledElement.Classes.WeakSubscribe((o, e) => OnSourceClassesChanged(o, e, arg1)); + } + + private static void OnSourceClassesChanged(object sender, NotifyCollectionChangedEventArgs e, StyledElement target) + { + if (sender is not AvaloniaList classes) return; + target.Classes.Clear(); + var nonPseudoClasses = classes.Where(c => !c.StartsWith(":")); + target.Classes.AddRange(nonPseudoClasses); + } + + public static void SetClasses(AvaloniaObject obj, string value) + { + obj.SetValue(ClassesProperty, value); + } + + public static string GetClasses(AvaloniaObject obj) + { + return obj.GetValue(ClassesProperty); + } + + private static void OnClassesChanged(StyledElement sender, AvaloniaPropertyChangedEventArgs value) + { + var classes = value.GetNewValue(); + if (classes is null) return; + sender.Classes.Clear(); + sender.Classes.Add(classes); + } + + public static void SetClassSource(StyledElement obj, StyledElement value) + { + obj.SetValue(ClassSourceProperty, value); + } + + public static StyledElement GetClassSource(StyledElement obj) + { + return obj.GetValue(ClassSourceProperty); + } +} \ No newline at end of file diff --git a/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems b/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems index 88af842..5c29919 100644 --- a/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems +++ b/src/Irihi.Avalonia.Shared.Public/Irihi.Avalonia.Shared.Public.projitems @@ -22,6 +22,7 @@ + diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Helpers/ClassHelperTests.cs b/test/Irihi.Avalonia.Shared.UnitTest.Public/Helpers/ClassHelperTests.cs new file mode 100644 index 0000000..66369e8 --- /dev/null +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Helpers/ClassHelperTests.cs @@ -0,0 +1,86 @@ +using Avalonia.Controls; +using Avalonia.Styling; +using Irihi.Avalonia.Shared.Helpers; + +namespace Irihi.Avalonia.Shared.UnitTest.Helpers; + +public class ClassHelperTests +{ + [Fact] + public void ClassesProperty_Should_Set_And_Get_Value() + { + var control = new Button(); + ClassHelper.SetClasses(control, "test-class"); + Assert.Equal("test-class", ClassHelper.GetClasses(control)); + } + + [Fact] + public void ClassSourceProperty_Should_Set_And_Get_Value() + { + var sourceControl = new Button(); + var targetControl = new Button(); + ClassHelper.SetClassSource(targetControl, sourceControl); + Assert.Equal(sourceControl, ClassHelper.GetClassSource(targetControl)); + } + + [Fact] + public void OnClassSourceChanged_Should_Copy_Classes() + { + var sourceControl = new Button(); + sourceControl.Classes.Add("source-class"); + var targetControl = new Button(); + ClassHelper.SetClassSource(targetControl, sourceControl); + Assert.Contains("source-class", targetControl.Classes); + } + + [Fact] + public void OnClassSourceChanged_Should_Not_Copy_PseudoClasses() + { + var sourceControl = new Button(); + IPseudoClasses pseudoClasses = sourceControl.Classes; + pseudoClasses.Add(":pseudo-class"); + var targetControl = new Button(); + ClassHelper.SetClassSource(targetControl, sourceControl); + Assert.DoesNotContain(":pseudo-class", targetControl.Classes); + } + + [Fact] + public void OnClassesChanged_Should_Update_Classes() + { + var control = new Button(); + ClassHelper.SetClasses(control, "initial-class"); + ClassHelper.SetClasses(control, "updated-class"); + Assert.Contains("updated-class", control.Classes); + Assert.DoesNotContain("initial-class", control.Classes); + } + + [Fact] + public void OnSourceClassesChanged_Should_Update_Target_Classes() + { + var sourceControl = new Button(); + var targetControl = new Button(); + ClassHelper.SetClassSource(targetControl, sourceControl); + sourceControl.Classes.Add("new-class"); + Assert.Contains("new-class", targetControl.Classes); + } + + [Fact] + public void OnClassesChanged_Should_Not_Remove_PseudoClasses() + { + var control = new Button(); + IPseudoClasses pseudoClasses = control.Classes; + pseudoClasses.Add(":pseudo-class"); + pseudoClasses.Add(":pseudo-class2"); + ClassHelper.SetClasses(control, "initial-class"); + Assert.Contains(":pseudo-class", control.Classes); + Assert.Contains(":pseudo-class2", control.Classes); + } + + [Fact] + public void ClassesProperty_Should_Not_Throw_Exception_When_Null() + { + var control = new Button(); + ClassHelper.SetClasses(control, null); + Assert.Null(ClassHelper.GetClasses(control)); + } +} \ No newline at end of file diff --git a/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems b/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems index 9fc6e54..2187879 100644 --- a/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems +++ b/test/Irihi.Avalonia.Shared.UnitTest.Public/Irihi.Avalonia.Shared.UnitTest.Public.projitems @@ -17,6 +17,7 @@ +