Generating Typed References to Avalonia Controls with the x: Name Attribute in XAML Using C # Source Generators





In April 2020, the developers of the .NET 5 platform announced a new way to generate source code in the C # programming language - using an interface implementation ISourceGenerator. This method allows developers to parse custom code and create new source files at compile time. At the same time, the API of the new source code generators is similar to the API of Roslyn analyzers . You can generate code both using the Roslyn Compiler API , and by concatenating ordinary strings.



In this article, we 'll walk through the implementation process ISourceGeneratorfor generating typed references to AvaloniaUI controls declared in XAML. During development, we will teach the generator to compile XAML using the XamlX compiler API used in AvaloniaUI and the XamlX type system implemented on top of the Roslyn semantic model API .



Formulation of the problem



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