Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions src/Avalonia.Base/ClassBindingManager.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Data;
using Avalonia.Reactive;

Expand All @@ -12,8 +13,83 @@ internal static class ClassBindingManager
private static readonly Dictionary<string, AvaloniaProperty> s_RegisteredProperties =
new Dictionary<string, AvaloniaProperty>();

public static IDisposable Bind(StyledElement target, string className, IBinding source, object anchor)
public static readonly AttachedProperty<string> ClassesProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"Classes", typeof(ClassBindingManager), "");

public static readonly AttachedProperty<HashSet<string>?> BoundClassesProperty =
AvaloniaProperty.RegisterAttached<StyledElement, HashSet<string>?>(
"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<string>? value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(BoundClassesProperty, value);
}

public static HashSet<string>? GetBoundClasses(StyledElement element)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
return element.GetValue(BoundClassesProperty);
}

static ClassBindingManager()
{
ClassesProperty.Changed.AddClassHandler<StyledElement, string>(ClassesPropertyChanged);
}

private static void ClassesPropertyChanged(StyledElement sender, AvaloniaPropertyChangedEventArgs<string> e)
{
var boundClasses = GetBoundClasses(sender);

var newValue = e.GetNewValue<string?>() ?? "";
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);
}
Expand All @@ -25,8 +101,8 @@ private static AvaloniaProperty RegisterClassProxyProperty(string className)
var prop = AvaloniaProperty.Register<StyledElement, bool>(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;
Expand Down
20 changes: 20 additions & 0 deletions src/Avalonia.Base/Controls/Classes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,26 @@ public void Replace(IList<string> source)
NotifyChanged();
}

internal void Replace(IList<string>? toRemove, IList<string> 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();
}

/// <inheritdoc/>
void IPseudoClasses.Add(string name)
{
Expand Down
11 changes: 10 additions & 1 deletion src/Avalonia.Base/StyledElementExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ void InsertBeforeMany(Type[] types, params IXamlAstTransformer[] t)
// Targeted
InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlResolveClassesPropertyTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter<PropertyReferenceResolver>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IXamlILEmitter>
{
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<IXamlType> Parameters { get; } = [parameter];
public IReadOnlyList<IXamlCustomAttribute> 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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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");
Expand Down