Browse Source

Merge branch 'master' into jmacato/missing-skia-api-lottie

pull/5683/head
Jumar Macato 6 years ago
committed by GitHub
parent
commit
6e3bcd265d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      nukebuild/Build.cs
  2. 7
      nukebuild/BuildParameters.cs
  3. 2
      readme.md
  4. 6
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  5. 2
      samples/ControlCatalog/ControlCatalog.csproj
  6. 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  7. 73
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  8. 22
      src/Avalonia.Controls/NativeControlHost.cs
  9. 2
      src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs
  10. 12
      src/Avalonia.Diagnostics/DevToolsExtensions.cs
  11. 24
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  12. 26
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  13. 2
      src/Avalonia.Native/avn.idl
  14. 31
      src/Avalonia.Styling/ClassBindingManager.cs
  15. 21
      src/Avalonia.Styling/Controls/Classes.cs
  16. 11
      src/Avalonia.Styling/StyledElementExtensions.cs
  17. 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  18. 97
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs
  19. 40
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs
  20. 10
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  21. 32
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

17
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 => _ => _

7
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()

2
readme.md

@ -2,8 +2,6 @@
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
<img alt="Avalonia" src="https://user-images.githubusercontent.com/6759207/84897744-cab6d800-b0ae-11ea-8214-e5174d71f5c8.png" width="400"/>
## 📖 About AvaloniaUI
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.

6
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>
@ -15,6 +15,10 @@
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
</ItemGroup>
<PropertyGroup>
<!-- For Microsoft.CodeAnalysis -->
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />

2
samples/ControlCatalog/ControlCatalog.csproj

@ -27,6 +27,6 @@
<ProjectReference Include="..\..\src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj" />
<ProjectReference Include="..\MiniMvvm\MiniMvvm.csproj" />
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

1
src/Avalonia.Controls/ApiCompatBaseline.txt

@ -3,6 +3,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Control
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract.
MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.

73
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)
/// <summary>
/// Currently attached window.
/// </summary>
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;
}
}
protected virtual 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<Panel>("PART_MinimiseButton");
var fullScreenButton = e.NameScope.Get<Panel>("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();
}
}
}

22
src/Avalonia.Controls/NativeControlHost.cs

@ -16,30 +16,16 @@ namespace Avalonia.Controls
private bool _queuedForDestruction;
private bool _queuedForMoveResize;
private readonly List<Visual> _propertyChangedSubscriptions = new List<Visual>();
private readonly EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChangedHandler;
static NativeControlHost()
{
IsVisibleProperty.Changed.AddClassHandler<NativeControlHost>(OnVisibleChanged);
}
public NativeControlHost()
{
_propertyChangedHandler = PropertyChangedHandler;
}
private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2)
=> host.UpdateHost();
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
_currentRoot = e.Root as TopLevel;
var visual = (IVisual)this;
while (visual != _currentRoot)
while (visual != null)
{
if (visual is Visual v)
{
v.PropertyChanged += _propertyChangedHandler;
v.PropertyChanged += PropertyChangedHandler;
_propertyChangedSubscriptions.Add(v);
}
@ -51,7 +37,7 @@ namespace Avalonia.Controls
private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.IsEffectiveValueChange && e.Property == BoundsProperty)
if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty))
EnqueueForMoveResize();
}
@ -61,7 +47,7 @@ namespace Avalonia.Controls
if (_propertyChangedSubscriptions != null)
{
foreach (var v in _propertyChangedSubscriptions)
v.PropertyChanged -= _propertyChangedHandler;
v.PropertyChanged -= PropertyChangedHandler;
_propertyChangedSubscriptions.Clear();
}
UpdateHost();

2
src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs

@ -16,7 +16,7 @@ namespace Avalonia.Platform
/// <summary>
/// The default for the platform.
/// </summary>
Default = SystemChrome,
Default = PreferSystemChrome,
/// <summary>
/// Use SystemChrome

12
src/Avalonia.Diagnostics/DevToolsExtensions.cs

