Browse Source

Merge branch 'master' into features/micro-optimization

pull/9415/head
Giuseppe Lippolis 4 years ago
parent
commit
8e791848d2
  1. 8
      samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
  2. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  3. 9
      samples/ControlCatalog/MainView.xaml
  4. 51
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml
  5. 20
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
  6. 54
      samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
  7. 22
      src/Avalonia.Base/Metadata/MarkupExtensionOption.cs
  8. 13
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  9. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  10. 20
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  11. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  12. 48
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  13. 403
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
  14. 11
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  15. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  16. 3
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  17. 15
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs
  18. 60
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs
  19. 73
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs
  20. 22
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  21. 26
      src/Web/Avalonia.Web/BrowserRuntimePlatform.cs
  22. 2
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  23. 5
      src/Web/Avalonia.Web/Interop/AvaloniaModule.cs
  24. 8
      src/Web/Avalonia.Web/WindowingPlatform.cs
  25. 18
      src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts
  26. 25
      tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs
  27. 5
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  28. 71
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs
  29. 75
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs
  30. 605
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs

8
samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json

@ -8,14 +8,6 @@
}
},
"profiles": {
"ControlCatalog.Web - IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"ControlCatalog.Web": {
"commandName": "Project",
"dotnetRunMessages": "true",

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeFrameworkVersion>6.0.9</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion>6.0.8</RuntimeFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">

9
samples/ControlCatalog/MainView.xaml

@ -12,8 +12,8 @@
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
<TabItem Header="Acrylic">
@ -118,6 +118,9 @@
<TabItem Header="OpenGL">
<pages:OpenGlPage />
</TabItem>
<TabItem Header="Platform Information">
<pages:PlatformInfoPage />
</TabItem>
<TabItem Header="Pointers">
<pages:PointersPage />
</TabItem>
@ -130,7 +133,7 @@
<TabItem Header="RelativePanel">
<pages:RelativePanelPage />
</TabItem>
<TabItem Header="ScrollViewer">
<TabItem Header="ScrollViewer">
<pages:ScrollViewerPage />
</TabItem>
<TabItem Header="Slider">

51
samples/ControlCatalog/Pages/PlatformInfoPage.xaml

@ -0,0 +1,51 @@
<UserControl x:Class="ControlCatalog.Pages.PlatformInfoPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="800"
d:DesignWidth="400"
mc:Ignorable="d">
<StackPanel Spacing="20">
<TextBlock Text="{Binding PlatformInfo}" />
<StackPanel TextElement.Foreground="White">
<StackPanel Orientation="Horizontal">
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Desktop=Green}">
<TextBlock Text="Desktop" />
</Border>
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Mobile=Green}">
<TextBlock Text="Mobile" />
</Border>
</StackPanel>
<WrapPanel>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Windows=Green}">
<TextBlock Text="Windows" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, macOS=Green}">
<TextBlock Text="macOS" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Linux=Green}">
<TextBlock Text="Linux" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Browser=Green}">
<TextBlock Text="Browser" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, iOS=Green}">
<TextBlock Text="iOS" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Android=Green}">
<TextBlock Text="Android" />
</Border>
<Border Height="100" Width="100">
<Border.Background>
<OnPlatform Default="Gray" >
<On Options="macOS, Linux, Windows" Content="Green" />
</OnPlatform>
</Border.Background>
<TextBlock Text="Win, Lin or Mac" />
</Border>
</WrapPanel>
</StackPanel>
</StackPanel>
</UserControl>

20
samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs

@ -0,0 +1,20 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class PlatformInfoPage : UserControl
{
public PlatformInfoPage()
{
this.InitializeComponent();
DataContext = new PlatformInformationViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

54
samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs

@ -0,0 +1,54 @@
using Avalonia;
using Avalonia.Platform;
using MiniMvvm;
namespace ControlCatalog.ViewModels;
#nullable enable
public class PlatformInformationViewModel : ViewModelBase
{
public PlatformInformationViewModel()
{
var runtimeInfo = AvaloniaLocator.Current.GetService<IRuntimePlatform>()?.GetRuntimeInfo();
if (runtimeInfo is { } info)
{
if (info.IsBrowser)
{
if (info.IsDesktop)
{
PlatformInfo = "Platform: Desktop (browser)";
}
else if (info.IsMobile)
{
PlatformInfo = "Platform: Mobile (browser)";
}
else
{
PlatformInfo = "Platform: Unknown (browser) - please report";
}
}
else
{
if (info.IsDesktop)
{
PlatformInfo = "Platform: Desktop (native)";
}
else if (info.IsMobile)
{
PlatformInfo = "Platform: Mobile (native)";
}
else
{
PlatformInfo = "Platform: Unknown (native) - please report";
}
}
}
else
{
}
}
public string PlatformInfo { get; }
}

22
src/Avalonia.Base/Metadata/MarkupExtensionOption.cs

@ -0,0 +1,22 @@
using System;
namespace Avalonia.Metadata;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class MarkupExtensionOptionAttribute : Attribute
{
public MarkupExtensionOptionAttribute(object value)
{
Value = value;
}
public object Value { get; }
public int Priority { get; set; } = 0;
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class MarkupExtensionDefaultOptionAttribute : Attribute
{
}

13
src/Avalonia.Base/Platform/IRuntimePlatform.cs

@ -17,13 +17,16 @@ namespace Avalonia.Platform
IntPtr Address { get; }
int Size { get; }
bool IsDisposed { get; }
}
[Unstable]
public struct RuntimePlatformInfo
{
public OperatingSystemType OperatingSystem { get; set; }
public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop :
IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown;
public bool IsDesktop { get; set; }
public bool IsMobile { get; set; }
public bool IsBrowser { get; set; }
@ -44,4 +47,12 @@ namespace Avalonia.Platform
iOS,
Browser
}
[Unstable]
public enum FormFactorType
{
Unknown,
Desktop,
Mobile
}
}

1
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs

@ -198,6 +198,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType);
var created = tb.CreateTypeInfo();
clrPropertyBuilder.CreateTypeInfo();
indexerClosureType.CreateTypeInfo();
trampolineBuilder.CreateTypeInfo();
return LoadOrPopulate(created, rootInstance);

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

