18 changed files with 1143 additions and 1150 deletions
@ -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 |
|||
{ |
|||
|
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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); |
|||
} |
|||
} |
|||
|
|||
@ -1 +1 @@ |
|||
Subproject commit afbc488cedf0f20be554c3b63e48689b70b5dbf6 |
|||
Subproject commit dab9e09fa56064be92d80c7c4328793fb0f93dbf |
|||
@ -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> {} |
|||
@ -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…
Reference in new issue