2020年4月,.NET 5平台的开发人员宣布了一种使用C#编程语言生成源代码的新方法-使用接口实现ISourceGenerator
。此方法使开发人员可以在编译时分析自定义代码并创建新的源文件。同时,新的源代码生成器的API与Roslyn分析器的API相似。可以使用Roslyn Compiler API或通过串联普通字符串来生成代码。
在本文中,我们将ISourceGenerator
逐步介绍实现过程,以生成对XAML中声明的AvaloniaUI控件的类型化引用。在开发过程中,我们会教发电机使用编译XAML XamlX编译器API中使用AvaloniaUI和XamlX类型系统的基础上实现的罗斯林语义模型API 。
问题的提法
新的源代码生成器可以优雅地解决各种各样的问题,包括生成不太有趣且完全无法人工编写的样板代码。例如,在使用AvaloniaUI的应用程序中-一种用于开发具有图形界面的跨平台应用程序的框架(最近在Habré上发表了关于该框架的文章),编写以下代码来引用XAML中声明的控件并不少见:
private TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");
然后,在XAML中声明TextBox
命名的type元素PasswordTextBox
,如下所示:
<TextBox x:Name="PasswordTextBox"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
XAML , , ReactiveUI, , Bind
, BindCommand
, BindValidation
, View ViewModel {Binding}
XAML-.
public class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
// ReactiveUI ReactiveUI.Validation.
// Binding,
// C#.
// ( ) ?
//
this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text);
}
//
// , XAML.
TextBox UserNameTextBox => this.FindControl<TextBox>("UserNameTextBox");
TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");
TextBlock CompoundValidation => this.FindControl<TextBlock>("CompoundValidation");
}
, XAML-, SignUpView
, . , , , , — , XAML, .
, , , XAML-, , - , . , , (, , ).
, . SignUpView
, XAML- SignUpView.xaml
, code-behind SignUpView.xaml.cs
, . , SignUpView.xaml
:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.NameGenerator.Sandbox.Views.SignUpView">
<StackPanel>
<TextBox x:Name="UserNameTextBox"
Watermark="Please, enter user name..."
UseFloatingWatermark="True" />
<TextBlock Name="UserNameValidation"
Foreground="Red"
FontSize="12" />
</StackPanel>
</Window>
SignUpView.xaml.cs
:
public partial class SignUpView : Window
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
// ,
// , , :
UserNameTextBox.Text = "Violet Evergarden";
UserNameValidation.Text = "An optional validation error message";
}
}
SignUpView.xaml.cs
:
partial class SignUpView
{
internal global::Avalonia.Controls.TextBox UserNameTextBox => this.FindControl<global::Avalonia.Controls.TextBox>("UserNameTextBox");
internal global::Avalonia.Controls.TextBlock UserNameValidation => this.FindControl<global::Avalonia.Controls.TextBlock>("UserNameValidation");
}
global::
. , . WPF, internal
. partial
- partial
-, — Window
, ReactiveWindow<TViewModel>
, .
, FindControl
— Avalonia , INameScope
Avalonia. , FindControl FindNameScope GitHub.
ISourceGenerator
[Generator]
public class EmptyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context) { }
}
Initialize
, Execute
— , context.AddSource(fileName, sourceText)
. , :
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>preview</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include="Microsoft.CodeAnalysis.CSharp"
Version="3.8.0-5.final"
PrivateAssets="all" />
<PackageReference
Include="Microsoft.CodeAnalysis.Analyzers"
Version="3.3.1"
PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll"
Pack="true"
PackagePath="analyzers/dotnet/cs"
Visible="false" />
</ItemGroup>
</Project>
, , , , , , Avalonia, XAML. :
[Generator]
public class NameReferenceGenerator : ISourceGenerator
{
private const string AttributeName = "GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute.g.cs";
private const string AttributeCode = @"// <auto-generated />
using System;
[AttributeUsage(AttributeTargets.Class, Inherited=false, AllowMultiple=false)]
internal sealed class GenerateTypedNameReferencesAttribute : Attribute { }
";
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
// 'GenerateTypedNameReferencesAttribute.cs'
// , .
context.AddSource(AttributeFile,
SourceText.From(
AttributeCode, Encoding.UTF8));
}
}
— , , , SourceText.From(code)
, context.AddSource(fileName, sourceText)
. , , [GenerateTypedNameReferences]
. , , , XAML. SignUpView.xaml
, code-behind :
[GenerateTypedNameReferences]
public partial class SignUpView : Window
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
// .
// , ().
// UserNameTextBox.Text = "Violet Evergarden";
// UserNameValidation.Text = "An optional validation error message";
}
}
ISourceGenerator
:
- ,
[GenerateTypedNameReferences]
; - XAML-;
- , XAML-;
- XAML- (
Name
x:Name
) ; -
partial
- .
,
API ISyntaxReceiver
, . ISyntaxReceiver
, :
internal class NameReferenceSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } =
new List<ClassDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax &&
classDeclarationSyntax.AttributeLists.Count > 0)
CandidateClasses.Add(classDeclarationSyntax);
}
}
ISourceGenerator.Initialize(GeneratorInitializationContext context)
:
context.RegisterForSyntaxNotifications(() => new NameReferenceSyntaxReceiver());
, , ClassDeclarationSyntax
, , :
// CSharpCompilation .
var options = (CSharpParseOptions)existingCompilation.SyntaxTrees[0].Options;
var compilation = existingCompilation.AddSyntaxTrees(CSharpSyntaxTree
.ParseText(SourceText.From(AttributeCode, Encoding.UTF8), options));
var attributeSymbol = compilation.GetTypeByMetadataName(AttributeName);
var symbols = new List<INamedTypeSymbol>();
foreach (var candidateClass in nameReferenceSyntaxReceiver.CandidateClasses)
{
// INamedTypeSymbol -.
var model = compilation.GetSemanticModel(candidateClass.SyntaxTree);
var typeSymbol = (INamedTypeSymbol) model.GetDeclaredSymbol(candidateClass);
// , .
var relevantAttribute = typeSymbol!
.GetAttributes()
.FirstOrDefault(attr => attr.AttributeClass!.Equals(
attributeSymbol, SymbolEqualityComparer.Default));
if (relevantAttribute == null) {
continue;
}
// , 'partial'.
var isPartial = candidateClass
.Modifiers
.Any(modifier => modifier.IsKind(SyntaxKind.PartialKeyword));
// , 'symbols'
// , 'partial'
// 'GenerateTypedNameReferences'.
if (isPartial) {
symbols.Add(typeSymbol);
}
}
XAML-
Avalonia XAML- code-behind . SignUpView.xaml
code-behind SignUpView.xaml.cs
, , , SignUpView
. . Avalonia .xaml
.axaml
, , XAML- :
var xamlFileName = $"{typeSymbol.Name}.xaml";
var aXamlFileName = $"{typeSymbol.Name}.axaml";
var relevantXamlFile = context
.AdditionalFiles
.FirstOrDefault(text =>
text.Path.EndsWith(xamlFileName) ||
text.Path.EndsWith(aXamlFileName));
, typeSymbol
INamedTypeSymbol
symbols
, . . AdditionalFiles
, MSBuild <AdditionalFiles />
. , .csproj
, <ItemGroup />
:
<ItemGroup>
<!-- ,
! -->
<AdditionalFiles Include="**\*.xaml" />
</ItemGroup>
<AdditionalFiles />
New C# Source Generator Samples.
XAML
, . , , , XAML-. , - , , , .
, AvaloniaUI XamlX, @kekekeks. , -, XAML , XAML WPF, UWP, XF , API XAML . , XamlX (git submodule add ://repo ./path
), MiniCompiler
, XAML , - . XamlX.XamlCompiler MiniCompiler
, XAML-, :
internal sealed class MiniCompiler : XamlCompiler<object, IXamlEmitResult>
{
public static MiniCompiler CreateDefault(
RoslynTypeSystem typeSystem,
params string[] additionalTypes)
{
var mappings = new XamlLanguageTypeMappings(typeSystem);
foreach (var additionalType in additionalTypes)
mappings.XmlnsAttributes.Add(typeSystem.GetType(additionalType));
var configuration = new TransformerConfiguration(
typeSystem,
typeSystem.Assemblies[0],
mappings);
return new MiniCompiler(configuration);
}
private MiniCompiler(TransformerConfiguration configuration)
: base(configuration,
new XamlLanguageEmitMappings<object, IXamlEmitResult>(),
false)
{
// AST XamlX
// , .
Transformers.Add(new NameDirectiveTransformer());
Transformers.Add(new DataTemplateTransformer());
Transformers.Add(new KnownDirectivesTransformer());
Transformers.Add(new XamlIntrinsicsTransformer());
Transformers.Add(new XArgumentsTransformer());
Transformers.Add(new TypeReferenceResolver());
}
protected override XamlEmitContext<object, IXamlEmitResult> InitCodeGen(
IFileSource file,
Func<string, IXamlType, IXamlTypeBuilder<object>> createSubType,
object codeGen, XamlRuntimeContext<object, IXamlEmitResult> context,
bool needContextLocal) =>
throw new NotSupportedException();
}
MiniCompiler
XamlX, DataTemplateTransformer
, NameDirectiveTransformer
, Avalonia, XAML- x:Name
XAML- Name
, AST . NameDirectiveTransformer
:
internal class NameDirectiveTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
// .
if (node is XamlAstObjectNode objectNode)
{
for (var index = 0; index < objectNode.Children.Count; index++)
{
// x:Name, Name
// XamlAstObjectNode .
var child = objectNode.Children[index];
if (child is XamlAstXmlDirective directive &&
directive.Namespace == XamlNamespaces.Xaml2006 &&
directive.Name == "Name")
objectNode.Children[index] =
new XamlAstXamlPropertyValueNode(
directive,
new XamlAstNamePropertyReference(
directive, objectNode.Type, "Name", objectNode.Type),
directive.Values);
}
}
return node;
}
}
DataTemplateTransformer
, , XAML, <DataTemplate />
. AvaloniaUI , , — x:Name
. DataTemplateTransformer
:
internal class DataTemplateTransformer : IXamlAstTransformer
{
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode &&
objectNode.Type is XamlAstXmlTypeReference typeReference &&
(typeReference.Name == "DataTemplate" ||
typeReference.Name == "ControlTemplate"))
objectNode.Children.Clear(); // .
return node;
}
}
MiniCompiler.CreateDefault
RoslynTypeSystem
, XamlX. IXamlTypeSystem
, , . , XamlX API Roslyn. IXamlTypeSystem
- (IXamlType
, IXamlAssembly
, IXamlMethod
, IXamlProperty
). IXamlAssembly
, , :
public class RoslynAssembly : IXamlAssembly
{
private readonly IAssemblySymbol _symbol;
public RoslynAssembly(IAssemblySymbol symbol) => _symbol = symbol;
public bool Equals(IXamlAssembly other) =>
other is RoslynAssembly roslynAssembly &&
SymbolEqualityComparer.Default.Equals(_symbol, roslynAssembly._symbol);
public string Name => _symbol.Name;
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes =>
_symbol.GetAttributes()
.Select(data => new RoslynAttribute(data, this))
.ToList();
public IXamlType FindType(string fullName)
{
var type = _symbol.GetTypeByMetadataName(fullName);
return type is null ? null : new RoslynType(type, this);
}
}
XAML XamlX, RoslynTypeSystem
, CSharpCompilation
, , AST AST :
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());
MiniCompiler.CreateDefault(
// 'compilation' 'CSharpCompilation'
new RoslynTypeSystem(compilation),
"Avalonia.Metadata.XmlnsDefinitionAttribute")
.Transform(parsed);
! — .
XAML
AST XamlX, IXamlAstTransformer
, AST, IXamlAstVisitor
. :
internal sealed class NameReceiver : IXamlAstVisitor
{
private readonly List<(string TypeName, string Name)> _items =
new List<(string TypeName, string Name)>();
public IReadOnlyList<(string TypeName, string Name)> Controls => _items;
public IXamlAstNode Visit(IXamlAstNode node)
{
if (node is XamlAstObjectNode objectNode)
{
// AST-. XamlX
// RoslynTypeSystem.
//
var clrType = objectNode.Type.GetClrType();
foreach (var child in objectNode.Children)
{
// ,
// 'Name', 'Name' ,
// '_items' CLR- AST.
//
if (child is XamlAstXamlPropertyValueNode propertyValueNode &&
propertyValueNode.Property is XamlAstNamePropertyReference named &&
named.Name == "Name" &&
propertyValueNode.Values.Count > 0 &&
propertyValueNode.Values[0] is XamlAstTextNode text)
{
var nsType = $@"{clrType.Namespace}.{clrType.Name}";
var typeNamePair = (nsType, text.Text);
if (!_items.Contains(typeNamePair))
_items.Add(typeNamePair);
}
}
return node;
}
return node;
}
public void Push(IXamlAstNode node) { }
public void Pop() { }
}
XAML XAML- :
var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>());
MiniCompiler.CreateDefault(
// 'compilation' 'CSharpCompilation'
new RoslynTypeSystem(compilation),
"Avalonia.Metadata.XmlnsDefinitionAttribute")
.Transform(parsed);
var visitor = new NameReceiver();
parsed.Root.Visit(visitor);
parsed.Root.VisitChildren(visitor);
// , .
var controls = visitor.Controls;
, . , — , , . , partial
-, , XAML. , partial
-, :
private static string GenerateSourceCode(
List<(string TypeName, string Name)> controls,
INamedTypeSymbol classSymbol,
AdditionalText xamlFile)
{
var className = classSymbol.Name;
var nameSpace = classSymbol.ContainingNamespace
.ToDisplayString(SymbolDisplayFormat);
var namedControls = controls
.Select(info => " " +
$"internal global::{info.TypeName} {info.Name} => " +
$"this.FindControl<global::{info.TypeName}>(\"{info.Name}\");");
return $@"// <auto-generated />
using Avalonia.Controls;
namespace {nameSpace}
{{
partial class {className}
{{
{string.Join("\n", namedControls)}
}}
}}
";
}
GeneratorExecutionContext
:
var sourceCode = GenerateSourceCode(controls, symbol, relevantXamlFile);
context.AddSource($"{symbol.Name}.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
!
Visual Studio , XAML-, <AdditionalFile />
, , . , XAML-, , XAML , C#- .xaml.cs
.
JetBrains Rider ReSharper EAP, , , Windows, Linux, macOS. Avalonia, . , ReactiveUI.Validation:
[GenerateTypedNameReferences]
public class SignUpView : ReactiveWindow<SignUpViewModel>
{
public SignUpView()
{
AvaloniaXamlLoader.Load(this);
this.Bind(ViewModel, x => x.Username, x => x.UserNameTextBox.Text);
this.Bind(ViewModel, x => x.Password, x => x.PasswordTextBox.Text);
this.BindValidation(ViewModel, x => x.CompoundValidation.Text);
}
}
- XamlX — XAML- ;
- Avalonia.NameGenerator —
x:Name
, ; - Avalonia — UI-;
- ReactiveUI.Validation-反应性验证和绑定;
- 小孩子们的Avalonia - Larymar和Kontur讨论了框架。