Browse Source

Refactor extension to use a custom xaml method

pull/7812/head
Max Katz 3 years ago
parent
commit
12921c17f7
  1. 8
      samples/ControlCatalog/MainView.xaml
  2. 12
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml
  3. 22
      src/Avalonia.Base/Metadata/MarkupExtensionOption.cs
  4. 82
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlConditionalNode.cs
  5. 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  6. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  7. 159
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOnFormFactorTransformer.cs
  8. 205
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOnPlatformTransformer.cs
  9. 405
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
  10. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  11. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  12. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  13. 15
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs
  14. 87
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs
  15. 136
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs
  16. 238
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs
  17. 294
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs
  18. 604
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs

8
samples/ControlCatalog/MainView.xaml

@ -13,9 +13,6 @@
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Platform Information">
<pages:PlatformInfoPage />
</TabItem>
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
@ -121,7 +118,10 @@
<TabItem Header="OpenGL">
<pages:OpenGlPage />
</TabItem>
<TabItem Header="Pointers">
<TabItem Header="Platform Information">
<pages:PlatformInfoPage />
</TabItem>
<TabItem Header="Pointers">
<pages:PointersPage />
</TabItem>
<TabItem Header="ProgressBar">

12
samples/ControlCatalog/Pages/PlatformInfoPage.xaml

@ -18,7 +18,7 @@
<TextBlock Text="Mobile" />
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal">
<WrapPanel>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Windows=Green}">
<TextBlock Text="Windows" />
</Border>
@ -37,7 +37,15 @@
<Border Height="100" Width="100" Background="{OnPlatform Gray, Android=Green}">
<TextBlock Text="Android" />
</Border>
</StackPanel>
<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>

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
{
}

82
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlConditionalNode.cs

@ -1,82 +0,0 @@
using System.Collections.Generic;
using System.Reflection.Emit;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
internal sealed class AvaloniaXamlIlConditionalNode : XamlAstNode,
IXamlAstEmitableNode<IXamlILEmitter, XamlILNodeEmitResult>, IXamlAstManipulationNode
{
private readonly AvaloniaXamlIlConditionalDefaultNode? _defaultValue;
private readonly IReadOnlyList<AvaloniaXamlIlConditionalBranchNode> _branches;
public AvaloniaXamlIlConditionalNode(
AvaloniaXamlIlConditionalDefaultNode defaultValue,
IReadOnlyList<AvaloniaXamlIlConditionalBranchNode> values,
IXamlLineInfo info) : base(info)
{
_defaultValue = defaultValue;
_branches = values;
}
public override void VisitChildren(IXamlAstVisitor visitor)
{
_defaultValue?.VisitChildren(visitor);
foreach (var branch in _branches)
{
branch.VisitChildren(visitor);
}
}
public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
var ret = codeGen.DefineLabel();
foreach (var platform in _branches)
{
var next = codeGen.DefineLabel();
platform.EmitCondition(context, codeGen);
codeGen.Brfalse(next);
platform.EmitBody(context, codeGen);
codeGen.Br(ret);
codeGen.MarkLabel(next);
}
if (_defaultValue is not null)
{
codeGen.Emit(OpCodes.Nop);
_defaultValue.EmitBody(context, codeGen);
}
else
{
codeGen.Pop();
}
codeGen.MarkLabel(ret);
return XamlILNodeEmitResult.Void(1);
}
}
internal abstract class AvaloniaXamlIlConditionalBranchNode : XamlAstNode, IXamlAstManipulationNode
{
protected AvaloniaXamlIlConditionalBranchNode(IXamlLineInfo lineInfo) : base(lineInfo)
{
}
public abstract void EmitCondition(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen);
public abstract void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen);
}
internal abstract class AvaloniaXamlIlConditionalDefaultNode : XamlAstNode, IXamlAstManipulationNode
{
protected AvaloniaXamlIlConditionalDefaultNode(IXamlLineInfo lineInfo) : base(lineInfo)
{
}
public abstract void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen);
}

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