@ -22,10 +22,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings)
: base(configuration, emitMappings, true)
{
void InsertAfter<T>(params IXamlAstTransformer[] t)
void InsertAfter<T>(params IXamlAstTransformer[] t)
=> Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t);
void InsertBefore<T>(params IXamlAstTransformer[] t)
void InsertBefore<T>(params IXamlAstTransformer[] t)
=> Transformers.InsertRange(Transformers.FindIndex(x => x is T), t);
@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
Transformers.Insert(1, new IgnoredDirectivesTransformer());
Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
// Targeted
InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@ -58,6 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
InsertBefore<ConvertPropertyValuesToAssignmentsTransformer>(
new AvaloniaXamlIlOptionMarkupExtensionTransformer());
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());
@ -89,14 +91,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
_contextType = CreateContextType(contextTypeBuilder);
}
public AvaloniaXamlIlCompiler(TransformerConfiguration configuration,
XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings,
IXamlType contextType) : this(configuration, emitMappings)
{
_contextType = contextType;
}
public const string PopulateName = "__AvaloniaXamlIlPopulate";
public const string BuildName = "__AvaloniaXamlIlBuild";
@ -118,7 +120,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
var rootObject = (XamlAstObjectNode)parsed.Root;
var classDirective = rootObject.Children
@ -133,8 +135,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))
@ -147,7 +149,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)

