diff --git a/src/Avalonia.Base/ClassBindingManager.cs b/src/Avalonia.Base/ClassBindingManager.cs index 35b5bc88632..5d4741e9bf8 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,8 +13,83 @@ internal static class ClassBindingManager private static readonly Dictionary s_RegisteredProperties = new Dictionary(); - public static IDisposable Bind(StyledElement target, string className, IBinding source, object anchor) + public static readonly AttachedProperty ClassesProperty = + 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)); + element.SetValue(ClassesProperty, value); + } + + public static string GetClasses(StyledElement element) + { + _ = element ?? throw new ArgumentNullException(nameof(element)); + 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); + } + + 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) && boundClasses?.Contains(c) != true) + .ToList(); + if (currentValues.SequenceEqual(newValues)) + return; + + 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, IBinding source, object anchor) + { + 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, IBinding source, object anchor) { + AddBoundClass(target, className); var prop = GetClassProperty(className); return target.Bind(prop, source); } @@ -25,8 +101,8 @@ private static AvaloniaProperty RegisterClassProxyProperty(string className) 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 611cc239920..51497d51d0c 100644 --- a/src/Avalonia.Base/Controls/Classes.cs +++ b/src/Avalonia.Base/Controls/Classes.cs @@ -280,6 +280,26 @@ public void Replace(IList source) 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 e68664a5038..297f079c1b8 100644 --- a/src/Avalonia.Base/StyledElementExtensions.cs +++ b/src/Avalonia.Base/StyledElementExtensions.cs @@ -6,8 +6,17 @@ namespace Avalonia { public static class StyledElementExtensions { + public static IDisposable BindClasses(this StyledElement target, IBinding 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, IBinding source, object anchor) => - ClassBindingManager.Bind(target, className, source, 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/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index e0225a24f79..f18911e3e32 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 @@ void InsertBeforeMany(Type[] types, params IXamlAstTransformer[] t) // 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 eccd3d9e091..cc6c8ded1d5 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 @@ public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className) 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; @@ -86,7 +82,7 @@ public void Emit(IXamlILEmitter emitter) .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 00000000000..067027bd99f --- /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); + } + } + + 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 0aa9814e176..6218c04ab22 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -100,7 +100,10 @@ sealed class AvaloniaXamlIlWellKnownTypes public IXamlType ColumnDefinition { get; } public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } - public IXamlMethod ClassesBindMethod { get; } + public IXamlMethod BindClassMethod { get; } + public IXamlMethod SetClassMethod { get; } + public IXamlMethod BindClassesMethod { get; } + public IXamlMethod SetClassesMethod { get; } public IXamlProperty StyledElementClassesProperty { get; } public IXamlType IBrush { get; } public IXamlType ImmutableSolidColorBrush { get; } @@ -296,10 +299,18 @@ public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) 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, + cfg.WellKnownTypes.String, IBinding, 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, IBinding, 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");