@ -58,9 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
InsertAfter<ResolveContentPropertyTransformer>(
new AvaloniaXamlIlOnPlatformTransformer(),
new AvaloniaXamlIlOnFormFactorTransformer());
InsertBefore<ConvertPropertyValuesToAssignmentsTransformer>(
new AvaloniaXamlIlOptionMarkupExtensionTransformer());
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());

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;

159
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOnFormFactorTransformer.cs

@ -1,159 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
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 AvaloniaXamlIlOnFormFactorTransformer : IXamlAstTransformer
{
private const string OnFormFactorFqn = "Avalonia.Markup.Xaml:Avalonia.Markup.Xaml.MarkupExtensions.OnFormFactorExtension";
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstXamlPropertyValueNode targetPropertyNode
&& targetPropertyNode.Values.OfType<XamlMarkupExtensionNode>().FirstOrDefault() is
{
Value: XamlAstObjectNode { Type: XamlAstClrTypeReference { Type: { } type } } objectNode
}
&& type.GetFqn().StartsWith(OnFormFactorFqn))
{
var typeArgument = type.GenericArguments?.FirstOrDefault();
OnFormFactorDefaultNode defaultValue = null;
var values = new List<OnFormFactorBranchNode>();
var directives = objectNode.Children.OfType<XamlAstXmlDirective>().ToArray();
foreach (var child in objectNode.Arguments.Take(1))
{
defaultValue = new OnFormFactorDefaultNode(new XamlAstXamlPropertyValueNode(child, targetPropertyNode.Property, child));
}
foreach (var extProp in objectNode.Children.OfType<XamlAstXamlPropertyValueNode>())
{
var propName = extProp.Property.GetClrProperty().Name.Trim();
var transformed = TransformNode(targetPropertyNode.Property, extProp.Values,
typeArgument, directives, extProp);
if (propName.Equals("DEFAULT", StringComparison.OrdinalIgnoreCase))
{
defaultValue = new OnFormFactorDefaultNode(transformed);
}
else if (propName != "CONTENT")
{
values.Add(new OnFormFactorBranchNode(
ConvertPlatformNode(propName, extProp),
transformed));
}
}
return new AvaloniaXamlIlConditionalNode(defaultValue, values, node);
}
return node;
XamlConstantNode ConvertPlatformNode(string propName, IXamlLineInfo li)
{
var enumType = context.GetAvaloniaTypes().FormFactorType;
if (TypeSystemHelpers.TryGetEnumValueNode(enumType, propName, li, false, out var enumConstantNode))
{
return enumConstantNode;
}
throw new XamlParseException($"Unable to parse form factor name: \"{propName}\"", li);
}
XamlAstXamlPropertyValueNode TransformNode(
IXamlAstPropertyReference property,
IReadOnlyCollection<IXamlAstValueNode> values,
IXamlType suggestedType,
IReadOnlyCollection<XamlAstXmlDirective> directives,
IXamlLineInfo line)
{
if (suggestedType is not null)
{
values = values
.Select(v => XamlTransformHelpers
.TryGetCorrectlyTypedValue(context, v, suggestedType, out var converted)
? converted : v)
.ToArray();
}
if (directives.Any())
{
foreach (var value in values)
{
if (value is XamlAstObjectNode xamlAstObjectNode)
{
xamlAstObjectNode.Children.AddRange(directives);
}
}
}
return new XamlAstXamlPropertyValueNode(line, property, values);
}
}
private sealed class OnFormFactorBranchNode : AvaloniaXamlIlConditionalBranchNode
{
private IXamlAstNode _platform;
private IXamlAstNode _value;
public OnFormFactorBranchNode(
IXamlAstNode platform,
IXamlAstNode value)
: base(value)
{
_platform = platform;
_value = value;
}
public override void VisitChildren(IXamlAstVisitor visitor)
{
_platform = _platform.Visit(visitor);
_value = _value.Visit(visitor);
}
public override void EmitCondition(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
var enumType = context.GetAvaloniaTypes().FormFactorType;
var isOnFormFactorMethod = context.GetAvaloniaTypes().IsOnFormFactorMethod;
codeGen.Ldloc(context.ContextLocal);
context.Emit(_platform, codeGen, enumType);
codeGen.EmitCall(isOnFormFactorMethod);
}
public override void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
context.Emit(_value, codeGen, null);
}
}
private sealed class OnFormFactorDefaultNode : AvaloniaXamlIlConditionalDefaultNode
{
private IXamlAstNode _value;
public OnFormFactorDefaultNode(
IXamlAstNode value)
: base(value)
{
_value = value;
}
public override void VisitChildren(IXamlAstVisitor visitor)
{
_value = _value.Visit(visitor);
}
public override void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
context.Emit(_value, codeGen, null);
}
}
}

