我们将实现IIncrementalGenerator,以便它生成我们所需的代码。
语法树
IIncrementalGenerator只有一个方法需要实现,BindablePropSG.cs应该是这样的:
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var fieldGroups = context.SyntaxProvider
.CreateSyntaxProvider(
// 一个告诉我们感兴趣的代码片段的函数
// 对于语法树中的每一个节点,都会应用这个函数来判断语法节点是否会在Transform阶段被处理
predicate: IsBindableProp,
// Transform 是一个从 predicate 阶段接收输出的函数
// 它的输入是上一阶段接受的节点
// 在这个阶段,我们只需要提取所有必要的信息来生成代码(字段名、数据类型等)
transform: Transform
)
.Where(item => item is not (null, null))
.Collect();
// fieldGroups 是所有具有 BindableProp 属性的字段
// Execute 是一个函数。 它将从 fieldGroups 生成代码
context.RegisterSourceOutput(fieldGroups, Execute);
}
}
}
在继续之前,让我简要解释一下生成器发生了什么。
编译器会将源代码文件解析为语法树,然后将其传递给我们的生成器。 然后生成器分析并创建新代码。 然后,将它们返回给编译器。
C# 文件语法树
我使用的是 Visual Studio 2022。如果找不到 Syntax Visualizer 窗口,请查看 Visual Installer 以安装 .NET 平台编译器 SDK。
我希望以上面的说明能让您了解发生了什么。 让我详细展示一下是如何实现所有功能的。
// BindableProp.cs:
namespace BindableProps
{
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class BindableProp : Attribute
{
public int DefaultBindingMode { get; set; }
public string ValidateValueDelegate { get; set; }
public string PropertyChangedDelegate { get; set; }
public string PropertyChangingDelegate { get; set; }
public string CoerceValueDelegate { get; set; }
public string CreateDefaultValueDelegate { get; set; }
public BindableProp()
{
}
}
}
// BindablePropsSG/Generators/BindablePropSG.cs
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}
// 寻找标有 BindableProp 属性的代码
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
if (node is not AttributeSyntax attributeSyntax)
{
return false;
}
var name = SyntaxUtil.ExtractName(attributeSyntax?.Name);
return name is "BindableProp" or "BindablePropAttribute";
}
}
}
// Utils/SyntaxUtil.cs
namespace BindablePropsSG.Utils
{
public class SyntaxUtil
{
public static string? ExtractName(NameSyntax? name)
{
return name switch
{
SimpleNameSyntax ins => ins.Identifier.Text,
QualifiedNameSyntax qns => qns.Right.Identifier.Text,
_ => null
};
}
}
}
现在我们拿到了所有 BindableProp 语法节点。 让我们提取必要的信息。
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}
// 注意,我们返回一个元组
// 对于那些不知道元组是什么的人,只要认为我返回一个包含 2 个项目的数组
private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// context对象包含很多东西,您可以调试和探索适合您需要的内容
// 就我而言,FieldDeclarationSyntax 和 IFieldSymbol 足以
// 这是从 IsBindableProp 筛选出来的语法节点
var attributeSyntax = (AttributeSyntax)context.Node;
// Attribute --> AttributeList --> Field
// 通过上面的语法树图像,您将看到对应信息
if (attributeSyntax.Parent?.Parent is not FieldDeclarationSyntax fieldSyntax)
return (null, null);
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(fieldSyntax.Declaration.Variables.FirstOrDefault()!) as IFieldSymbol;
return (fieldSyntax, fieldSymbol);
}
}
}
要为 BindableProperty.Create 函数生成代码。 这个需要:
- 字段名称
- 他的数据类型
- 类的名称
- 默认值
- 可选的绑定模式和一些委托函数
对于必选的参数,FieldDeclarationSyntax 和 IFieldSymbol 就足够了。 对于可选参数,让开发人员将它们输入到 BindableProp 的属性中。
// 这是开发人员使用所有设置时的示例
public partial class TextInput : ContentView
{
// 使用一些设置创建属性
[BindableProp(DefaultBindingMode = ((int)BindingMode.TwoWay))]
string text = "From every time";
// 完整设置
[BindableProp(
DefaultBindingMode = ((int)BindingMode.OneWay),
ValidateValueDelegate = nameof(ValidateValue),
PropertyChangedDelegate = nameof(PropertyChangedDelegate),
PropertyChangingDelegate = nameof(PropertyChangingDelegate),
CoerceValueDelegate = nameof(CoerceValueDelegate),
CreateDefaultValueDelegate = nameof(CreateDefaultValueDelegate)
)]
string placeHolder = "Always!";
static bool ValidateValue(BindableObject bindable, object value)
{
return true;
}
static void PropertyChangedDelegate(BindableObject bindable, object oldValue, object newValue)
{
// Do something
}
static void PropertyChangingDelegate(BindableObject bindable, object oldValue, object newValue)
{
// Do something
}
static object CoerceValueDelegate(BindableObject bindable, object value)
{
// Do something
return 0;
}
static object CreateDefaultValueDelegate(BindableObject bindable)
{
// Do something
return string.Empty;
}
}
我们收集了足够的原料。 现在,让我们把它们放在一起。
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}
private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}
private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// 转换后,得到一个元组
if (fieldSyntaxesAndSymbols.IsDefaultOrEmpty)
return;
// 这些字段可能来自不同或相同的类
// 我们需要按类别对它们进行分组
var groupList = fieldSyntaxesAndSymbols.GroupBy<(FieldDeclarationSyntax, IFieldSymbol), ClassDeclarationSyntax>(
fieldGroup => (ClassDeclarationSyntax)fieldGroup.Item1!.Parent!
);
// 如果上面的代码让你头疼,我很抱歉
// 几个月前,我写这个时也有同样的感觉
// 你只需要知道
// groupList 是map列表
// 它的形式类似于下面的代码:
// [
// { classDeclaration1, [tuple1, tuple2, ...] },
// { classDeclaration2, [tuple1, tuple2, ...] },
// ...
// ]
foreach (var group in groupList)
{
string sourceCode = ProcessClass(group.Key, group.ToList());
// 全名包括命名空间和类名
// 例如 MyMauiProject.Controls.Textlnput
var className = SyntaxUtil.GetClassFullname(group.Key);
// 注意,输出文件应该包含'g'字母
// 这样您的 IDE 就可以知道这是生成的代码
context.AddSource(#34;{className}.g.cs", sourceCode);
}
}
}
}
// BindablePropsSG 项目, Utils/SyntaxUtil.cs
namespace BindablePropsSG.Utils
{
public class SyntaxUtil
{
public static string? ExtractName(NameSyntax? name)
{
// ...
}
public static string GetClassFullname(TypeDeclarationSyntax source)
{
var namespaces = new LinkedList<BaseNamespaceDeclarationSyntax>();
var types = new LinkedList<TypeDeclarationSyntax>();
for (var parent = source.Parent; parent is object; parent = parent.Parent)
{
if (parent is BaseNamespaceDeclarationSyntax @namespace)
{
namespaces.AddFirst(@namespace);
}
else if (parent is TypeDeclarationSyntax type)
{
types.AddFirst(type);
}
}
var result = new StringBuilder();
for (var item = namespaces.First; item is object; item = item.Next)
{
result.Append(item.Value.Name).Append(".");
}
for (var item = types.First; item is object; item = item.Next)
{
var type = item.Value;
AppendName(result, type);
result.Append(".");
}
AppendName(result, source);
return result.ToString();
}
static void AppendName(StringBuilder builder, TypeDeclarationSyntax type)
{
builder.Append(type.Identifier.Text);
var typeArguments = type.TypeParameterList?.ChildNodes()
.Count(node => node is TypeParameterSyntax) ?? 0;
if (typeArguments != 0)
builder.Append(".").Append(typeArguments);
}
public static SyntaxNode? FindSyntaxBySymbol(SyntaxNode syntaxNode, ISymbol symbol)
{
var span = symbol.Locations.FirstOrDefault()!.SourceSpan;
var syntax = syntaxNode.FindNode(span);
return syntax;
}
}
}
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}
private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}
private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// ...
}
private string ProcessClass(ClassDeclarationSyntax classSyntax, List<(FieldDeclarationSyntax, IFieldSymbol)> fieldGroup)
{
if (classSyntax is null)
{
return string.Empty;
}
// 导入所有的包
var usingDirectives = classSyntax.SyntaxTree.GetCompilationUnitRoot().Usings;
// 如果类没有命名空间,我们将使用'global'
var namespaceSyntax = classSyntax.Parent as BaseNamespaceDeclarationSyntax;
var namespaceName = namespaceSyntax?.Name?.ToString() ?? "global";
// 放在一起
// 制作类的部分
var source = new StringBuilder($@"
// <auto-generated/>
{usingDirectives}
namespace {namespaceName}
{{
public partial class {classSyntax.Identifier}
{{
");
// 来到字段部分
// 为每个字段创建属性
foreach (var (fieldSynTax, fieldSymbol) in fieldGroup)
{
ProcessField(source, classSyntax, fieldSynTax, fieldSymbol);
}
// 不要忘记右括号
source.Append(@#34;
}}
}}
");
return source.ToString();
}
}
}
namespace BindablePropsSG.Generators
{
[Generator]
public class BindablePropSG : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// ...
}
private bool IsBindableProp(SyntaxNode node, CancellationToken _)
{
// ...
}
private (FieldDeclarationSyntax?, IFieldSymbol?) Transform(GeneratorSyntaxContext context, CancellationToken cancellationToken)
{
// ...
}
private void Execute(SourceProductionContext context, ImmutableArray<(FieldDeclarationSyntax?, IFieldSymbol?)> fieldSyntaxesAndSymbols)
{
// ...
}
private string ProcessClass(ClassDeclarationSyntax classSyntax, List<(FieldDeclarationSyntax, IFieldSymbol)> fieldGroup)
{
// ...
}
private void ProcessField(StringBuilder source, ClassDeclarationSyntax classSyntax, FieldDeclarationSyntax fieldSyntax, IFieldSymbol fieldSymbol)
{
var fieldName = fieldSymbol.Name;
// C#中的命名约定,字段名是驼峰式,属性名是pascal形式
// StringUtil.PascalCaseOf 位于 Utils/StringUtil.cs 中。 我稍后会展示它
var propName = StringUtil.PascalCaseOf(fieldName);
if (propName.Length == 0 || propName == fieldName)
{
return;
}
var fieldType = fieldSyntax.Declaration.Type;
var className = classSyntax.Identifier;
var defaultFieldValue = GetFieldDefaultValue(fieldSyntax) ?? "default";
var attributeSyntax = GetAttributeByName(fieldSyntax, "BindableProp");
var attributeArguments = attributeSyntax?.ArgumentList?.Arguments;
// 得到可选参数
var defaultBindingMode = GetAttributeParam(attributeArguments, "DefaultBindingMode") ?? "0";
var validateValueDelegate = GetAttributeParam(attributeArguments, "ValidateValueDelegate") ?? "null";
// 如果没有PropertyChangedDelegate
// 我们给他一个默认的
// 这将有助于属性与 MVVM 一起工作
var propertyChangedDelegate = GetAttributeParam(
attributeArguments, "PropertyChangedDelegate"
) ?? @#34;(bindable, oldValue, newValue) =>
(({className})bindable).{propName} = ({fieldType})newValue";
var propertyChangingDelegate = GetAttributeParam(attributeArguments, "PropertyChangingDelegate") ?? "null";
var coerceValueDelegate = GetAttributeParam(attributeArguments, "CoerceValueDelegate") ?? "null";
var createDefaultValueDelegate = GetAttributeParam(attributeArguments, "CreateDefaultValueDelegate") ?? "null";
// 检索所有必需和可选参数后,我们只需编写新的源代码
source.Append($@"
public static readonly BindableProperty {propName}Property = BindableProperty.Create(
nameof({propName}),
typeof({fieldType}),
typeof({className}),
{defaultFieldValue},
(BindingMode){defaultBindingMode},
{validateValueDelegate},
{propertyChangedDelegate},
{propertyChangingDelegate},
{coerceValueDelegate},
{createDefaultValueDelegate}
);
public {fieldType} {propName}
{{
get => {fieldName};
set
{{
OnPropertyChanging(nameof({propName}));
{fieldName} = value;
SetValue({className}.{propName}Property, {fieldName});
OnPropertyChanged(nameof({propName}));
}}
}}
");
}
string? GetAttributeParam(SeparatedSyntaxList<AttributeArgumentSyntax>? attributeArguments, string paramName)
{
var paramSyntax = attributeArguments?.FirstOrDefault(
attrArg => attrArg?.NameEquals?.Name.Identifier.Text == paramName
);
if (paramSyntax?.Expression is InvocationExpressionSyntax invocationExpressionSyntax)
{
return invocationExpressionSyntax.ArgumentList.Arguments.FirstOrDefault()?.ToString();
}
else if (paramSyntax?.Expression is LiteralExpressionSyntax literalExpressionSyntax)
{
return literalExpressionSyntax.Token.Value?.ToString();
}
return paramSyntax?.Expression.ToString();
}
string? GetFieldDefaultValue(FieldDeclarationSyntax fieldSyntax)
{
var variableDeclaration = fieldSyntax.DescendantNodesAndSelf()
.OfType<VariableDeclarationSyntax>()
.FirstOrDefault();
var variableDeclarator = variableDeclaration?.Variables.FirstOrDefault();
var initializer = variableDeclarator?.Initializer;
return initializer?.Value?.ToString();
}
AttributeSyntax? GetAttributeByName(FieldDeclarationSyntax fieldSyntax, string attributeName)
{
var attributeSyntax = fieldSyntax.AttributeLists
.FirstOrDefault(attrList =>
{
var attr = attrList.Attributes.FirstOrDefault();
return attr is not null && SyntaxUtil.ExtractName(attr.Name) == attributeName;
})
?.Attributes
.FirstOrDefault();
return attributeSyntax;
}
}
}
// Utils/StringUtil.cs
namespace BindablePropsSG.Utils
{
public class StringUtil
{
public static string PascalCaseOf(string fieldName)
{
fieldName = fieldName.TrimStart('_');
if (fieldName.Length == 0)
return string.Empty;
if (fieldName.Length == 1)
return fieldName.ToUpper();
return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
}
}
}
回顾
多么多的代码! 我试图以尽可能清晰的方式编写它们。 但是,可能仍然有许多您从未听说过的类和方法。 所以我来总结一下。
1.遍历抛出Syntax Tree,找到BindableProp属性节点
2.从我们找到的节点中,获取FieldDeclarationSyntax和IFieldSymbol
3.根据所属类别对它们进行分组
4.生成我们需要的代码并保存到文件中
在 MAUI 项目中使用时的样子
您可以尝试从单元测试项目运行或调试代码。