9
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -185,6 +185,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public static bool CustomValueConverter(AstTransformationContext context,
IXamlAstValueNode node, IXamlType type, out IXamlAstValueNode result)
{
if (node is AvaloniaXamlIlOptionMarkupExtensionTransformer.OptionsMarkupExtensionNode optionsNode)
{
if (optionsNode.ConvertToReturnType(context, type, out var newOptionsNode))
{
result = newOptionsNode;
return true;
}
}
if (!(node is XamlAstTextNode textNode))
{
result = null;

48
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@ -48,10 +48,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var thickness = Thickness.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Thickness, types.ThicknessFullConstructor,
new[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom });
return true;
}
catch
@ -65,10 +65,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var point = Point.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Point, types.PointFullConstructor,
new[] { point.X, point.Y });
return true;
}
catch
@ -76,16 +76,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a point", node);
}
}
if (type.Equals(types.Vector))
{
try
{
var vector = Vector.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Vector, types.VectorFullConstructor,
new[] { vector.X, vector.Y });
return true;
}
catch
@ -93,16 +93,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a vector", node);
}
}
if (type.Equals(types.Size))
{
try
{
var size = Size.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Size, types.SizeFullConstructor,
new[] { size.Width, size.Height });
return true;
}
catch
@ -110,16 +110,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a size", node);
}
}
if (type.Equals(types.Matrix))
{
try
{
var matrix = Matrix.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Matrix, types.MatrixFullConstructor,
new[] { matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32 });
return true;
}
catch
@ -127,16 +127,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a matrix", node);
}
}
if (type.Equals(types.CornerRadius))
{
try
{
var cornerRadius = CornerRadius.Parse(text);
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.CornerRadius, types.CornerRadiusFullConstructor,
new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft });
return true;
}
catch
@ -144,7 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a corner radius", node);
}
}
if (type.Equals(types.Color))
{
if (!Color.TryParse(text, out Color color))
@ -165,9 +165,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var relativePoint = RelativePoint.Parse(text);
var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false);
result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List<IXamlAstValueNode>
{
new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X),
@ -188,9 +188,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var gridLength = GridLength.Parse(text);
result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength);
return true;
}
catch
@ -201,12 +201,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
if (type.Equals(types.Cursor))
{
if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, out var enumConstantNode))
if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, false, out var enumConstantNode))
{
var cursorTypeRef = new XamlAstClrTypeReference(node, types.Cursor, false);
result = new XamlAstNewClrObjectNode(node, cursorTypeRef, types.CursorTypeConstructor, new List<IXamlAstValueNode> { enumConstantNode });
return true;
}
}
@ -296,7 +296,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
private static bool ConvertDefinitionList(
IXamlAstValueNode node,
IXamlAstValueNode node,
string text,
AvaloniaXamlIlWellKnownTypes types,
IXamlType listType,

403
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs

@ -0,0 +1,403 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Emit;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
internal class AvaloniaXamlIlOptionMarkupExtensionTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlMarkupExtensionNode
{
Value: XamlAstObjectNode { Type: XamlAstClrTypeReference { Type: { } type } } objectNode
} markupExtensionNode
&& type.FindMethods(m => m.IsPublic && m.Parameters.Count is 1 or 2 && m.ReturnType == context.Configuration.WellKnownTypes.Boolean && m.Name == "ShouldProvideOption").ToArray() is { } methods
&& methods.Any())
{
var optionAttribute = context.GetAvaloniaTypes().MarkupExtensionOptionAttribute;
var defaultOptionAttribute = context.GetAvaloniaTypes().MarkupExtensionDefaultOptionAttribute;
var typeArgument = type.GenericArguments?.FirstOrDefault();
IXamlAstValueNode defaultValue = null;
var values = new List<OptionsMarkupExtensionBranch>();
if (objectNode.Arguments.FirstOrDefault() is { } argument)
{
var hasDefaultProp = objectNode.Type.GetClrType().GetAllProperties().Any(p =>
p.CustomAttributes.Any(a => a.Type == defaultOptionAttribute));
if (hasDefaultProp)
{
if (objectNode.Arguments.Count > 1)
{
throw new XamlParseException("Options MarkupExtensions allow only single argument", objectNode);
}
defaultValue = TransformNode(new[] { argument }, typeArgument, objectNode);
objectNode.Arguments.Remove(argument);
}
}
foreach (var extProp in objectNode.Children.OfType<XamlAstXamlPropertyValueNode>().ToArray())
{
if (!extProp.Values.Any())
{
continue;
}
var shouldRemoveProp = false;
var onObjs = extProp.Values.OfType<XamlAstObjectNode>()
.Where(o => o.Type.GetClrType() == context.GetAvaloniaTypes().OnExtensionType).ToArray();
if (onObjs.Any())
{
shouldRemoveProp = true;
foreach (var onObj in onObjs)
{
var optionsPropNode = onObj.Children.OfType<XamlAstXamlPropertyValueNode>()
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Options")
?.Values.Single();
var options = (optionsPropNode as XamlAstTextNode)?.Text?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
?? Array.Empty<string>();
if (options.Length == 0)
{
throw new XamlParseException("On.Options string must be set", onObj);
}
var content = onObj.Children.OfType<XamlAstXamlPropertyValueNode>()
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Content");
if (content is null)
{
throw new XamlParseException("On content object must be set", onObj);
}
var propertiesSet = options
.Select(o => type.GetAllProperties()
.FirstOrDefault(p => o.Equals(p.Name, StringComparison.Ordinal))
?? throw new XamlParseException($"Property \"{o}\" wasn't found on the \"{type.Name}\" type", onObj))
.ToArray();
foreach (var propertySet in propertiesSet)
{
AddBranchNode(content.Values, propertySet.CustomAttributes, content);
}
}
}
else
{
shouldRemoveProp = AddBranchNode(extProp.Values, extProp.Property.GetClrProperty().CustomAttributes, extProp);
}
if (shouldRemoveProp)
{
objectNode.Children.Remove(extProp);
}
}
if (defaultValue is null && !values.Any())
{
throw new XamlParseException("Options markup extension requires at least one option to be set", objectNode);
}
return new OptionsMarkupExtensionNode(
markupExtensionNode, values.ToArray(), defaultValue,
context.Configuration.TypeMappings.ServiceProvider);
bool AddBranchNode(
IReadOnlyCollection<IXamlAstValueNode> valueNodes,
IReadOnlyCollection<IXamlCustomAttribute> propAttributes,
IXamlLineInfo li)
{
var transformed = TransformNode(valueNodes, typeArgument, li);
if (propAttributes.FirstOrDefault(a => a.Type == defaultOptionAttribute) is { } defAttr)
{
defaultValue = transformed;
return true;
}
else if (propAttributes.FirstOrDefault(a => a.Type == optionAttribute) is { } optAttr)
{
var option = optAttr.Parameters.Single();
if (option is null)
{
throw new XamlParseException("MarkupExtension option must not be null", li);
}
var optionAsString = option.ToString();
IXamlAstValueNode optionNode = null;
foreach (var method in methods)
{
try
{
var targetType = method.Parameters.Last();
if (targetType.FullName == "System.Type")
{
if (option is IXamlType typeOption)
{
optionNode = new XamlTypeExtensionNode(li,
new XamlAstClrTypeReference(li, typeOption, false), targetType);
}
}
else if (targetType == context.Configuration.WellKnownTypes.String)
{
optionNode = new XamlConstantNode(li, targetType, optionAsString);
}
else if (targetType.IsEnum)
{
if (TypeSystemHelpers.TryGetEnumValueNode(targetType, optionAsString, li, false,
out var enumConstantNode))
{
optionNode = enumConstantNode;
}
}
else if (TypeSystemHelpers.ParseConstantIfTypeAllows(optionAsString, targetType, li,
out var constantNode))
{
optionNode = constantNode;
}
}
catch (FormatException)
{
// try next method overload
}
if (optionNode is not null)
{
values.Add(new OptionsMarkupExtensionBranch(optionNode, transformed, method));
return true;
}
}
throw new XamlParseException($"Option value \"{optionAsString}\" is not assignable to any of existing ShouldProvideOption methods", li);
}
return false;
}
}
return node;
IXamlAstValueNode TransformNode(
IReadOnlyCollection<IXamlAstValueNode> values,
IXamlType suggestedType,
IXamlLineInfo line)
{
if (suggestedType is not null)
{
values = values
.Select(v => XamlTransformHelpers
.TryGetCorrectlyTypedValue(context, v, suggestedType, out var converted)
? converted : v)
.ToArray();
}
if (values.Count > 1)
{
throw new XamlParseException("Options markup extension supports only a singular value", line);
}
return values.Single();
}
}
internal sealed class OptionsMarkupExtensionNode : XamlMarkupExtensionNode, IXamlAstValueNode
{
private readonly IXamlType _contextParameter;
public OptionsMarkupExtensionNode(
XamlMarkupExtensionNode original,
OptionsMarkupExtensionBranch[] branches,
IXamlAstValueNode defaultNode,
IXamlType contextParameter)
: base(
original.Value,
new OptionsMarkupExtensionMethod(new OptionsMarkupExtensionNodesContainer(branches, defaultNode), original.Value.Type.GetClrType(), contextParameter),
original.Value)
{
_contextParameter = contextParameter;
}
public new OptionsMarkupExtensionMethod ProvideValue => (OptionsMarkupExtensionMethod)base.ProvideValue;
IXamlAstTypeReference IXamlAstValueNode.Type => new XamlAstClrTypeReference(this, ProvideValue.ReturnType, false);
public override void VisitChildren(IXamlAstVisitor visitor)
{
ProvideValue.ExtensionNodeContainer.Visit(visitor);
base.VisitChildren(visitor);
}
public bool ConvertToReturnType(AstTransformationContext context, IXamlType type, out OptionsMarkupExtensionNode res)
{
IXamlAstValueNode convertedDefaultNode = null;
if (ProvideValue.ExtensionNodeContainer.DefaultNode is { } defaultNode)
{
if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, defaultNode, type, out convertedDefaultNode))
{
res = null;
return false;
}
}
var convertedBranches = ProvideValue.ExtensionNodeContainer.Branches.Select(b => XamlTransformHelpers
.TryGetCorrectlyTypedValue(context, b.Value, type, out var convertedValue) ?
new OptionsMarkupExtensionBranch(b.Option, convertedValue, b.ConditionMethod) :
null).ToArray();
if (convertedBranches.Any(b => b is null))
{
res = null;
return false;
}
res = new OptionsMarkupExtensionNode(this, convertedBranches, convertedDefaultNode, _contextParameter);
return true;
}
}
internal sealed class OptionsMarkupExtensionNodesContainer : XamlAstNode
{
public OptionsMarkupExtensionNodesContainer(
OptionsMarkupExtensionBranch[] branches,
IXamlAstValueNode defaultNode) : base(branches.FirstOrDefault()?.Value ?? defaultNode)
{
Branches = branches;
DefaultNode = defaultNode;
}
public OptionsMarkupExtensionBranch[] Branches { get; }
public IXamlAstValueNode DefaultNode { get; private set; }
public override void VisitChildren(IXamlAstVisitor visitor)
{
VisitList(Branches, visitor);
DefaultNode = (IXamlAstValueNode)DefaultNode?.Visit(visitor);
}
public IXamlType GetReturnType()
{
var types = Branches.Select(b => b.Value.Type);
if (DefaultNode?.Type is { } type)
{
types = types.Concat(new [] { type });
}
return types.Select(t => t.GetClrType()).ToArray().GetCommonBaseClass();
}
}
internal sealed class OptionsMarkupExtensionBranch : XamlAstNode
{
public OptionsMarkupExtensionBranch(IXamlAstValueNode option, IXamlAstValueNode value, IXamlMethod conditionMethod) : base(value)
{
Option = option;
Value = value;
ConditionMethod = conditionMethod;
}
public IXamlAstValueNode Option { get; set; }
public IXamlAstValueNode Value { get; set; }
public IXamlMethod ConditionMethod { get; }
public bool HasContext => ConditionMethod.Parameters.Count > 1;
public override void VisitChildren(IXamlAstVisitor visitor)
{
Option = (IXamlAstValueNode)Option.Visit(visitor);
Value = (IXamlAstValueNode)Value.Visit(visitor);
}
}
internal sealed class OptionsMarkupExtensionMethod : IXamlCustomEmitMethodWithContext<IXamlILEmitter, XamlILNodeEmitResult>
{
public OptionsMarkupExtensionMethod(
OptionsMarkupExtensionNodesContainer extensionNodeContainer,
IXamlType declaringType,
IXamlType contextParameter)
{
ExtensionNodeContainer = extensionNodeContainer;
DeclaringType = declaringType;
Parameters = extensionNodeContainer.Branches.Any(c => c.HasContext) ?
new[] { contextParameter } :
Array.Empty<IXamlType>();
}
public OptionsMarkupExtensionNodesContainer ExtensionNodeContainer { get; }
public string Name => "ProvideValue";
public bool IsPublic => true;
public bool IsStatic => false;
public IXamlType ReturnType => ExtensionNodeContainer.GetReturnType();
public IReadOnlyList<IXamlType> Parameters { get; }
public IXamlType DeclaringType { get; }
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => throw new NotImplementedException();
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => Array.Empty<IXamlCustomAttribute>();
public void EmitCall(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
// At this point this extension will be called from MarkupExtensionEmitter.
// Since it's a "fake" method, we share stack and locals with parent method.
// Real ProvideValue method would pop 2 parameters from the stack and return one. This method should do the same.
// At this point we will have on stack:
// - context (if parameters > 1)
// - markup ext "@this" instance (always)
// We always pop context from the stack, as this method decide by itself either context is needed.
// We store "@this" as a local variable. But only if any conditional method is an instance method.
IXamlLocal @this = null;
if (Parameters.Count > 0)
{
codeGen.Pop();
}
if (ExtensionNodeContainer.Branches.Any(b => !b.ConditionMethod.IsStatic))
{
codeGen.Stloc(@this = codeGen.DefineLocal(DeclaringType));
}
else
{
codeGen.Pop();
}
// Iterate over all branches and push prepared locals into the stack if needed.
var ret = codeGen.DefineLabel();
foreach (var branch in ExtensionNodeContainer.Branches)
{
var next = codeGen.DefineLabel();
if (branch.HasContext)
{
codeGen.Ldloc(context.ContextLocal);
}
if (!branch.ConditionMethod.IsStatic)
{
codeGen.Ldloc(@this);
}
context.Emit(branch.Option, codeGen, branch.Option.Type.GetClrType());
codeGen.EmitCall(branch.ConditionMethod);
codeGen.Brfalse(next);
context.Emit(branch.Value, codeGen, branch.Value.Type.GetClrType());
codeGen.Br(ret);
codeGen.MarkLabel(next);
}
if (ExtensionNodeContainer.DefaultNode is {} defaultNode)
{
// Nop is needed, otherwise Label wouldn't be set on nested CALL op (limitation of our IL validator).
codeGen.Emit(OpCodes.Nop);
context.Emit(defaultNode, codeGen, defaultNode.Type.GetClrType());
}
else
{
codeGen.EmitDefault(ReturnType);
}
codeGen.MarkLabel(ret);
}
public bool Equals(IXamlMethod other) => ReferenceEquals(this, other);
}
}

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