@ -15,7 +15,7 @@ namespace Avalonia
/// <param name="root">The window to attach DevTools to.</param>
public static void AttachDevTools(this TopLevel root)
{
DevTools.Attach(root, new KeyGesture(Key.F12));
DevTools.Attach(root, new DevToolsOptions());
}
/// <summary>
@ -27,5 +27,15 @@ namespace Avalonia
{
DevTools.Attach(root, gesture);
}
/// <summary>
/// Attaches DevTools to a window, to be opened with the specified options.
/// </summary>
/// <param name="root">The window to attach DevTools to.</param>
/// <param name="options">Additional settings of DevTools.</param>
public static void AttachDevTools(this TopLevel root, DevToolsOptions options)
{
DevTools.Attach(root, options);
}
}
}

24
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<TopLevel, Window> s_open = new Dictionary<TopLevel, Window>();
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);
}

26
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -0,0 +1,26 @@
using Avalonia.Input;
namespace Avalonia.Diagnostics
{
/// <summary>
/// Describes options used to customize DevTools.
/// </summary>
public class DevToolsOptions
{
/// <summary>
/// Gets or sets the key gesture used to open DevTools.
/// </summary>
public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12);
/// <summary>
/// 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.
/// </summary>
public bool ShowAsChildWindow { get; set; } = true;
/// <summary>
/// Gets or sets the initial size of the DevTools window. The default value is 1024x512.
/// </summary>
public Size Size { get; set; } = new Size(1024, 512);
}
}

2
src/Avalonia.Native/avn.idl

@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints
AvnSystemChrome = 0x01,
AvnPreferSystemChrome = 0x02,
AvnOSXThickTitleBar = 0x08,
AvnDefaultChrome = AvnSystemChrome,
AvnDefaultChrome = AvnPreferSystemChrome,
}
[uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]

31
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<string, AvaloniaProperty> s_RegisteredProperties =
new Dictionary<string, AvaloniaProperty>();
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<StyledElement, bool>("__AvaloniaReserved::Classes::" + className);
prop.Changed.Subscribe(args =>
{
var classes = ((IStyledElement)args.Sender).Classes;
classes.Set(className, args.NewValue.GetValueOrDefault());
});
return prop;
}
}
}

21
src/Avalonia.Styling/Controls/Classes.cs

@ -265,5 +265,26 @@ namespace Avalonia.Controls
$"The pseudoclass '{name}' may only be {operation} by the control itself.");
}
}
/// <summary>
/// Adds a or removes a style class to/from the collection.
/// </summary>
/// <param name="name">The class names.</param>
/// <param name="value">If true adds the class, if false, removes it.</param>
/// <remarks>
/// Only standard classes may be added or removed via this method. To add pseudoclasses (classes
/// beginning with a ':' character) use the protected <see cref="StyledElement.PseudoClasses"/>
/// property.
/// </remarks>
public void Set(string name, bool value)
{
if (value)
{
if (!Contains(name))
Add(name);
}
else
Remove(name);
}
}
}

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

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

@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
// Targeted
InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
new AvaloniaXamlIlTransformInstanceAttachedProperties(),
new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers());
InsertAfter<PropertyReferenceResolver>(
new AvaloniaXamlIlAvaloniaPropertyResolver());
new AvaloniaXamlIlAvaloniaPropertyResolver(),
new AvaloniaXamlIlReorderClassesPropertiesTransformer()
);
InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlBindingPathParser(),

97
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<IXamlILEmitter>
{
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<IXamlType> Parameters { get; }
}
class ClassBindingSetter : IXamlEmitablePropertySetter<IXamlILEmitter>
{
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<IXamlType> Parameters { get; }
}
}
}

40
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;
}
}
}

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

32
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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button' Classes.MyClass='{Binding Foo}' Classes.MySecondClass='True' Classes='foo bar'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = window.FindControl<Button>("button");
button.DataContext = new { Foo = true };
window.ApplyTemplate();
Assert.True(button.Classes.Contains("MyClass"));
Assert.True(button.Classes.Contains("MySecondClass"));
Assert.True(button.Classes.Contains("foo"));
Assert.True(button.Classes.Contains("bar"));
button.DataContext = new { Foo = false };
Assert.False(button.Classes.Contains("MyClass"));
Assert.True(button.Classes.Contains("MySecondClass"));
Assert.True(button.Classes.Contains("foo"));
Assert.True(button.Classes.Contains("bar"));
}
}
}
}

Loading…
Cancel
Save