diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index f325e6e2d6..9e2962baca 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -21,20 +21,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) : base(configuration, emitMappings, true) { - void InsertAfter(params IXamlAstTransformer[] t) + void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); - void InsertBefore(params IXamlAstTransformer[] t) + void InsertBefore(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); // Before everything else - + Transformers.Insert(0, new XNameTransformer()); Transformers.Insert(1, new IgnoredDirectivesTransformer()); Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); - + // Targeted InsertBefore( new AvaloniaXamlIlResolveClassesPropertiesTransformer(), @@ -48,7 +48,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlControlThemeTransformer(), new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), @@ -57,6 +57,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); + InsertBefore( + new OnPlatformTransformer()); InsertAfter( new XDataTypeTransformer()); @@ -87,14 +89,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions _contextType = CreateContextType(contextTypeBuilder); } - + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings, IXamlType contextType) : this(configuration, emitMappings) { _contextType = contextType; } - + public const string PopulateName = "__AvaloniaXamlIlPopulate"; public const string BuildName = "__AvaloniaXamlIlBuild"; @@ -116,7 +118,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} }); - + var rootObject = (XamlAstObjectNode)parsed.Root; var classDirective = rootObject.Children @@ -131,8 +133,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions false) : TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), (XamlAstXmlTypeReference)rootObject.Type, true); - - + + if (overrideRootType != null) { if (!rootType.Type.IsAssignableFrom(overrideRootType)) @@ -145,7 +147,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transform(parsed); Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); - + } public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/OnPlatformTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/OnPlatformTransformer.cs new file mode 100644 index 0000000000..9aa18b70d2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/OnPlatformTransformer.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; + +internal class OnPlatformTransformer : IXamlAstTransformer +{ + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode xmlobj + && xmlobj.Type is XamlAstXmlTypeReference xmlref + && xmlref.Name.StartsWith("OnPlatform") + && !xmlref.GenericArguments.Any()) + { + IXamlType propertyType = null; + + if (context.ParentNodes().FirstOrDefault() is XamlAstXamlPropertyValueNode parentPropertyValueNode) + { + var property = (XamlAstNamePropertyReference)parentPropertyValueNode.Property; + var declaringType = TypeReferenceResolver.ResolveType(context, (XamlAstXmlTypeReference)property.DeclaringType, context.StrictMode); + propertyType = declaringType.Type.GetAllProperties().First(p => p.Name == property.Name).PropertyType; + } + else if (context.ParentNodes().FirstOrDefault() is XamlAstObjectNode parentNode) + { + var parentType = parentNode.Type is XamlAstClrTypeReference clrType + ? clrType.Type + : TypeReferenceResolver.ResolveType(context, (XamlAstXmlTypeReference)parentNode.Type, context.StrictMode).Type; + var contentProperty = context.Configuration.FindContentProperty(parentType); + propertyType = contentProperty.PropertyType; + } + + if (propertyType is null) + { + throw new InvalidOperationException("Unable to find OnPlatform property type"); + } + + xmlobj.Type = TypeReferenceResolver.ResolveType(context, xmlref.XmlNamespace, xmlref.Name, + xmlref.IsMarkupExtension, new[] { new XamlAstClrTypeReference(xmlref, propertyType, false) }, + xmlref, context.StrictMode); + } + + if (node is XamlAstNamePropertyReference xmlprop + && xmlprop.DeclaringType is XamlAstXmlTypeReference propxmlref + && propxmlref.Name.StartsWith("OnPlatform") + && !propxmlref.GenericArguments.Any()) + { + var expectedType = context.ParentNodes().OfType() + .First(n => n.Type is XamlAstClrTypeReference clrRef && clrRef.Type.Name.StartsWith("OnPlatform") + || n.Type is XamlAstXmlTypeReference xmlRef && xmlRef.Name.StartsWith("OnPlatform")) + .Type; + xmlprop.DeclaringType = expectedType; + xmlprop.TargetType = expectedType; + } + + // if (node is XamlAstObjectNode onobj + // && onobj.Type is XamlAstXmlTypeReference onref + // && onref.Name == "On") + // { + // var platformStr = (onobj.Children.OfType() + // .FirstOrDefault(v => ((XamlAstNamePropertyReference)v.Property).Name == "Platform")?.Values.Single() as XamlAstTextNode)? + // .Text; + // if (string.IsNullOrWhiteSpace(platformStr)) + // { + // throw new InvalidOperationException("On.Platform string must be set"); + // } + // var content = onobj.Children.OfType().FirstOrDefault(); + // if (content is null) + // { + // throw new InvalidOperationException("On content object must be set"); + // } + // + // var parentOnPlatformObject = context.ParentNodes().OfType() + // .First(n => n.Type is XamlAstClrTypeReference clrRef && clrRef.Type.Name.StartsWith("OnPlatform") + // || n.Type is XamlAstXmlTypeReference xmlRef && xmlRef.Name.StartsWith("OnPlatform")); + // parentOnPlatformObject.Children.Remove(onobj); + // foreach (var platform in platformStr.Split(new [] { ',' }, StringSplitOptions.RemoveEmptyEntries)) + // { + // var propertyNode = new XamlAstXamlPropertyValueNode(onobj, + // new XamlAstNamePropertyReference(onobj, parentOnPlatformObject.Type, platform.Trim(), + // parentOnPlatformObject.Type), content); + // parentOnPlatformObject.Children.Add(propertyNode); + // } + // + // return parentOnPlatformObject.Children.Last(); + // } + // if (node is XamlAstXamlPropertyValueNode propNode) + // { + // var type = (propNode.Property as XamlAstNamePropertyReference).TargetType as XamlAstXmlTypeReference; + // + // propNode.VisitChildren(new OnPlatformGenericTypeVisitor(type)); + // + // return node; + // } + + return node; + } + + private class OnPlatformGenericTypeVisitor : IXamlAstVisitor + { + private readonly XamlAstXmlTypeReference _type; + public OnPlatformGenericTypeVisitor(IXamlAstTypeReference type) + { + + } + + public IXamlAstNode Visit(IXamlAstNode node) + { + if (node is XamlAstXmlTypeReference { Name: "OnPlatform" } xmlref) + { + xmlref.GenericArguments.Add(_type); + } + + return node; + } + + public void Push(IXamlAstNode node) + { + } + + public void Pop() + { + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index c1c0594ec2..2c3b53a3db 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d +Subproject commit 2c3b53a3db61018db620418fab509ba1d168ff24 diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs index a9e8e0c994..0dc418a47a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs @@ -1,97 +1,74 @@ #nullable enable using System; -using System.Globalization; -using System.Reflection; -using Avalonia.Data.Converters; +using System.Collections.Generic; +using System.Linq; using Avalonia.Metadata; using Avalonia.Platform; -using Avalonia.Styling; namespace Avalonia.Markup.Xaml.MarkupExtensions; -public class OnPlatformExtension +public class On { - private static readonly object s_unset = new object(); + public string Platform { get; set; } = "Unknown"; + + [Content] + public object? Content { get; set; } +} + +public class OnPlatformExtension : IAddChild +{ + private readonly Dictionary _values = new(); public OnPlatformExtension() { - + } - - public OnPlatformExtension(object defaultValue) + + public OnPlatformExtension(TReturn defaultValue) { Default = defaultValue; } - - [Content] - public object? Default { get; set; } = s_unset; - public object? Windows { get; set; } = s_unset; - public object? macOS { get; set; } = s_unset; - public object? Linux { get; set; } = s_unset; - public object? Android { get; set; } = s_unset; - public object? iOS { get; set; } = s_unset; - public object? Browser { get; set; } = s_unset; - - public IValueConverter? Converter { get; set; } - public object? ConverterParameter { get; set; } + public TReturn? Default { get => _values.TryGetValue(nameof(Default), out var value) ? value : default; set { _values[nameof(Default)] = value; } } + public TReturn? Windows { get => _values.TryGetValue(nameof(Windows), out var value) ? value : default; set { _values[nameof(Windows)] = value; } } + public TReturn? macOS { get => _values.TryGetValue(nameof(macOS), out var value) ? value : default; set { _values[nameof(macOS)] = value; } } + public TReturn? Linux { get => _values.TryGetValue(nameof(Linux), out var value) ? value : default; set { _values[nameof(Linux)] = value; } } + public TReturn? Android { get => _values.TryGetValue(nameof(Android), out var value) ? value : default; set { _values[nameof(Android)] = value; } } + public TReturn? iOS { get => _values.TryGetValue(nameof(iOS), out var value) ? value : default; set { _values[nameof(iOS)] = value; } } + public TReturn? Browser { get => _values.TryGetValue(nameof(Browser), out var value) ? value : default; set { _values[nameof(Browser)] = value; } } - public object? ProvideValue(IServiceProvider serviceProvider) + public object? ProvideValue() { - if (Default == s_unset - && Windows == s_unset - && macOS == s_unset - && Linux == s_unset - && Android == s_unset - && iOS == s_unset - && Browser == s_unset) + if (!_values.Any()) { throw new InvalidOperationException("OnPlatformExtension requires a value to be specified for at least one platform or Default."); } - var provideTarget = serviceProvider.GetService(); - - var targetType = provideTarget.TargetProperty switch - { - AvaloniaProperty ap => ap.PropertyType, - PropertyInfo pi => pi.PropertyType, - _ => null, - }; - - if (provideTarget.TargetObject is Setter setter) - { - targetType = setter.Property?.PropertyType ?? targetType; - } - - if (!TryGetValueForPlatform(out var value)) - { - return AvaloniaProperty.UnsetValue; - } - - if (targetType is null) - { - return value; - } - - var converter = Converter ?? DefaultValueConverter.Instance; - return converter.Convert(value, targetType, ConverterParameter, CultureInfo.CurrentUICulture); + var (value, hasValue) = TryGetValueForPlatform(); + return !hasValue ? AvaloniaProperty.UnsetValue : value; } - private bool TryGetValueForPlatform(out object? value) + private (TReturn? value, bool hasValue) TryGetValueForPlatform() { var runtimeInfo = AvaloniaLocator.Current.GetRequiredService().GetRuntimeInfo(); - value = runtimeInfo.OperatingSystem switch + return runtimeInfo.OperatingSystem switch { - OperatingSystemType.WinNT when Windows != s_unset => Windows, - OperatingSystemType.Linux when Linux != s_unset => Linux, - OperatingSystemType.OSX when macOS != s_unset => macOS, - OperatingSystemType.Android when Android != s_unset => Android, - OperatingSystemType.iOS when iOS != s_unset => iOS, - OperatingSystemType.Browser when Browser != s_unset => Browser, - _ => Default + OperatingSystemType.WinNT => _values.TryGetValue(nameof(Windows), out var val) ? (val, true) : default, + OperatingSystemType.OSX => _values.TryGetValue(nameof(macOS), out var val) ? (val, true) : default, + OperatingSystemType.Linux => _values.TryGetValue(nameof(Linux), out var val) ? (val, true) : default, + OperatingSystemType.Android => _values.TryGetValue(nameof(Android), out var val) ? (val, true) : default, + OperatingSystemType.iOS => _values.TryGetValue(nameof(iOS), out var val) ? (val, true) : default, + OperatingSystemType.Browser => _values.TryGetValue(nameof(Browser), out var val) ? (val, true) : default, + _ => _values.TryGetValue(nameof(Default), out var val) ? (val, true) : default }; + } - return value != s_unset; + public void AddChild(On child) + { + foreach (var platform in child.Platform.Split(new [] { "," }, StringSplitOptions.RemoveEmptyEntries)) + { + _values[platform.Trim()] = (TReturn?)child.Content; + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs index 0583de691f..615d08f4f8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs @@ -4,7 +4,6 @@ using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Media; using Avalonia.Platform; -using Avalonia.PlatformSupport; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; @@ -18,7 +17,7 @@ public class OnPlatformExtensionTests : XamlTestBase { AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown)); - + var xaml = @" @@ -31,7 +30,7 @@ public class OnPlatformExtensionTests : XamlTestBase Assert.Equal("Hello World", textBlock.Text); } } - + [Fact] public void Should_Resolve_Default_Value_From_Ctor() { @@ -39,7 +38,7 @@ public class OnPlatformExtensionTests : XamlTestBase { AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown)); - + var xaml = @" @@ -52,7 +51,7 @@ public class OnPlatformExtensionTests : XamlTestBase Assert.Equal("Hello World", textBlock.Text); } } - + [Theory] [InlineData(OperatingSystemType.WinNT, "Im Windows")] [InlineData(OperatingSystemType.OSX, "Im macOS")] @@ -67,7 +66,7 @@ public class OnPlatformExtensionTests : XamlTestBase { AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestRuntimePlatform(currentPlatform)); - + var xaml = @" @@ -91,7 +90,7 @@ public class OnPlatformExtensionTests : XamlTestBase { AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT)); - + var xaml = @" @@ -104,7 +103,7 @@ public class OnPlatformExtensionTests : XamlTestBase Assert.Equal(50.1, border.Height); } } - + [Fact] public void Should_Convert_Avalonia_Type() { @@ -112,7 +111,7 @@ public class OnPlatformExtensionTests : XamlTestBase { AvaloniaLocator.CurrentMutable.Bind() .ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT)); - + var xaml = @" @@ -126,6 +125,27 @@ public class OnPlatformExtensionTests : XamlTestBase } } + [Fact] + public void Should_Respect_Custom_TypeArgument() + { + using (AvaloniaLocator.EnterScope()) + { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT)); + + var xaml = @" + + +"; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var textBlock = (TextBlock)userControl.Content!; + + Assert.Equal(new Thickness(10, 10, 10, 10), textBlock.DataContext); + } + } + [Fact] public void Should_Allow_Nester_Markup_Extensions() { @@ -149,9 +169,9 @@ public class OnPlatformExtensionTests : XamlTestBase Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color); } } - + [Fact] - public void Should_Use_Converter_If_Provided() + public void Should_Support_Xml_Syntax() { using (AvaloniaLocator.EnterScope()) { @@ -160,23 +180,27 @@ public class OnPlatformExtensionTests : XamlTestBase var xaml = @" - - - - + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> + + + + + + + + + "; var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); var border = (Border)userControl.Content!; - Assert.Equal(new Thickness(4), border.Padding); + Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color); } } - + [Fact] - public void Should_Support_Xml_Syntax() + public void Should_Support_Xml_Syntax_With_Custom_TypeArguments() { using (AvaloniaLocator.EnterScope()) { @@ -187,11 +211,39 @@ public class OnPlatformExtensionTests : XamlTestBase + + + + +"; + + var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var border = (Border)userControl.Content!; + + Assert.Equal(new Thickness(10, 10, 10, 10), border.Tag); + } + } + + [Fact] + public void Should_Support_Special_On_Syntax() + { + using (AvaloniaLocator.EnterScope()) + { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new TestRuntimePlatform(OperatingSystemType.OSX)); + + var xaml = @" + + - + - + + + + @@ -203,7 +255,7 @@ public class OnPlatformExtensionTests : XamlTestBase Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color); } } - + [Fact] public void Should_Support_Control_Inside_Xml_Syntax() { @@ -232,12 +284,12 @@ public class OnPlatformExtensionTests : XamlTestBase private class TestRuntimePlatform : StandardRuntimePlatform { private readonly OperatingSystemType _operatingSystemType; - + public TestRuntimePlatform(OperatingSystemType operatingSystemType) { _operatingSystemType = operatingSystemType; } - + public override RuntimePlatformInfo GetRuntimeInfo() { return new RuntimePlatformInfo() { OperatingSystem = _operatingSystemType };