205
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOnPlatformTransformer.cs

@ -1,205 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
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 AvaloniaXamlIlOnPlatformTransformer : IXamlAstTransformer
{
private const string OnPlatformFqn = "Avalonia.Markup.Xaml:Avalonia.Markup.Xaml.MarkupExtensions.OnPlatformExtension";
private const string OnFqn = "Avalonia.Markup.Xaml:Avalonia.Markup.Xaml.MarkupExtensions.On";
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstXamlPropertyValueNode targetPropertyNode
&& targetPropertyNode.Values.OfType<XamlMarkupExtensionNode>().FirstOrDefault() is
{
Value: XamlAstObjectNode { Type: XamlAstClrTypeReference { Type: { } type } } objectNode
}
&& type.GetFqn().StartsWith(OnPlatformFqn))
{
var typeArgument = type.GenericArguments?.FirstOrDefault();
OnPlatformDefaultNode defaultValue = null;
var values = new List<OnPlatformBranchNode>();
var directives = objectNode.Children.OfType<XamlAstXmlDirective>().ToArray();
foreach (var child in objectNode.Arguments.Take(1))
{
defaultValue = new OnPlatformDefaultNode(new XamlAstXamlPropertyValueNode(child, targetPropertyNode.Property, child));
}
foreach (var extProp in objectNode.Children.OfType<XamlAstXamlPropertyValueNode>())
{
var onObjs = extProp.Values.OfType<XamlAstObjectNode>()
.Where(o => o.Type.GetClrType().GetFqn() == OnFqn).ToArray();
if (onObjs.Any())
{
foreach (var onObj in onObjs)
{
var platformStr = (onObj.Children.OfType<XamlAstXamlPropertyValueNode>()
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Platform")
?.Values.Single() as XamlAstTextNode)?
.Text;
if (string.IsNullOrWhiteSpace(platformStr))
{
throw new XamlParseException("On.Platform 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 transformed = TransformNode(targetPropertyNode.Property, content.Values,
typeArgument, directives, content);
foreach (var platform in platformStr.Split(new[] { ',' },
StringSplitOptions.RemoveEmptyEntries))
{
values.Add( new OnPlatformBranchNode(
ConvertPlatformNode(platform.Trim().ToUpperInvariant(), onObj),
transformed));
}
}
}
else
{
var propName = extProp.Property.GetClrProperty().Name.Trim().ToUpperInvariant();
var transformed = TransformNode(targetPropertyNode.Property, extProp.Values,
typeArgument, directives, extProp);
if (propName.Equals("DEFAULT", StringComparison.OrdinalIgnoreCase))
{
defaultValue = new OnPlatformDefaultNode(transformed);
}
else if (propName != "CONTENT")
{
values.Add(new OnPlatformBranchNode(
ConvertPlatformNode(propName, extProp),
transformed));
}
}
}
return new AvaloniaXamlIlConditionalNode(defaultValue, values, node);
}
return node;
XamlConstantNode ConvertPlatformNode(string platform, IXamlLineInfo li)
{
var osTypeEnum = context.GetAvaloniaTypes().OperatingSystemType;
if (platform.Equals("MACOS", StringComparison.OrdinalIgnoreCase))
{
platform = "OSX";
}
if (platform.Equals("WINDOWS", StringComparison.OrdinalIgnoreCase))
{
platform = "WINNT";
}
if (TypeSystemHelpers.TryGetEnumValueNode(osTypeEnum, platform, li, true, out var enumConstantNode))
{
return enumConstantNode;
}
throw new XamlParseException($"Unable to parse platform name: \"{platform}\"", li);
}
XamlAstXamlPropertyValueNode TransformNode(
IXamlAstPropertyReference property,
IReadOnlyCollection<IXamlAstValueNode> values,
IXamlType suggestedType,
IReadOnlyCollection<XamlAstXmlDirective> directives,
IXamlLineInfo line)
{
if (suggestedType is not null)
{
values = values
.Select(v => XamlTransformHelpers
.TryGetCorrectlyTypedValue(context, v, suggestedType, out var converted)
? converted : v)
.ToArray();
}
if (directives.Any())
{
foreach (var value in values)
{
if (value is XamlAstObjectNode xamlAstObjectNode)
{
xamlAstObjectNode.Children.AddRange(directives);
}
}
}
return new XamlAstXamlPropertyValueNode(line, property, values);
}
}
private sealed class OnPlatformBranchNode : AvaloniaXamlIlConditionalBranchNode
{
private IXamlAstNode _platform;
private IXamlAstNode _value;
public OnPlatformBranchNode(
IXamlAstNode platform,
IXamlAstNode value)
: base(value)
{
_platform = platform;
_value = value;
}
public override void VisitChildren(IXamlAstVisitor visitor)
{
_platform = _platform.Visit(visitor);
_value = _value.Visit(visitor);
}
public override void EmitCondition(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
var osTypeEnum = context.GetAvaloniaTypes().OperatingSystemType;
var isOSPlatformMethod = context.GetAvaloniaTypes().IsOnPlatformMethod;
codeGen.Ldloc(context.ContextLocal);
context.Emit(_platform, codeGen, osTypeEnum);
codeGen.EmitCall(isOSPlatformMethod);
}
public override void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
context.Emit(_value, codeGen, null);
}
}
private sealed class OnPlatformDefaultNode : AvaloniaXamlIlConditionalDefaultNode
{
private IXamlAstNode _value;
public OnPlatformDefaultNode(
IXamlAstNode value)
: base(value)
{
_value = value;
}
public override void VisitChildren(IXamlAstVisitor visitor)
{
_value = _value.Visit(visitor);
}
public override void EmitBody(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
context.Emit(_value, codeGen, null);
}
}
}

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

