diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs
index d627a2bf19..eec21d4423 100644
--- a/nukebuild/Build.cs
+++ b/nukebuild/Build.cs
@@ -301,14 +301,19 @@ partial class Build : NukeBuild
.Executes(() =>
{
var data = Parameters;
+ var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore";
+ var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish";
+
+ DotNetPublish(c => c
+ .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj")
+ .EnableNoBuild()
+ .SetConfiguration(data.Configuration)
+ .AddProperty("PackageVersion", data.Version)
+ .AddProperty("PublishDir", pathToPublish));
+
Zip(data.ZipCoreArtifacts, data.BinRoot);
Zip(data.ZipNuGetArtifacts, data.NugetRoot);
- Zip(data.ZipTargetControlCatalogDesktopDir,
- GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat(
- GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat(
- GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat(
- GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat(
- GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe")));
+ Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish);
});
Target CreateIntermediateNugetPackages => _ => _
diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs
index c76019d9eb..a92c988fbd 100644
--- a/nukebuild/BuildParameters.cs
+++ b/nukebuild/BuildParameters.cs
@@ -58,8 +58,7 @@ public partial class Build
public string FileZipSuffix { get; }
public AbsolutePath ZipCoreArtifacts { get; }
public AbsolutePath ZipNuGetArtifacts { get; }
- public AbsolutePath ZipSourceControlCatalogDesktopDir { get; }
- public AbsolutePath ZipTargetControlCatalogDesktopDir { get; }
+ public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; }
public BuildParameters(Build b)
@@ -129,9 +128,7 @@ public partial class Build
FileZipSuffix = Version + ".zip";
ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix);
ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix);
- ZipSourceControlCatalogDesktopDir =
- RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461");
- ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix);
+ ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix);
}
string GetVersion()
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index d5aedf7783..3c2d2ee359 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -1,7 +1,7 @@
- Exe
+ WinExe
netcoreapp3.1
true
@@ -15,6 +15,10 @@
+
+
+ en
+
diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj
index 1aa926a2a6..53ad213d92 100644
--- a/samples/ControlCatalog/ControlCatalog.csproj
+++ b/samples/ControlCatalog/ControlCatalog.csproj
@@ -27,6 +27,6 @@
-
+
diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Styling/ClassBindingManager.cs
new file mode 100644
index 0000000000..e8b1cc301d
--- /dev/null
+++ b/src/Avalonia.Styling/ClassBindingManager.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Data;
+
+namespace Avalonia
+{
+ internal static class ClassBindingManager
+ {
+ private static readonly Dictionary s_RegisteredProperties =
+ new Dictionary();
+
+ public static IDisposable Bind(IStyledElement target, string className, IBinding source, object anchor)
+ {
+ if (!s_RegisteredProperties.TryGetValue(className, out var prop))
+ s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className);
+ return target.Bind(prop, source, anchor);
+ }
+
+ private static AvaloniaProperty RegisterClassProxyProperty(string className)
+ {
+ var prop = AvaloniaProperty.Register("__AvaloniaReserved::Classes::" + className);
+ prop.Changed.Subscribe(args =>
+ {
+ var classes = ((IStyledElement)args.Sender).Classes;
+ classes.Set(className, args.NewValue.GetValueOrDefault());
+ });
+
+ return prop;
+ }
+ }
+}
diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs
index 51dca57928..4e2783d4ec 100644
--- a/src/Avalonia.Styling/Controls/Classes.cs
+++ b/src/Avalonia.Styling/Controls/Classes.cs
@@ -265,5 +265,26 @@ namespace Avalonia.Controls
$"The pseudoclass '{name}' may only be {operation} by the control itself.");
}
}
+
+ ///
+ /// Adds a or removes a style class to/from the collection.
+ ///
+ /// The class names.
+ /// If true adds the class, if false, removes it.
+ ///
+ /// Only standard classes may be added or removed via this method. To add pseudoclasses (classes
+ /// beginning with a ':' character) use the protected
+ /// property.
+ ///
+ public void Set(string name, bool value)
+ {
+ if (value)
+ {
+ if (!Contains(name))
+ Add(name);
+ }
+ else
+ Remove(name);
+ }
}
}
diff --git a/src/Avalonia.Styling/StyledElementExtensions.cs b/src/Avalonia.Styling/StyledElementExtensions.cs
new file mode 100644
index 0000000000..0c5a5f7438
--- /dev/null
+++ b/src/Avalonia.Styling/StyledElementExtensions.cs
@@ -0,0 +1,11 @@
+using System;
+using Avalonia.Data;
+
+namespace Avalonia
+{
+ public static class StyledElementExtensions
+ {
+ public static IDisposable BindClass(this IStyledElement target, string className, IBinding source, object anchor) =>
+ ClassBindingManager.Bind(target, className, source, anchor);
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index abff763bb1..a191dc59fb 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// Targeted
InsertBefore(
+ new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter(
- new AvaloniaXamlIlAvaloniaPropertyResolver());
+ new AvaloniaXamlIlAvaloniaPropertyResolver(),
+ new AvaloniaXamlIlReorderClassesPropertiesTransformer()
+ );
InsertBefore(
new AvaloniaXamlIlBindingPathParser(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
new file mode 100644
index 0000000000..23232dbcf3
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
@@ -0,0 +1,97 @@
+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 AvaloniaXamlIlResolveClassesPropertiesTransformer : IXamlAstTransformer
+ {
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstNamePropertyReference prop
+ && prop.TargetType is XamlAstClrTypeReference targetRef
+ && prop.DeclaringType is XamlAstClrTypeReference declaringRef)
+ {
+ var types = context.GetAvaloniaTypes();
+ if (types.StyledElement.IsAssignableFrom(targetRef.Type)
+ && types.Classes.Equals(declaringRef.Type))
+ {
+ return new XamlAstClrProperty(node, "class:" + prop.Name, types.Classes,
+ null)
+ {
+ Setters = { new ClassValueSetter(types, prop.Name), new ClassBindingSetter(types, prop.Name) }
+ };
+ }
+ }
+ return node;
+ }
+
+
+ class ClassValueSetter : IXamlEmitablePropertySetter
+ {
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+ private readonly string _className;
+
+ public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+ {
+ _types = types;
+ _className = className;
+ Parameters = new[] { types.XamlIlTypes.Boolean };
+ }
+
+ 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)));
+ }
+ }
+
+ public IXamlType TargetType => _types.StyledElement;
+
+ public PropertySetterBinderParameters BinderParameters { get; } =
+ new PropertySetterBinderParameters { AllowXNull = false };
+ public IReadOnlyList Parameters { get; }
+ }
+
+ class ClassBindingSetter : IXamlEmitablePropertySetter
+ {
+ private readonly AvaloniaXamlIlWellKnownTypes _types;
+ private readonly string _className;
+
+ public ClassBindingSetter(AvaloniaXamlIlWellKnownTypes types, string className)
+ {
+ _types = types;
+ _className = className;
+ Parameters = new[] {types.IBinding};
+ }
+
+ public void Emit(IXamlILEmitter emitter)
+ {
+ using (var bloc = emitter.LocalsPool.GetLocal(_types.IBinding))
+ emitter
+ .Stloc(bloc.Local)
+ .Ldstr(_className)
+ .Ldloc(bloc.Local)
+ // TODO: provide anchor?
+ .Ldnull();
+ emitter.EmitCall(_types.ClassesBindMethod, true);
+ }
+
+ public IXamlType TargetType => _types.StyledElement;
+
+ public PropertySetterBinderParameters BinderParameters { get; } =
+ new PropertySetterBinderParameters { AllowXNull = false };
+ public IReadOnlyList Parameters { get; }
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
new file mode 100644
index 0000000000..ae3515a6d6
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
@@ -0,0 +1,40 @@
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
+{
+ class AvaloniaXamlIlReorderClassesPropertiesTransformer : IXamlAstTransformer
+ {
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlAstObjectNode obj)
+ {
+ IXamlAstNode classesNode = null;
+ IXamlAstNode firstSingleClassNode = null;
+ var types = context.GetAvaloniaTypes();
+ foreach (var child in obj.Children)
+ {
+ if (child is XamlAstXamlPropertyValueNode propValue
+ && propValue.Property is XamlAstClrProperty prop)
+ {
+ if (prop.DeclaringType.Equals(types.Classes))
+ {
+ if (firstSingleClassNode == null)
+ firstSingleClassNode = child;
+ }
+ else if (prop.Name == "Classes" && prop.DeclaringType.Equals(types.StyledElement))
+ classesNode = child;
+ }
+ }
+
+ if (classesNode != null && firstSingleClassNode != null)
+ {
+ obj.Children.Remove(classesNode);
+ obj.Children.Insert(obj.Children.IndexOf(firstSingleClassNode), classesNode);
+ }
+ }
+
+ return node;
+ }
+ }
+}
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 34aae2c5ed..c4995b2de3 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AssignBindingAttribute { get; }
public IXamlType UnsetValueType { get; }
public IXamlType StyledElement { get; }
+ public IXamlType IStyledElement { get; }
public IXamlType NameScope { get; }
public IXamlMethod NameScopeSetNameScope { get; }
public IXamlType INameScope { get; }
@@ -78,6 +79,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType ColumnDefinition { get; }
public IXamlType ColumnDefinitions { get; }
public IXamlType Classes { get; }
+ public IXamlMethod ClassesBindMethod { get; }
+ public IXamlProperty StyledElementClassesProperty { get; set; }
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
@@ -97,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
IBinding, cfg.WellKnownTypes.Object);
UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType");
StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement");
+ IStyledElement = cfg.TypeSystem.GetType("Avalonia.IStyledElement");
INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope");
INameScopeRegister = INameScope.GetMethod(
new FindMethodMethodSignature("Register", XamlIlTypes.Void,
@@ -168,6 +172,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition");
RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions");
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")
+ .FindMethod( "BindClass", IDisposable, false, IStyledElement,
+ cfg.WellKnownTypes.String,
+ IBinding, cfg.WellKnownTypes.Object);
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
index 4f2b580bce..8af638c5d7 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
@@ -377,5 +377,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
public string Greeting1 { get; set; } = "Hello";
public string Greeting2 { get; set; } = "World";
}
+
+ [Fact]
+ public void Binding_Classes_Works()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ // Note, this test also checks `Classes` reordering, so it should be kept AFTER the last single class
+ var xaml = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var button = window.FindControl