@ -9,6 +9,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
class AvaloniaXamlIlWellKnownTypes
{
public IXamlType RuntimeHelpers { get; }
public IXamlType AvaloniaObject { get; }
public IXamlType IAvaloniaObject { get; }
public IXamlType BindingPriority { get; }
@ -29,6 +30,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AssignBindingAttribute { get; }
public IXamlType DependsOnAttribute { get; }
public IXamlType DataTypeAttribute { get; }
public IXamlType MarkupExtensionOptionAttribute { get; }
public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
public IXamlType OnExtensionType { get; }
public IXamlType UnsetValueType { get; }
public IXamlType StyledElement { get; }
public IXamlType IStyledElement { get; }
@ -107,6 +111,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
RuntimeHelpers = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers");
XamlIlTypes = cfg.WellKnownTypes;
AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject");
IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject");
@ -128,6 +134,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute");
DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute");
DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute");
MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");
AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject,
AvaloniaProperty,
IBinding, cfg.WellKnownTypes.Object);
@ -245,7 +254,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
return rv;
}
public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> ctx)
{
if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv))

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d
Subproject commit 880ba5742e52b67afda048c4023cf7e3c3c16a46

3
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -29,6 +29,9 @@
<Compile Include="MarkupExtensions\CompiledBindings\StrongTypeCastNode.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
<Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />
<Compile Include="MarkupExtensions\On.cs" />
<Compile Include="MarkupExtensions\OnFormFactorExtension.cs" />
<Compile Include="MarkupExtensions\OnPlatformExtension.cs" />
<Compile Include="MarkupExtensions\ResolveByNameExtension.cs" />
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />

