From 5bfbfb97d3689acd353136b05136f8d825010ef6 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Sun, 14 Feb 2021 11:36:06 +0300 Subject: [PATCH 1/7] Use ControlCatalog.NetCore as an artifact instead of ControlCatalog.Desktop --- nukebuild/Build.cs | 17 +++++++++++------ nukebuild/BuildParameters.cs | 7 ++----- .../ControlCatalog.NetCore.csproj | 6 +++++- samples/ControlCatalog/ControlCatalog.csproj | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 8e331edab4..2c6ec70bd6 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -305,14 +305,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 @@ - + From d7f3c24365a9cb73649363892ea0296e89b1a9c1 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 13:24:20 +0100 Subject: [PATCH 2/7] Allow for controlling behavior of CaptionButtons. --- .../Chrome/CaptionButtons.cs | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index cd60130c5b..d41a95b5a0 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -14,17 +14,21 @@ namespace Avalonia.Controls.Chrome public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; - private Window? _hostWindow; - public void Attach(Window hostWindow) + /// + /// Currently attached window. + /// + protected Window? HostWindow { get; private set; } + + public virtual void Attach(Window hostWindow) { if (_disposables == null) { - _hostWindow = hostWindow; + HostWindow = hostWindow; _disposables = new CompositeDisposable { - _hostWindow.GetObservable(Window.WindowStateProperty) + HostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { PseudoClasses.Set(":minimized", x == WindowState.Minimized); @@ -36,14 +40,45 @@ namespace Avalonia.Controls.Chrome } } - public void Detach() + public virtual void Detach() { if (_disposables != null) { _disposables.Dispose(); _disposables = null; - _hostWindow = null; + HostWindow = null; + } + } + + protected virtual void OnClose() + { + HostWindow?.Close(); + } + + protected virtual void OnRestore() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + protected virtual void OnMinimize() + { + if (HostWindow != null) + { + HostWindow.WindowState = WindowState.Minimized; + } + } + + private void OnToggleFullScreen() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen + ? WindowState.Normal + : WindowState.FullScreen; } } @@ -56,31 +91,13 @@ namespace Avalonia.Controls.Chrome var minimiseButton = e.NameScope.Get("PART_MinimiseButton"); var fullScreenButton = e.NameScope.Get("PART_FullScreenButton"); - closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); + closeButton.PointerReleased += (sender, e) => OnClose(); - restoreButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - }; + restoreButton.PointerReleased += (sender, e) => OnRestore(); - minimiseButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = WindowState.Minimized; - } - }; + minimiseButton.PointerReleased += (sender, e) => OnMinimize(); - fullScreenButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; - } - }; + fullScreenButton.PointerReleased += (sender, e) => OnToggleFullScreen(); } } } From 8a79141c24647376e392790607170ba3b4a31aa5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 25 Feb 2021 14:56:51 +0100 Subject: [PATCH 3/7] Fix access modifer. --- src/Avalonia.Controls/Chrome/CaptionButtons.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index d41a95b5a0..be15d3d444 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -72,7 +72,7 @@ namespace Avalonia.Controls.Chrome } } - private void OnToggleFullScreen() + protected virtual void OnToggleFullScreen() { if (HostWindow != null) { From d1f44dcdf4d89abf20bba753494a3ac23728fd5e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 16 Mar 2021 11:57:59 +0100 Subject: [PATCH 4/7] Added DevToolsOptions. With options to: - Set the key gesture - Show as child window - Set the initial size Co-Authored-By: workgroupengineering --- .../DevToolsExtensions.cs | 12 ++++++++- .../Diagnostics/DevTools.cs | 24 ++++++++++++----- .../Diagnostics/DevToolsOptions.cs | 26 +++++++++++++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs index 4bc2ca313f..a432c94a5d 100644 --- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -15,7 +15,7 @@ namespace Avalonia /// The window to attach DevTools to. public static void AttachDevTools(this TopLevel root) { - DevTools.Attach(root, new KeyGesture(Key.F12)); + DevTools.Attach(root, new DevToolsOptions()); } /// @@ -27,5 +27,15 @@ namespace Avalonia { DevTools.Attach(root, gesture); } + + /// + /// Attaches DevTools to a window, to be opened with the specified options. + /// + /// The window to attach DevTools to. + /// additional settint of DevTools + public static void AttachDevTools(this TopLevel root, DevToolsOptions options) + { + DevTools.Attach(root, options); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 4899be2955..7942d22962 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -6,6 +6,8 @@ using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Interactivity; +#nullable enable + namespace Avalonia.Diagnostics { public static class DevTools @@ -13,12 +15,20 @@ namespace Avalonia.Diagnostics private static readonly Dictionary s_open = new Dictionary(); public static IDisposable Attach(TopLevel root, KeyGesture gesture) + { + return Attach(root, new DevToolsOptions() + { + Gesture = gesture, + }); + } + + public static IDisposable Attach(TopLevel root, DevToolsOptions options) { void PreviewKeyDown(object sender, KeyEventArgs e) { - if (gesture.Matches(e)) + if (options.Gesture.Matches(e)) { - Open(root); + Open(root, options); } } @@ -28,7 +38,9 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - public static IDisposable Open(TopLevel root) + public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); + + public static IDisposable Open(TopLevel root, DevToolsOptions options) { if (s_open.TryGetValue(root, out var window)) { @@ -38,15 +50,15 @@ namespace Avalonia.Diagnostics { window = new MainWindow { - Width = 1024, - Height = 512, Root = root, + Width = options.Size.Width, + Height = options.Size.Height, }; window.Closed += DevToolsClosed; s_open.Add(root, window); - if (root is Window inspectedWindow) + if (options.ShowAsChildWindow && root is Window inspectedWindow) { window.Show(inspectedWindow); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs new file mode 100644 index 0000000000..ee46192207 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -0,0 +1,26 @@ +using Avalonia.Input; + +namespace Avalonia.Diagnostics +{ + /// + /// Describes options used to customize DevTools. + /// + public class DevToolsOptions + { + /// + /// Gets or sets the key gesture used to open DevTools. + /// + public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12); + + /// + /// Gets or sets a value indicating whether DevTools should be displayed as a child window + /// of the window being inspected. The default value is true. + /// + public bool ShowAsChildWindow { get; set; } = true; + + /// + /// Gets or sets the initial size of the DevTools window. The default value is 1024x512. + /// + public Size Size { get; set; } = new Size(1024, 512); + } +} From 61792ce45faa422f328c6a545304a9b1c93d4982 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Mar 2021 18:30:22 +0300 Subject: [PATCH 5/7] Allow binding to classes with Classes.ClassName --- src/Avalonia.Styling/ClassBindingManager.cs | 38 ++++++++ src/Avalonia.Styling/Controls/Classes.cs | 21 ++++ .../StyledElementExtensions.cs | 11 +++ .../AvaloniaXamlIlCompiler.cs | 5 +- .../AvaloniaXamlIlClassesPropertyResolver.cs | 97 +++++++++++++++++++ ...mlIlReorderClassesPropertiesTransformer.cs | 40 ++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 10 ++ .../Xaml/BindingTests.cs | 32 ++++++ 8 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Styling/ClassBindingManager.cs create mode 100644 src/Avalonia.Styling/StyledElementExtensions.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Styling/ClassBindingManager.cs new file mode 100644 index 0000000000..6ddea934bb --- /dev/null +++ b/src/Avalonia.Styling/ClassBindingManager.cs @@ -0,0 +1,38 @@ +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 enable = args.NewValue.GetValueOrDefault(); + var classes = ((IStyledElement)args.Sender).Classes; + if (enable) + { + if (!classes.Contains(className)) + classes.Add(className); + } + else + classes.Remove(className); + }); + + 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 = @" + + /// The window to attach DevTools to. - /// additional settint of DevTools + /// Additional settings of DevTools. public static void AttachDevTools(this TopLevel root, DevToolsOptions options) { DevTools.Attach(root, options);