@ -0,0 +1,405 @@
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
{
private const string OnFqn = "Avalonia.Markup.Xaml:Avalonia.Markup.Xaml.MarkupExtensions.On";
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().GetFqn() == OnFqn).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.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.Append(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);
}
}

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

@ -30,6 +30,8 @@ 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 UnsetValueType { get; }
public IXamlType StyledElement { get; }
public IXamlType IStyledElement { get; }
@ -133,6 +135,8 @@ 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");
AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject,
AvaloniaProperty,
IBinding, cfg.WellKnownTypes.Object);
@ -235,11 +239,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
cfg.TypeSystem.GetType("System.Func`2").MakeGenericType(
cfg.TypeSystem.GetType("System.IServiceProvider"),
XamlIlTypes.Object));
OperatingSystemType = cfg.TypeSystem.GetType("Avalonia.Platform.OperatingSystemType");
IsOnPlatformMethod = RuntimeHelpers.FindMethod(m => m.IsStatic && m.Parameters.Count == 2 && m.Name == "IsOnPlatform");
FormFactorType = cfg.TypeSystem.GetType("Avalonia.Platform.FormFactorType");
IsOnFormFactorMethod = RuntimeHelpers.FindMethod(m => m.IsStatic && m.Parameters.Count == 2 && m.Name == "IsOnFormFactor");
}
}

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