15
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs

@ -0,0 +1,15 @@
#nullable enable
using System.Collections.Generic;
using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class On<TReturn>
{
public IReadOnlyList<string> Options { get; } = new List<string>();
[Content]
public TReturn? Content { get; set; }
}
public class On : On<object> {}

60
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs

@ -0,0 +1,60 @@
#nullable enable
using System;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On>
{
public OnFormFactorExtension()
{
}
public OnFormFactorExtension(object defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option;
}
}
public class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>>
{
public OnFormFactorExtension()
{
}
public OnFormFactorExtension(TReturn defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option;
}
}
public abstract class OnFormFactorExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionDefaultOption]
public TReturn? Default { get; set; }
[MarkupExtensionOption(FormFactorType.Desktop)]
public TReturn? Desktop { get; set; }
[MarkupExtensionOption(FormFactorType.Mobile)]
public TReturn? Mobile { get; set; }
// Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {}
}

73
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs

@ -0,0 +1,73 @@
#nullable enable
using System;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnPlatformExtension : OnPlatformExtensionBase<object, On>
{
public OnPlatformExtension()
{
}
public OnPlatformExtension(object defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option;
}
}
public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>>
{
public OnPlatformExtension()
{
}
public OnPlatformExtension(TReturn defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option;
}
}
public abstract class OnPlatformExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionDefaultOption]
public TReturn? Default { get; set; }
[MarkupExtensionOption(OperatingSystemType.WinNT)]
public TReturn? Windows { get; set; }
[MarkupExtensionOption(OperatingSystemType.OSX)]
// ReSharper disable once InconsistentNaming
public TReturn? macOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Linux)]
public TReturn? Linux { get; set; }
[MarkupExtensionOption(OperatingSystemType.Android)]
public TReturn? Android { get; set; }
[MarkupExtensionOption(OperatingSystemType.iOS)]
// ReSharper disable once InconsistentNaming
public TReturn? iOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Browser)]
public TReturn? Browser { get; set; }
// Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {}
}

22
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Platform;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Global
@ -18,7 +19,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
{
return DeferredTransformationFactoryV2<IControl>(builder, provider);
}
public static Func<IServiceProvider, object> DeferredTransformationFactoryV2<T>(Func<IServiceProvider, object> builder,
IServiceProvider provider)
{
@ -31,7 +32,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
scope.Complete();
if(typeof(T) == typeof(IControl))
return new ControlTemplateResult((IControl)obj, scope);
@ -107,9 +108,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
throw new ArgumentException("Don't know what to do with " + value.GetType());
}
public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled)
public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled)
=> new InnerServiceProvider(compiled);
class InnerServiceProvider : IServiceProvider
{
private readonly IServiceProvider _compiledProvider;
@ -136,7 +137,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
{
_nsInfo = nsInfo;
}
public Type Resolve(string qualifiedTypeName)
{
var sp = qualifiedTypeName.Split(new[] {':'}, 2);
@ -166,22 +167,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
return new RootServiceProvider(new NameScope());
}
#line default
class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider
{
private readonly INameScope _nameScope;
private readonly IRuntimePlatform _runtimePlatform;
public RootServiceProvider(INameScope nameScope)
{
_nameScope = nameScope;
_runtimePlatform = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
}
public object GetService(Type serviceType)
{
if (serviceType == typeof(INameScope))
return _nameScope;
if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider))
return this;
if (serviceType == typeof(IRuntimePlatform))
return _runtimePlatform ?? throw new KeyNotFoundException($"{nameof(IRuntimePlatform)} was not registered");
return null;
}

26
src/Web/Avalonia.Web/BrowserRuntimePlatform.cs

