使用C#源生成器在XAML中使用x:Name属性生成对Avalonia控件的类型化引用





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 :



  1. , [GenerateTypedNameReferences];
  2. XAML-;
  3. , XAML-;
  4. XAML- ( Name x:Name) ;
  5. 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.



ezgif-1-f52e7303c26f



GitHub.



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);
    }
}







All Articles