@ -1 +1 @@
Subproject commit afbc488cedf0f20be554c3b63e48689b70b5dbf6
Subproject commit dab9e09fa56064be92d80c7c4328793fb0f93dbf

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -29,6 +29,7 @@
<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" />

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> {}

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

@ -1,27 +1,31 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnFormFactorExtension : OnFormFactorExtension<object>
public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On>
{
public OnFormFactorExtension()
{
}
public OnFormFactorExtension(object defaultValue) : base(defaultValue)
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>
public class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>>
{
private readonly Dictionary<string, TReturn?> _values = new();
public OnFormFactorExtension()
{
@ -32,64 +36,25 @@ public class OnFormFactorExtension<TReturn>
Default = defaultValue;
}
public TReturn? Default
{
get => _values.TryGetValue(nameof(Default), out var value) ? value : default;
set { _values[nameof(Default)] = value; }
}
public TReturn? Desktop
{
get => _values.TryGetValue(nameof(Desktop), out var value) ? value : default;
set { _values[nameof(Desktop)] = value; }
}
public TReturn? Mobile
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option)
{
get => _values.TryGetValue(nameof(Mobile), out var value) ? value : default;
set { _values[nameof(Mobile)] = value; }
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; }
public object? ProvideValue()
{
if (!_values.Any())
{
throw new InvalidOperationException(
"OnPlatformExtension requires a value to be specified for at least one platform or Default.");
}
var (value, hasValue) = TryGetValueForPlatform();
return !hasValue ? AvaloniaProperty.UnsetValue : value;
}
private (object? value, bool hasValue) TryGetValueForPlatform()
{
var runtimeInfo = AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>().GetRuntimeInfo();
TReturn val;
if (runtimeInfo.IsDesktop)
{
if (_values.TryGetValue(nameof(Desktop), out val))
{
return (val, true);
}
}
if (runtimeInfo.IsMobile)
{
if (_values.TryGetValue(nameof(Mobile), out val))
{
return (val, true);
}
}
[MarkupExtensionOption(FormFactorType.Desktop)]
public TReturn? Desktop { get; set; }
if (_values.TryGetValue(nameof(Default), out val))
{
return (val, true);
}
[MarkupExtensionOption(FormFactorType.Mobile)]
public TReturn? Mobile { get; set; }
return default;
}
// Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {}
}

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