@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Text.RegularExpressions;
using Avalonia.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web;
internal class BrowserRuntimePlatform : StandardRuntimePlatform
{
private static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
{
var result = new RuntimePlatformInfo
{
IsCoreClr = true, // WASM browser is always CoreCLR
IsBrowser = true, // BrowserRuntimePlatform only runs on Browser.
OperatingSystem = OperatingSystemType.Browser,
IsMobile = AvaloniaModule.IsMobile()
};
return result;
});
public override RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
}

2
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Web.Skia;
using System.Runtime.Versioning;
using Avalonia.Platform;
namespace Avalonia.Web;
@ -23,7 +24,6 @@ public class BrowserPlatformOptions
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}");
}
[SupportedOSPlatform("browser")]
public static class WebAppBuilder
{

5
src/Web/Avalonia.Web/Interop/AvaloniaModule.cs

@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static class AvaloniaModule
internal static partial class AvaloniaModule
{
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
@ -19,4 +19,7 @@ internal static class AvaloniaModule
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js"));
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
}

8
src/Web/Avalonia.Web/WindowingPlatform.cs

@ -31,8 +31,10 @@ namespace Avalonia.Web
public static void Register()
{
var instance = new BrowserWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToSingleton<BrowserRuntimePlatform>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
@ -64,11 +66,11 @@ namespace Avalonia.Web
{
if (_signaled)
return;
_signaled = true;
IDisposable? disp = null;
disp = GetRuntimePlatform()
.StartSystemTimer(TimeSpan.FromMilliseconds(1),
() =>

18
src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts

@ -1,13 +1,25 @@
export class Caniuse {
public static canShowOpenFilePicker(): boolean {
return typeof window.showOpenFilePicker !== "undefined";
return typeof globalThis.showOpenFilePicker !== "undefined";
}
public static canShowSaveFilePicker(): boolean {
return typeof window.showSaveFilePicker !== "undefined";
return typeof globalThis.showSaveFilePicker !== "undefined";
}
public static canShowDirectoryPicker(): boolean {
return typeof window.showDirectoryPicker !== "undefined";
return typeof globalThis.showDirectoryPicker !== "undefined";
}
public static isMobile(): boolean {
const userAgentData = (globalThis.navigator as any)?.userAgentData;
if (userAgentData) {
return userAgentData.mobile;
}
const userAgent = navigator.userAgent;
const regex1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i;
const regex2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw(n|u)|c55\/|capi|ccwa|cdm|cell|chtm|cldc|cmd|co(mp|nd)|craw|da(it|ll|ng)|dbte|dcs|devi|dica|dmob|do(c|p)o|ds(12|d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(|_)|g1 u|g560|gene|gf5|gmo|go(\.w|od)|gr(ad|un)|haie|hcit|hd(m|p|t)|hei|hi(pt|ta)|hp( i|ip)|hsc|ht(c(| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i(20|go|ma)|i230|iac( ||\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|[a-w])|libw|lynx|m1w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|mcr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|([1-8]|c))|phil|pire|pl(ay|uc)|pn2|po(ck|rt|se)|prox|psio|ptg|qaa|qc(07|12|21|32|60|[2-7]|i)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h|oo|p)|sdk\/|se(c(|0|1)|47|mc|nd|ri)|sgh|shar|sie(|m)|sk0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h|v|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl|tdg|tel(i|m)|tim|tmo|to(pl|sh)|ts(70|m|m3|m5)|tx9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas|your|zeto|zte/i;
return regex1.test(userAgent) || regex2.test(userAgent.substr(0, 4));
}
}

25
tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs

@ -1,27 +1,34 @@
#nullable enable
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Xunit;
namespace Avalonia.IntegrationTests.Appium
namespace Avalonia
{
[Flags]
internal enum TestPlatforms
{
Windows = 0x01,
MacOS = 0x02,
All = Windows | MacOS,
Linux = 0x04,
All = Windows | MacOS | Linux,
}
internal class PlatformFactAttribute : FactAttribute
{
public PlatformFactAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;
private readonly string? _reason;
public PlatformFactAttribute(TestPlatforms platforms, string? reason = null)
{
_reason = reason;
Platforms = platforms;
}
public TestPlatforms Platforms { get; }
public override string? Skip
{
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}" + (_reason is not null ? $" reason: \"{_reason}\"" : "");
set => throw new NotSupportedException();
}
@ -31,6 +38,8 @@ namespace Avalonia.IntegrationTests.Appium
return Platforms.HasAnyFlag(TestPlatforms.Windows);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Platforms.HasAnyFlag(TestPlatforms.MacOS);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return Platforms.HasAnyFlag(TestPlatforms.Linux);
return false;
}
}

5
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -31,5 +31,10 @@
<AvaloniaResource Include="Xaml\XamlIlClassWithPrecompiledXaml.xaml" />
<AvaloniaResource Include="Xaml\XamlIlClassWithCustomProperty.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.IntegrationTests.Appium\PlatformFactAttribute.cs">
<Link>PlatformFactAttribute.cs</Link>
</Compile>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

71
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs

@ -0,0 +1,71 @@
using Avalonia.Controls;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OnFormFactorExtensionTests : XamlTestBase
{
[Fact]
public void Should_Resolve_Default_Value()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(false, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnFormFactor Default=""Hello World""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal("Hello World", textBlock.Text);
}
}
[Theory]
[InlineData(false, true, "Im Mobile")]
[InlineData(true, false, "Im Desktop")]
[InlineData(false, false, "Default value")]
public void Should_Resolve_Expected_Value_Per_Platform(bool isDesktop, bool isMobile, string expectedResult)
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(isDesktop, isMobile));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnFormFactor ""Default value"",
Mobile=""Im Mobile"", Desktop=""Im Desktop""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal(expectedResult, textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly bool _isDesktop;
private readonly bool _isMobile;
public TestRuntimePlatform(bool isDesktop, bool isMobile)
{
_isDesktop = isDesktop;
_isMobile = isMobile;
}
public override RuntimePlatformInfo GetRuntimeInfo()
{
return new RuntimePlatformInfo() { IsDesktop = _isDesktop, IsMobile = _isMobile };
}
}
}

75
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs

@ -0,0 +1,75 @@
using Avalonia.Controls;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OnPlatformExtensionTests : XamlTestBase
{
[Fact]
public void Should_Resolve_Default_Value()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnPlatform Default=""Hello World""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal("Hello World", textBlock.Text);
}
}
[Theory]
[InlineData(OperatingSystemType.WinNT, "Im Windows")]
[InlineData(OperatingSystemType.OSX, "Im macOS")]
[InlineData(OperatingSystemType.Linux, "Im Linux")]
[InlineData(OperatingSystemType.Android, "Im Android")]
[InlineData(OperatingSystemType.iOS, "Im iOS")]
[InlineData(OperatingSystemType.Browser, "Im Browser")]
[InlineData(OperatingSystemType.Unknown, "Default value")]
public void Should_Resolve_Expected_Value_Per_Platform(OperatingSystemType currentPlatform, string expectedResult)
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(currentPlatform));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnPlatform ""Default value"",
Windows=""Im Windows"", macOS=""Im macOS"",
Linux=""Im Linux"", Android=""Im Android"",
iOS=""Im iOS"", Browser=""Im Browser""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal(expectedResult, textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly OperatingSystemType _operatingSystemType;
public TestRuntimePlatform(OperatingSystemType operatingSystemType)
{
_operatingSystemType = operatingSystemType;
}
public override RuntimePlatformInfo GetRuntimeInfo()
{
return new RuntimePlatformInfo() { OperatingSystem = _operatingSystemType };
}
}
}

