Em abril de 2020, os desenvolvedores da plataforma .NET 5 anunciaram uma nova maneira de gerar código-fonte na linguagem de programação C # - usando uma implementação de interface ISourceGenerator. Este método permite que os desenvolvedores analisem o código personalizado e criem novos arquivos de origem em tempo de compilação. Ao mesmo tempo, a API dos novos geradores de código-fonte é semelhante à API dos analisadores Roslyn . Você pode gerar código usando a API do compilador Roslyn e concatenando strings comuns.
Neste artigo, vamos percorrer o processo de implementação ISourceGeneratorpara gerar referências digitadas para controles AvaloniaUI declarados em XAML. Durante o desenvolvimento, ensinaremos o gerador a compilar XAML usando a API do compilador XamlX usada em AvaloniaUI e o sistema de tipo XamlX implementado na API do modelo semântico Roslyn .
Formulação do problema
, , . , , AvaloniaUI — , — , XAML:
private TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");
TextBox PasswordTextBox XAML :
<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- (
Namex: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 - validação reativa e ligações;
- Avalonia para os mais pequenos - Larymar e Kontur falam sobre o quadro.