@ -1,36 +1,30 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class On
{
public string Platform { get; set; } = "Unknown";
[Content]
public object? Content { get; set; }
}
public class OnPlatformExtension : OnPlatformExtension<object>
public class OnPlatformExtension : OnPlatformExtensionBase<object, On>
{
public OnPlatformExtension()
{
}
public OnPlatformExtension(object defaultValue) : base(defaultValue)
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> : IAddChild<On>
public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>>
{
private readonly Dictionary<string, TReturn?> _values = new();
public OnPlatformExtension()
{
@ -41,89 +35,39 @@ public class OnPlatformExtension<TReturn> : IAddChild<On>
Default = defaultValue;
}
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()
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option)
{
throw new NotSupportedException();
if (!_values.Any())
{
throw new InvalidOperationException("OnPlatformExtension requires a value to be specified for at least one platform or Default.");
}
var (value, hasValue) = TryGetValueForPlatform();
return !hasValue ? AvaloniaProperty.UnsetValue : value;
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option;
}
}
private (TReturn? value, bool hasValue) TryGetValueForPlatform()
{
var runtimeInfo = AvaloniaLocator.Current.GetRequiredService<IRuntimePlatform>().GetRuntimeInfo();
TReturn val;
switch (runtimeInfo.OperatingSystem)
{
case OperatingSystemType.WinNT:
if (_values.TryGetValue(nameof(Windows), out val))
{
return (val, true);
}
break;
case OperatingSystemType.OSX:
if (_values.TryGetValue(nameof(macOS), out val))
{
return (val, true);
}
break;
case OperatingSystemType.Linux:
if (_values.TryGetValue(nameof(Linux), out val))
{
return (val, true);
}
break;
case OperatingSystemType.Android:
if (_values.TryGetValue(nameof(Android), out val))
{
return (val, true);
}
break;
case OperatingSystemType.iOS:
if (_values.TryGetValue(nameof(iOS), out val))
{
return (val, true);
}
break;
case OperatingSystemType.Browser:
if (_values.TryGetValue(nameof(Browser), out val))
{
return (val, true);
}
break;
}
if (_values.TryGetValue(nameof(Default), out val))
{
return (val, true);
};
return default;
}
public abstract class OnPlatformExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionDefaultOption]
public TReturn? Default { get; set; }
public void AddChild(On child)
{
foreach (var platform in child.Platform.Split(new [] { "," }, StringSplitOptions.RemoveEmptyEntries))
{
_values[platform.Trim()] = (TReturn?)child.Content;
}
}
[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) {}
}

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

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
using Xunit;
@ -28,27 +27,6 @@ public class OnFormFactorExtensionTests : XamlTestBase
}
}
[Fact]
public void Should_Resolve_Default_Value_From_Ctor()
{
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 ""Hello World"", Mobile=""Im Mobile""}' Margin='10' />
</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")]
@ -74,222 +52,6 @@ public class OnFormFactorExtensionTests : XamlTestBase
}
}
[Fact]
public void Should_Convert_Bcl_Type()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border Height='{OnFormFactor Default=50.1}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(50.1, border.Height);
}
}
[Fact]
public void Should_Convert_Avalonia_Type()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border Padding='{OnFormFactor Default=""10, 8, 10, 8""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(new Thickness(10, 8, 10, 8), border.Padding);
}
}
[Fact]
public void Should_Respect_Custom_TypeArgument()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock DataContext='{OnFormFactor Default=""10, 10, 10, 10"", x:TypeArguments=Thickness}'/>
</UserControl>";
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()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</UserControl.Resources>
<Border Background='{OnFormFactor Default={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 Should_Support_Xml_Syntax()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border>
<Border.Background>
<OnFormFactor>
<OnFormFactor.Default>
<SolidColorBrush Color='#ff506070' />
</OnFormFactor.Default>
</OnFormFactor>
</Border.Background>
</Border>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
}
[Fact]
public void Should_Support_Xml_Syntax_With_Custom_TypeArguments()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border>
<Border.Tag>
<OnFormFactor x:TypeArguments='Thickness' Default='10, 10, 10, 10' />
</Border.Tag>
</Border>
</UserControl>";
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_Control_Inside_Xml_Syntax()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<OnFormFactor>
<OnFormFactor.Default>
<Button Content='Hello World' />
</OnFormFactor.Default>
</OnFormFactor>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
}
[Fact]
public void Should_Support_Complex_Property_Setters()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<OnFormFactor x:Key='MyKey'>
<OnFormFactor.Default>
<Button Content='Hello World' />
</OnFormFactor.Default>
</OnFormFactor>
</ResourceDictionary>";
var resourceDictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<Button>(resourceDictionary["MyKey"]);
Assert.Equal("Hello World", button.Content);
}
}
[Fact]
public void BindingExtension_Works_Inside_Of_OnFormFactor()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(true, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foobar</x:String>
</UserControl.Resources>
<TextBlock Name='textBlock' Text='{OnFormFactor Default={CompiledBinding Source={StaticResource text}}}'/>
</UserControl>";
var window = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
Assert.Equal("foobar", textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly bool _isDesktop;

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

@ -1,5 +1,4 @@
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Platform;
using Xunit;
@ -28,28 +27,6 @@ public class OnPlatformExtensionTests : XamlTestBase
}
}
[Fact]
public void Should_Resolve_Default_Value_From_Ctor()
{
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 ""Hello World"", Android=""Im Android""}'
Margin='10' />
</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")]
@ -81,277 +58,6 @@ public class OnPlatformExtensionTests : XamlTestBase
}
}
[Fact]
public void Should_Convert_Bcl_Type()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border Height='{OnPlatform Windows=50.1}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(50.1, border.Height);
}
}
[Fact]
public void Should_Convert_Avalonia_Type()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border Padding='{OnPlatform Windows=""10, 8, 10, 8""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(new Thickness(10, 8, 10, 8), border.Padding);
}
}
[Fact]
public void Should_Respect_Custom_TypeArgument()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock DataContext='{OnPlatform Windows=""10, 10, 10, 10"", x:TypeArguments=Thickness}'/>
</UserControl>";
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()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</UserControl.Resources>
<Border Background='{OnPlatform Windows={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 Should_Allow_Nester_On_Platform_Markup_Extensions()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border Margin='{OnPlatform Windows={OnPlatform Linux=5, Windows=""10,10,10,10"", x:TypeArguments=Thickness}}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(new Thickness(10), border.Margin);
}
}
[Fact]
public void Should_Support_Xml_Syntax()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border>
<Border.Background>
<OnPlatform>
<OnPlatform.Windows>
<SolidColorBrush Color='#ff506070' />
</OnPlatform.Windows>
</OnPlatform>
</Border.Background>
</Border>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
}
[Fact]
public void Should_Support_Xml_Syntax_With_Custom_TypeArguments()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border>
<Border.Tag>
<OnPlatform x:TypeArguments='Thickness' Windows='10, 10, 10, 10' />
</Border.Tag>
</Border>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(new Thickness(10, 10, 10, 10), border.Tag);
}
}
[Theory]
[InlineData(OperatingSystemType.OSX, "#ff506070")]
[InlineData(OperatingSystemType.Linux, "#000")]
public void Should_Support_Special_On_Syntax(OperatingSystemType os, string color)
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(os));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border>
<Border.Background>
<OnPlatform>
<On Platform='Windows, macOS'>
<SolidColorBrush Color='#ff506070' />
</On>
<On Platform='Linux'>
<SolidColorBrush Color='#000' />
</On>
</OnPlatform>
</Border.Background>
</Border>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(Color.Parse(color), ((ISolidColorBrush)border.Background!).Color);
}
}
[Fact]
public void Should_Support_Control_Inside_Xml_Syntax()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<OnPlatform>
<OnPlatform.Windows>
<Button Content='Hello World' />
</OnPlatform.Windows>
</OnPlatform>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
}
[Fact]
public void Should_Support_Complex_Property_Setters()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<OnPlatform x:Key='MyKey'>
<OnPlatform.Windows>
<Button Content='Hello World' />
</OnPlatform.Windows>
</OnPlatform>
</ResourceDictionary>";
var resourceDictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<Button>(resourceDictionary["MyKey"]);
Assert.Equal("Hello World", button.Content);
}
}
[Fact]
public void BindingExtension_Works_Inside_Of_OnPlatform()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.WinNT));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foobar</x:String>
</UserControl.Resources>
<TextBlock Name='textBlock' Text='{OnPlatform Windows={CompiledBinding Source={StaticResource text}}}'/>
</UserControl>";
var window = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
Assert.Equal("foobar", textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly OperatingSystemType _operatingSystemType;

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

@ -0,0 +1,604 @@
using System;
using System.Reactive.Disposables;
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);
}
[Fact]
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);
}
[Fact]
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