605
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs

@ -0,0 +1,605 @@
using System;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Metadata;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OptionsMarkupExtensionTests : XamlTestBase
{
public static Func<object, bool> RaisedOption;
public static int? ObjectsCreated;
[Fact]
public void Resolve_Default_Value()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension Default=""Hello World""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Hello World", textBlock.Text);
}
[Fact]
public void Resolve_Default_Value_From_Ctor()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension ""Hello World"", OptionB=""Im Android""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Hello World", textBlock.Text);
}
[Fact]
public void Resolve_Implicit_Default_Value_Ref_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Tag='{local:OptionsMarkupExtension OptionA=""Hello World"", x:DataType=x:String}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(null, userControl.Tag);
}
[Fact]
public void Resolve_Implicit_Default_Value_Val_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Height='{local:OptionsMarkupExtension OptionA=10}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(0d, userControl.Height);
}
[Fact]
public void Resolve_Implicit_Default_Value_Avalonia_Val_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Margin='{local:OptionsMarkupExtension OptionA=10}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(0), userControl.Margin);
}
[Theory]
[InlineData("option 1", "Im Option 1")]
[InlineData("option 2", "Im Option 2")]
[InlineData("3", "Im Option 3")]
[InlineData("unknown", "Default value")]
public void Resolve_Expected_Value_Per_Option(object option, string expectedResult)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension ""Default value"",
OptionA=""Im Option 1"", OptionB=""Im Option 2"",
OptionNumber=""Im Option 3""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(expectedResult, textBlock.Text);
}
[Theory]
[InlineData("option 1", "Im Option 1")]
[InlineData("option 2", "Im Option 2")]
[InlineData("3", "Im Option 3")]
[InlineData("unknown", "Default value")]
public void Resolve_Expected_Value_Per_Option_Create_Single_Object(object option, string expectedResult)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Content>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.Default>
<local:ChildObject Name=""Default value"" />
</local:OptionsMarkupExtension.Default>
<local:OptionsMarkupExtension.OptionA>
<local:ChildObject Name=""Im Option 1"" />
</local:OptionsMarkupExtension.OptionA>
<local:OptionsMarkupExtension.OptionB>
<local:ChildObject Name=""Im Option 2"" />
</local:OptionsMarkupExtension.OptionB>
<local:OptionsMarkupExtension.OptionNumber>
<local:ChildObject Name=""Im Option 3"" />
</local:OptionsMarkupExtension.OptionNumber>
</local:OptionsMarkupExtension>
</ContentControl.Content>
</ContentControl>";
var contentControl = (ContentControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var obj = Assert.IsType<ChildObject>(contentControl.Content);
Assert.Equal(expectedResult, obj.Name);
Assert.Equal(1, ObjectsCreated);
}
[Fact]
public void Convert_Bcl_Type()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Height='{local:OptionsMarkupExtension OptionA=50.1}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(50.1, border.Height);
}
[Fact]
public void Convert_Avalonia_Type()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Padding='{local:OptionsMarkupExtension OptionA=""10, 8, 10, 8""}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 8, 10, 8), border.Padding);
}
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")]
public void Respect_Custom_TypeArgument()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Tag='{local:OptionsMarkupExtensionWithGeneric Default=20, OptionA=""10, 10, 10, 10"", x:TypeArguments=Thickness}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 10, 10, 10), textBlock.Tag);
}
[Fact]
public void Allow_Nester_Markup_Extensions()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</UserControl.Resources>
<Border Background='{local:OptionsMarkupExtension OptionA={StaticResource brush}}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
[Fact]
public void Allow_Nester_On_Platform_Markup_Extensions()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Margin='{local:OptionsMarkupExtension OptionA={local:OptionsMarkupExtensionMinimal OptionA=""10,10,10,10""}}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10), border.Margin);
}
[Fact]
public void Support_Xml_Syntax()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Background>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<SolidColorBrush Color='#ff506070' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
</Border.Background>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")]
public void Support_Xml_Syntax_With_Custom_TypeArguments()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Tag>
<local:OptionsMarkupExtensionWithGeneric x:TypeArguments='Thickness' OptionA='10, 10, 10, 10' Default='20' />
</Border.Tag>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 10, 10, 10), border.Tag);
}
[Theory]
[InlineData("option 1", "#ff506070")]
[InlineData("3", "#000")]
public void Support_Special_On_Syntax(object option, string color)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Background>
<local:OptionsMarkupExtension>
<On Options='OptionA, OptionB'>
<SolidColorBrush Color='#ff506070' />
</On>
<On Options=' OptionNumber '>
<SolidColorBrush Color='#000' />
</On>
</local:OptionsMarkupExtension>
</Border.Background>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(Color.Parse(color), ((ISolidColorBrush)border.Background!).Color);
}
[Fact]
public void Support_Control_Inside_Xml_Syntax()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
[Fact]
public void Support_Default_Control_Inside_Xml_Syntax()
{
using var _ = SetupTestGlobals("unknown");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.Default>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.Default>
</local:OptionsMarkupExtension>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
[Fact]
public void Support_Complex_Property_Setters_Dictionary()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Color1'>Black</Color>
<local:OptionsMarkupExtension x:Key='MyKey'>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
<Color x:Key='Color2'>White</Color>
</ResourceDictionary>";
var resourceDictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<Button>(resourceDictionary["MyKey"]);
Assert.Equal("Hello World", button.Content);
Assert.Equal(Colors.Black, resourceDictionary["Color1"]);
Assert.Equal(Colors.White, resourceDictionary["Color2"]);
}
[Fact]
public void Support_Complex_Property_Setters_List()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Panel xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock />
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
<TextBox />
</Panel>";
var panel = (Panel)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(3, panel.Children.Count);
Assert.IsType<Button>(panel.Children[1]);
}
[Theory]
[InlineData("option 1", "foo")]
[InlineData("option 2", "bar")]
public void BindingExtension_Works_Inside_Of_OptionsMarkupExtension(string option, string expected)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foo</x:String>
</UserControl.Resources>
<TextBlock Name='textBlock' Text='{local:OptionsMarkupExtension OptionA={CompiledBinding Source={StaticResource text}}, OptionB=bar}'/>
</UserControl>";
var window = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
Assert.Equal(expected, textBlock.Text);
}
[Fact]
public void Resolve_Expected_Value_With_Method_Without_ServiceProvider()
{
using var _ = SetupTestGlobals(2);
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtensionNoServiceProvider OptionB=""Im Option 2""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Im Option 2", textBlock.Text);
}
[Fact]
public void Resolve_Expected_Value_Minimal_Extension()
{
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
IsVisible='{local:OptionsMarkupExtensionMinimal OptionA=True}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty));
Assert.True(textBlock.IsVisible);
}
[Fact]
public void Resolve_Expected_Value_Extension_With_Property()
{
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
IsVisible='{local:OptionsMarkupExtensionWithProperty OptionA=True, Property=5}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty));
Assert.True(textBlock.IsVisible);
}
private static IDisposable SetupTestGlobals(object acceptedOption)
{
RaisedOption = o => o.Equals(acceptedOption);
ObjectsCreated = 0;
return Disposable.Create(() =>
{
RaisedOption = null;
ObjectsCreated = null;
});
}
}
public class OptionsMarkupExtension : OptionsMarkupExtensionBase<object, On>
{
public OptionsMarkupExtension()
{
}
public OptionsMarkupExtension(object defaultValue)
{
Default = defaultValue;
}
}
public class OptionsMarkupExtension<TReturn> : OptionsMarkupExtensionBase<TReturn, On<TReturn>>
{
public OptionsMarkupExtension()
{
}
public OptionsMarkupExtension(TReturn defaultValue)
{
Default = defaultValue;
}
}
public class OptionsMarkupExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionOption("option 1")]
public TReturn OptionA { get; set; }
[MarkupExtensionOption("option 2")]
public TReturn OptionB { get; set; }
[MarkupExtensionOption(3)]
public TReturn OptionNumber { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public TReturn Default { get; set; }
public bool ShouldProvideOption(IServiceProvider serviceProvider, string option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public TReturn ProvideValue(IServiceProvider serviceProvider) { throw null; }
public void AddChild(TOn child) {}
}
public class OptionsMarkupExtensionNoServiceProvider
{
[MarkupExtensionOption(1)]
public object OptionA { get; set; }
[MarkupExtensionOption(2)]
public object OptionB { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public object Default { get; set; }
public static bool ShouldProvideOption(int option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public object ProvideValue(IServiceProvider serviceProvider) { throw null; }
}
public class OptionsMarkupExtensionMinimal
{
[MarkupExtensionOption(11.0)]
public bool OptionA { get; set; }
public static bool ShouldProvideOption(double option) => option > 0;
public object ProvideValue() { throw null; }
}
public class OptionsMarkupExtensionWithProperty
{
[MarkupExtensionOption(5)]
public bool OptionA { get; set; }
public int Property { get; set; }
public bool ShouldProvideOption(int option) => option == Property;
public object ProvideValue() { throw null; }
}
public class OptionsMarkupExtensionWithGeneric<TResult>
{
[MarkupExtensionOption("option 1")]
public TResult OptionA { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public TResult Default { get; set; }
public bool ShouldProvideOption(string option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public TResult ProvideValue() { throw null; }
}
public class ChildObject
{
public string Name { get; set; }
public ChildObject()
{
OptionsMarkupExtensionTests.ObjectsCreated++;
}
}
Loading…
Cancel
Save