Browse Source

Merge 8f4a7863de into 3068850405

pull/20182/merge
Alexander Prokhorov 3 days ago
committed by GitHub
parent
commit
d6e6887484
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 93
      src/Avalonia.Base/ClassBindingManager.cs
  2. 20
      src/Avalonia.Base/Controls/Classes.cs
  3. 13
      src/Avalonia.Base/StyledElementExtensions.cs
  4. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  5. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
  6. 71
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveClassesPropertyTransformer.cs
  7. 17
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  8. 142
      tests/Avalonia.Base.UnitTests/ClassBindingManagerTests.cs

93
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,10 +13,87 @@ namespace Avalonia
private static readonly Dictionary<string, AvaloniaProperty> s_RegisteredProperties =
new Dictionary<string, AvaloniaProperty>();
public static IDisposable Bind(StyledElement target, string className, BindingBase source, object anchor)
public static readonly AttachedProperty<string> ClassesProperty =
AvaloniaProperty.RegisterAttached<StyledElement, string>(
"Classes", typeof(ClassBindingManager), "");
private 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);
}
private static void SetBoundClasses(StyledElement element, HashSet<string>? value)
{
_ = element ?? throw new ArgumentNullException(nameof(element));
element.SetValue(BoundClassesProperty, value);
}
private 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)
.Where(c => boundClasses?.Contains(c) != true)
.ToList();
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, BindingBase source, object? anchor)
{
return target.Bind(ClassesProperty, source, anchor);
}
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);
return target.Bind(prop, source, anchor);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", "AVP1001:The same AvaloniaProperty should not be registered twice",
@ -25,8 +103,8 @@ namespace Avalonia
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;
@ -44,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;

20
src/Avalonia.Base/Controls/Classes.cs

@ -280,6 +280,26 @@ namespace Avalonia.Controls
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)
{

13
src/Avalonia.Base/StyledElementExtensions.cs

@ -6,8 +6,17 @@ namespace Avalonia
{
public static class StyledElementExtensions
{
public static IDisposable BindClass(this StyledElement target, string className, BindingBase source, object anchor) =>
ClassBindingManager.Bind(target, className, source, 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 = null) =>
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);

1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -48,6 +48,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// Targeted
InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlResolveClassesPropertyTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());

10
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;
@ -86,7 +82,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
.Ldloc(bloc.Local)
// TODO: provide anchor?
.Ldnull();
emitter.EmitCall(_types.ClassesBindMethod, true);
emitter.EmitCall(_types.BindClassMethod, true);
}
public IXamlType TargetType => _types.StyledElement;

71
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<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.BindingBase)
{
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);
}
}
}
}

17
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@ -102,7 +102,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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; }
@ -301,10 +304,18 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
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, 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);
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");

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

Loading…
Cancel
Save