Génération de références typées aux contrÎles Avalonia avec l'attribut x: Name en XAML à l'aide de générateurs de source C #





En avril 2020, les dĂ©veloppeurs de la plate-forme .NET 5 ont annoncĂ© une nouvelle façon de gĂ©nĂ©rer du code source dans le langage de programmation C # - Ă  l'aide d'une implĂ©mentation d'interface ISourceGenerator. Cette mĂ©thode permet aux dĂ©veloppeurs d'analyser le code personnalisĂ© et de crĂ©er de nouveaux fichiers source au moment de la compilation. Dans le mĂȘme temps, l'API des nouveaux gĂ©nĂ©rateurs de code source est similaire Ă  l'API des analyseurs Roslyn . Vous pouvez gĂ©nĂ©rer du code Ă  la fois Ă  l'aide de l' API Roslyn Compiler et en concatĂ©nant des chaĂźnes ordinaires.



Dans cet article, nous allons parcourir le processus d'implémentation ISourceGeneratorpour générer des références typées aux contrÎles AvaloniaUI déclarés en XAML. Au cours du développement, nous apprendrons au générateur à compiler XAML à l'aide de l'API du compilateur XamlX utilisée dans AvaloniaUI et du systÚme de type XamlX implémenté au-dessus de l' API du modÚle sémantique de Roslyn .



Formulation du problĂšme



, , . , , 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 :



  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