A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

579 lines
29 KiB

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static Avalonia.SourceGenerator.CompositionGenerator.Extensions;
namespace Avalonia.SourceGenerator.CompositionGenerator
{
public partial class Generator
{
private readonly ICompositionGeneratorSink _output;
private readonly GConfig _config;
private readonly HashSet<string> _objects;
private readonly HashSet<string> _brushes;
private readonly Dictionary<string, GManualClass> _manuals;
public Generator(ICompositionGeneratorSink output, GConfig config)
{
_output = output;
_config = config;
_manuals = _config.ManualClasses.ToDictionary(x => x.Name);
_objects = new HashSet<string>(_config.ManualClasses.Select(x => x.Name)
.Concat(_config.Classes.Select(x => x.Name)));
_brushes = new HashSet<string>(_config.Classes.OfType<GBrush>().Select(x => x.Name)) {"CompositionBrush"};
}
public void Generate()
{
foreach (var cl in _config.Classes)
GenerateClass(cl);
GenerateAnimations();
}
static string ServerName(string? c) => c != null ? ("Server" + c) : "ServerObject";
static string ChangesName(string? c) => c != null ? (c + "Changes") : "ChangeSet";
static string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields";
static string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name;
static string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst();
static string CompositionPropertyField(GProperty prop) => "s_IdOf" + prop.Name + "Property";
static ExpressionSyntax ClientProperty(GClass c, GProperty p) =>
MemberAccess(ServerName(c.Name), CompositionPropertyField(p));
void GenerateClass(GClass cl)
{
var list = cl as GList;
var unit = Unit();
var clientNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition"));
var serverNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Server"));
var transportNs = NamespaceDeclaration(IdentifierName("Avalonia.Rendering.Composition.Transport"));
var inherits = cl.Inherits ?? "CompositionObject";
var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null;
var visibilityModifier = cl.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword;
var client = ClassDeclaration(cl.Name)
.AddModifiers(abstractModifier)
.AddModifiers(visibilityModifier, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword)
.WithBaseType(inherits);
var serverName = ServerName(cl.Name);
var serverBase = cl.ServerBase ?? ServerName(cl.Inherits);
if (list != null)
serverBase = "ServerList<" + ServerName(list.ItemType) + ">";
var server = ClassDeclaration(serverName)
.AddModifiers(abstractModifier)
.AddModifiers(SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword)
.WithBaseType(serverBase);
string changesName = ChangesName(cl.Name);
string changedFieldsTypeName = ChangedFieldsTypeName(cl);
string changedFieldsName = ChangedFieldsFieldName(cl);
if (cl.Properties.Count > 0)
client = client
.AddMembers(DeclareField(changedFieldsTypeName, changedFieldsName));
if (!cl.CustomCtor)
{
client = client.AddMembers(PropertyDeclaration(ParseTypeName(serverName), "Server")
.AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword)
.AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
.WithSemicolonToken(Semicolon())));
client = client.AddMembers(
ConstructorDeclaration(cl.Name)
.AddModifiers(SyntaxKind.InternalKeyword)
.WithParameterList(ParameterList(SeparatedList(new[]
{
Parameter(Identifier("compositor")).WithType(ParseTypeName("Compositor")),
Parameter(Identifier("server")).WithType(ParseTypeName(serverName)),
})))
.WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer,
ArgumentList(SeparatedList(new[]
{
Argument(IdentifierName("compositor")),
Argument(IdentifierName("server")),
})))).WithBody(Block(
ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName("Server"),
CastExpression(ParseTypeName(serverName), IdentifierName("server")))),
ExpressionStatement(InvocationExpression(IdentifierName("InitializeDefaults")))
)));
}
if (!cl.CustomServerCtor)
{
server = server.AddMembers(
ConstructorDeclaration(serverName)
.AddModifiers(SyntaxKind.InternalKeyword)
.WithParameterList(ParameterList(SeparatedList(new[]
{
Parameter(Identifier("compositor")).WithType(ParseTypeName("ServerCompositor")),
})))
.WithInitializer(ConstructorInitializer(SyntaxKind.BaseConstructorInitializer,
ArgumentList(SeparatedList(new[]
{
Argument(IdentifierName("compositor")),
})))).WithBody(Block(ParseStatement("Initialize();"))));
}
server = server.AddMembers(
MethodDeclaration(ParseTypeName("void"), "Initialize")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
server = server.AddMembers(
MethodDeclaration(ParseTypeName("void"), "DeserializeChangesExtra")
.AddParameterListParameters(Parameter(Identifier("c")).WithType(ParseTypeName("BatchStreamReader")))
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
var resetBody = Block();
var startAnimationBody = Block();
var serverGetPropertyBody = Block();
var serverGetCompositionPropertyBody = Block();
var serializeMethodBody = SerializeChangesPrologue(cl);
var deserializeMethodBody = DeserializeChangesPrologue(cl);
var defaultsMethodBody = Block(ParseStatement("InitializeDefaultsExtra();"));
foreach (var prop in cl.Properties)
{
var fieldName = PropertyBackingFieldName(prop);
var typeInfo = GetTypeInfo(prop.Type);
var (propType, filteredPropertyType, isObject, isNullable, isPassthrough,
serverPropertyType) = (typeInfo.RoslynType,
typeInfo.FilteredTypeName,
typeInfo.IsObject, typeInfo.IsNullable, typeInfo.IsPassthrough, typeInfo.ServerType);
var animatedServer = prop.Animated;
client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable);
if (animatedServer)
server = server.AddMembers(
DeclareField(serverPropertyType, fieldName),
PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name)
.AddModifiers(SyntaxKind.PublicKeyword)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithExpressionBody(
ArrowExpressionClause(IdentifierName(fieldName))).WithSemicolonToken(Semicolon()),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration)
.WithExpressionBody(ArrowExpressionClause(
ParseExpression($"SetAnimatedValue({CompositionPropertyField(prop)}, out {PropertyBackingFieldName(prop)}, value)")))
.WithSemicolonToken(Semicolon())));
else
{
server = server
.AddMembers(DeclareField(serverPropertyType, fieldName))
.AddMembers(PropertyDeclaration(ParseTypeName(serverPropertyType), prop.Name)
.AddModifiers(SyntaxKind.PublicKeyword)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration,
Block(ReturnStatement(IdentifierName(fieldName)))),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration,
Block(
ParseStatement("var changed = false;"),
IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression,
IdentifierName(fieldName),
IdentifierName("value")),
Block(
ParseStatement("On" + prop.Name + "Changing();"),
ParseStatement($"changed = true;"))
),
ExpressionStatement(InvocationExpression(IdentifierName("SetValue"),
ArgumentList(SeparatedList(new[]{
Argument(IdentifierName(CompositionPropertyField(prop))),
Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)),
Argument(IdentifierName("value"))
}
)))),
ParseStatement($"if(changed) On" + prop.Name + "Changed();")
))
))
.AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()))
.AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
}
resetBody = resetBody.AddStatements(
ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset"))));
serializeMethodBody = ApplySerializeField(serializeMethodBody, cl, prop, isObject, isPassthrough);
deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject);
if (animatedServer)
{
startAnimationBody = ApplyStartAnimation(startAnimationBody, cl, prop);
}
serverGetPropertyBody = ApplyGetProperty(serverGetPropertyBody, prop);
serverGetCompositionPropertyBody = ApplyGetProperty(serverGetCompositionPropertyBody, prop, CompositionPropertyField(prop));
string compositionPropertyVariantGetter = "null";
if(VariantPropertyTypes.Contains(prop.Type))
compositionPropertyVariantGetter = $"obj => (({serverName})obj).{fieldName}";
server = server.AddMembers(DeclareField($"CompositionProperty<{serverPropertyType}>", CompositionPropertyField(prop),
EqualsValueClause(ParseExpression(
$"CompositionProperty.Register<{serverName}, {serverPropertyType}>(\"{prop.Name}\", obj => (({serverName})obj).{fieldName}, (obj, v) => (({serverName})obj).{fieldName} = v, {compositionPropertyVariantGetter})")),
SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword));
if (prop.DefaultValue != null)
{
defaultsMethodBody = defaultsMethodBody.AddStatements(
ExpressionStatement(
AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(prop.Name), ParseExpression(prop.DefaultValue))));
}
}
if (cl.Properties.Count > 0)
{
server = server.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration(
$"protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt){{}}")
!)
.WithBody(ApplyDeserializeChangesEpilogue(deserializeMethodBody, cl)));
server = server.AddMembers(MethodDeclaration(ParseTypeName("void"), "OnFieldsDeserialized")
.WithParameterList(ParameterList(SingletonSeparatedList(Parameter(Identifier("changed"))
.WithType(ParseTypeName(ChangedFieldsTypeName(cl))))))
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
}
client = client.AddMembers(
MethodDeclaration(ParseTypeName("void"), "InitializeDefaults").WithBody(defaultsMethodBody))
.AddMembers(
MethodDeclaration(ParseTypeName("void"), "InitializeDefaultsExtra")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
if (cl.Properties.Count > 0)
{
serializeMethodBody = serializeMethodBody.AddStatements(SerializeChangesEpilogue(cl));
client = client.AddMembers(((MethodDeclarationSyntax)ParseMemberDeclaration(
$"private protected override void SerializeChangesCore(BatchStreamWriter writer){{}}")!)
.WithBody(serializeMethodBody));
}
if (list != null)
client = AppendListProxy(list, client);
if (startAnimationBody.Statements.Count != 0)
client = WithStartAnimation(client, startAnimationBody);
if (!cl.ServerOnly)
{
//server = WithGetPropertyForAnimation(server, serverGetPropertyBody);
server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody);
}
if (cl.ServerOnly)
server = server.AddMembers(GenerateSerializeAllMethod(cl));
if(cl.Implements.Count > 0)
foreach (var impl in cl.Implements)
{
client = client.WithBaseList(client.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.Name))));
if (impl.ServerName != null)
server = server.WithBaseList(
server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName))));
if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member)
client = client.AddMembers(member);
}
SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport",
ChangedFieldsTypeName(cl) + ".generated.cs");
if (!cl.ServerOnly)
SaveTo(unit.AddMembers(clientNs.AddMembers(client)),
cl.Name + ".generated.cs");
SaveTo(unit.AddMembers(serverNs.AddMembers(server)),
"Server", "Server" + cl.Name + ".generated.cs");
}
private static ClassDeclarationSyntax GenerateClientProperty(ClassDeclarationSyntax client, GClass cl, GProperty prop,
TypeSyntax propType, bool isObject, bool isNullable)
{
var fieldName = PropertyBackingFieldName(prop);
return client
.AddMembers(DeclareField(prop.Type, fieldName))
.AddMembers(PropertyDeclaration(propType, prop.ClientName ?? prop.Name)
.AddModifiers(
prop.Private ? SyntaxKind.PrivateKeyword : prop.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword)
.AddAccessorListAccessors(
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration,
Block(ReturnStatement(IdentifierName(fieldName)))),
AccessorDeclaration(SyntaxKind.SetAccessorDeclaration,
Block(
ParseStatement("var changed = false;"),
IfStatement(BinaryExpression(SyntaxKind.NotEqualsExpression,
IdentifierName(fieldName),
IdentifierName("value")),
Block(
ParseStatement("On" + prop.Name + "Changing();"),
ParseStatement("changed = true;"),
GeneratePropertySetterAssignment(cl, prop, isObject, isNullable))
),
ExpressionStatement(AssignmentExpression(SyntaxKind.SimpleAssignmentExpression,
IdentifierName(fieldName), IdentifierName("value"))),
ParseStatement($"if(changed) On" + prop.Name + "Changed();")
)).WithModifiers(TokenList(prop.InternalSet ? new[]{Token(SyntaxKind.InternalKeyword)} : Array.Empty<SyntaxToken>()))
))
.AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changed")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()))
.AddMembers(MethodDeclaration(ParseTypeName("void"), "On" + prop.Name + "Changing")
.AddModifiers(SyntaxKind.PartialKeyword).WithSemicolonToken(Semicolon()));
}
static EnumDeclarationSyntax GenerateChangedFieldsEnum(GClass cl)
{
var changedFieldsEnum = EnumDeclaration(Identifier(ChangedFieldsTypeName(cl)));
int count = 0;
void AddValue(string name)
{
var value = 1ul << count;
changedFieldsEnum = changedFieldsEnum.AddMembers(
EnumMemberDeclaration(name)
.WithEqualsValue(EqualsValueClause(ParseExpression(value.ToString()))));
count++;
}
foreach (var prop in cl.Properties)
{
AddValue(prop.Name);
if (prop.Animated)
AddValue(prop.Name + "Animated");
}
var baseType = count <= 8 ? "byte" : count <= 16 ? "ushort" : count <= 32 ? "uint" : "ulong";
return changedFieldsEnum.AddBaseListTypes(SimpleBaseType(ParseTypeName(baseType)))
.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("System.Flags")))));
}
static StatementSyntax GeneratePropertySetterAssignment(GClass cl, GProperty prop, bool isObject, bool isNullable)
{
var code = @$"
// Update the backing value
{PropertyBackingFieldName(prop)} = value;
// Register object for serialization in the next batch
{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name};
RegisterForSerialization();
";
if (prop.Animated)
{
code += @$"
// Reset previous animation if any
PendingAnimations.Remove({ClientProperty(cl, prop)});
{ChangedFieldsFieldName(cl)} &= ~{ChangedFieldsTypeName(cl)}.{prop.Name}Animated;
// Check for implicit animations
if(ImplicitAnimations != null && ImplicitAnimations.TryGetValue(""{prop.Name}"", out var animation) == true)
{{
// Animation affects only current property
if(animation is CompositionAnimation a)
{{
{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated;
PendingAnimations[{ClientProperty(cl, prop)}] = a.CreateInstance(this.Server, value);
}}
// Animation is triggered by the current field, but does not necessary affects it
StartAnimationGroup(animation, ""{prop.Name}"", value);
}}
";
}
return ParseStatement("{\n" + code + "\n}");
}
static BlockSyntax ApplyStartAnimation(BlockSyntax body, GClass cl, GProperty prop)
{
var code = $@"
if (propertyName == ""{prop.Name}"")
{{
var current = {PropertyBackingFieldName(prop)};
var server = animation.CreateInstance(this.Server, finalValue);
PendingAnimations[{ClientProperty(cl, prop)}] = server;
{ChangedFieldsFieldName(cl)} |= {ChangedFieldsTypeName(cl)}.{prop.Name}Animated;
RegisterForSerialization();
return;
}}
";
return body.AddStatements(ParseStatement(code));
}
private static HashSet<string> VariantPropertyTypes = new HashSet<string>
{
"bool",
"float",
"Vector2",
"Vector3",
"Vector4",
"Matrix",
"Matrix3x2",
"Matrix4x4",
"Quaternion",
"Color",
"Avalonia.Media.Color",
"Vector3D"
};
static BlockSyntax ApplyGetProperty(BlockSyntax body, GProperty prop, string? expr = null)
{
if (VariantPropertyTypes.Contains(prop.Type))
return body.AddStatements(
ParseStatement($"if(name == \"{prop.Name}\")\n return {expr ?? prop.Name};\n")
);
return body;
}
private static BlockSyntax SerializeChangesPrologue(GClass cl)
{
return Block(
ParseStatement("base.SerializeChangesCore(writer);"),
ParseStatement($"writer.Write({ChangedFieldsFieldName(cl)});")
);
}
private MethodDeclarationSyntax GenerateSerializeAllMethod(GClass cl)
{
var declaration = (MethodDeclarationSyntax)ParseMemberDeclaration(
$"internal static void SerializeAllChanges(BatchStreamWriter writer){{}}")!;
declaration = declaration.AddParameterListParameters(cl.Properties.Select(prop =>
{
var type = GetTypeInfo(prop.Type);
return Parameter(Identifier(prop.Name.WithLowerFirst())).WithType(ParseTypeName(type.ServerType));
}).ToArray());
var changedName = ChangedFieldsTypeName(cl);
var bits = cl.Properties.Select(p => changedName + "." + p.Name);
var body = Block().AddStatements(ParseStatement($"writer.Write({string.Join("|", bits)});"));
foreach (var prop in cl.Properties)
{
var type = GetTypeInfo(prop.Type);
body = body.AddStatements(
ParseStatement($"writer.Write{(type.IsObject ? "Object" : "")}({prop.Name.WithLowerFirst()});"));
}
return declaration.WithBody(body);
}
private static BlockSyntax SerializeChangesEpilogue(GClass cl) =>
Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;"));
static BlockSyntax ApplySerializeField(BlockSyntax body, GClass cl, GProperty prop, bool isObject, bool isPassthrough)
{
var changedFields = ChangedFieldsFieldName(cl);
var changedFieldsType = ChangedFieldsTypeName(cl);
var code = "";
if (prop.Animated)
{
code = $@"
if(({changedFields} & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated)
writer.WriteObject(PendingAnimations.GetAndRemove({ClientProperty(cl, prop)}));
else ";
}
code += $@"
if(({changedFields} & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name})
writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")});
";
return body.AddStatements(ParseStatement(code));
}
private static BlockSyntax DeserializeChangesPrologue(GClass cl)
{
return Block(
ParseStatement("base.DeserializeChangesCore(reader, committedAt);"),
ParseStatement("DeserializeChangesExtra(reader);"),
ParseStatement($"var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();")
);
}
private static BlockSyntax ApplyDeserializeChangesEpilogue(BlockSyntax body, GClass cl)
{
return body.AddStatements(ParseStatement("OnFieldsDeserialized(changed);"));
}
static BlockSyntax ApplyDeserializeField(BlockSyntax body, GClass cl, GProperty prop, string serverType, bool isObject)
{
var changedFieldsType = ChangedFieldsTypeName(cl);
var code = "";
if (prop.Animated)
{
code = $@"
if((changed & {changedFieldsType}.{prop.Name}Animated) == {changedFieldsType}.{prop.Name}Animated)
SetAnimatedValue({CompositionPropertyField(prop)}, ref {PropertyBackingFieldName(prop)}, committedAt, reader.ReadObject<IAnimationInstance>());
else ";
}
var readValueCode = $"reader.Read{(isObject ? "Object" : "")}<{serverType}>()";
code += $@"
if((changed & {changedFieldsType}.{prop.Name}) == {changedFieldsType}.{prop.Name})
";
code += $"{prop.Name} = {readValueCode};";
return body.AddStatements(ParseStatement(code));
}
static ClassDeclarationSyntax WithGetPropertyForAnimation(ClassDeclarationSyntax cl, BlockSyntax body)
{
if (body.Statements.Count == 0)
return cl;
body = body.AddStatements(
ParseStatement("return base.GetPropertyForAnimation(name);"));
var method = ((MethodDeclarationSyntax) ParseMemberDeclaration(
$"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")!)
.WithBody(body);
return cl.AddMembers(method);
}
static ClassDeclarationSyntax WithGetCompositionProperty(ClassDeclarationSyntax cl, BlockSyntax body)
{
if (body.Statements.Count == 0)
return cl;
body = body.AddStatements(
ParseStatement("return base.GetCompositionProperty(name);"));
var method = ((MethodDeclarationSyntax)ParseMemberDeclaration(
$"public override CompositionProperty? GetCompositionProperty(string name){{}}")!)
.WithBody(body);
return cl.AddMembers(method);
}
static ClassDeclarationSyntax WithStartAnimation(ClassDeclarationSyntax cl, BlockSyntax body)
{
body = body.AddStatements(
ExpressionStatement(InvocationExpression(MemberAccess("base", "StartAnimation"),
ArgumentList(SeparatedList(new[]
{
Argument(IdentifierName("propertyName")),
Argument(IdentifierName("animation")),
Argument(IdentifierName("finalValue")),
}))))
);
return cl.AddMembers(
((MethodDeclarationSyntax) ParseMemberDeclaration(
"internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")!)
.WithBody(body));
}
}
}