- PVSM.RU - https://www.pvsm.ru -
В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован [1] новый способ генерации исходного кода на языке программирования C# — с помощью реализации интерфейса ISourceGenerator
[2]. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы [3] на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn [4]. Генерировать код можно как с помощью Roslyn Compiler API [5], так и методом конкатенации обычных строк.
В данном материале рассмотрим процесс реализации ISourceGenerator
для генерации типизированных ссылок на элементы управления AvaloniaUI [6], объявленные в XAML. В процессе разработки научим генератор компилировать XAML с помощью API компилятора XamlX [7], используемого в AvaloniaUI [8], и системы типов XamlX, реализованной поверх API семантической модели Roslyn [9].
С помощью новых генераторов исходного кода может получиться элегантно решить широкий спектр задач, включая генерацию шаблонного кода, который было бы не очень интересно и совсем не продуктивно писать вручную. Например, в приложениях, использующих AvaloniaUI [6] — фреймворк для разработки кроссплатформенных приложений с графическим интерфейсом, о котором недавно вышла статья на Хабре [10] — нередко приходится писать следующий код для получения ссылки на элемент управления, объявленный в XAML:
TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");
Элемент типа TextBox
с именем PasswordTextBox
при этом объявлен в XAML следующим образом:
<TextBox x:Name="PasswordTextBox"
Watermark="Please, enter your password..."
UseFloatingWatermark="True"
PasswordChar="*" />
Получать ссылку на элемент управления в XAML может понадобиться в случае необходимости применения анимаций, программного изменения стилей и свойств элемента управления, или использования кроссплатформенных типизированных привязок данных ReactiveUI, таких, как Bind
[11], BindCommand
[11], BindValidation
[12], позволяющих связывать компоненты 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 [13] и FindNameScope [14] на GitHub.
ISourceGenerator
Простейший генератор исходного кода, который ничего не делает, выглядит следующим образом [3]:
[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";
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]
;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);
}
}
В 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 [15].
Этот этап является самым сложным, но в то же время наиболее интересным. Дело в том, что нельзя просто взять и получить информацию о пространстве имён, в котором находится элемент управления, объявленный в XAML-разметке. А нам, из-за нашего желания избежать коллизий и генерировать вменяемый код, который всегда будет компилироваться и работать, позарез нужно уметь получать полную квалификацию пространства имён, в котором находится тип.
Хорошая новость заключается в том, что фреймворк AvaloniaUI [6] использует новый компилятор XamlX [16], целиком написанный @kekekeks [17]. Этот компилятор мало того, что не имеет рантайм-зависимостей, умеет находить ошибки в XAML на этапе компиляции, работает намного быстрее загрузчиков XAML из WPF, UWP, XF и других технологий, так ещё и предоставляет нам удобный API для парсинга XAML и разрешения типов. Таким образом, мы можем позволить себе подключить XamlX [16] в проект исходниками (git submodule add ://repo ./path
), и написать свой собственный MiniCompiler
, который наш генератор исходного кода будет вызывать для компиляции XAML и получения полной информации о типах, даже если они лежат в каких-нибудь сторонних сборках. Реализация XamlX.XamlCompiler [16] в виде нашего маленького 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)
{
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 [16] и один особенный NameDirectiveTransformer
[18], тоже написанный @kekekeks [17], который умеет преобразовывать XAML-атрибут x:Name
в XAML-атрибут Name
для того, чтобы впоследствии обходить полученное AST и вытаскивать имена элементов управления было проще. Такой NameDirectiveTransformer
[18] выглядит следующим образом:
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;
}
}
Фабрика MiniCompiler.CreateDefault
принимает первым аргументом любопытный тип RoslynTypeSystem
, который вы не найдёте в исходниках XamlX [16]. Данный тип реализует интерфейс IXamlTypeSystem
, а это значит, что всё самое сложное только начинается. Чтобы наш маленький компилятор заработал внутри нашего генератора исходного кода, нам необходимо реализовать систему типов XamlX [16] поверх API семантической модели компилятора Roslyn [9]. Для реализации новой 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);
Готово! Осталось извлечь все именованные объекты из дерева — и дело в шляпе.
На предыдущем этапе мы уже рассмотрели трансформер AST XamlX [7], реализующий 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
.
Исходный код генератора доступен на GitHub [19].
Интеграция генераторов исходного кода с JetBrains Rider и ReSharper [3] доступна в последних EAP, что позволяет утверждать, что реализованная технология является кроссплатформенной, и будет работать на Windows, Linux, и macOS. В дальнейшем мы собираемся заинтегрировать получившийся генератор в Avalonia, чтобы в новых версиях фреймворка генерация типизированных ссылок стала доступна из коробки. А вот так выглядит обновлённый пример кода из самого начала статьи, с биндингами и ReactiveUI.Validation [12]:
[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);
}
}
x:Name
, описанный в данном материале;Автор: Artyom V. Gorchakov
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/359270
Ссылки в тексте:
[1] был анонсирован: https://habr.com/ru/post/503172/
[2] ISourceGenerator
: https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.isourcegenerator?view=roslyn-dotnet
[3] создавать новые исходные файлы: https://blog.jetbrains.com/dotnet/2020/11/12/source-generators-in-net-5-with-resharper/
[4] анализаторов Roslyn: https://habr.com/ru/post/455952/
[5] Roslyn Compiler API: https://blog.zwezdin.com/2013/code-generating-with-roslyn/
[6] AvaloniaUI: http://avaloniaui.net/
[7] XamlX: https://github.com/kekekeks/xamlx
[8] AvaloniaUI: https://github.com/avaloniaui/avalonia
[9] семантической модели Roslyn: https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/get-started/semantic-analysis
[10] вышла статья на Хабре: https://habr.com/ru/company/skbkontur/blog/524518/
[11] Bind
: https://www.reactiveui.net/docs/handbook/data-binding/#types-of-bindings
[12] BindValidation
: https://github.com/reactiveui/reactiveui.validation
[13] FindControl: https://github.com/AvaloniaUI/Avalonia/blob/15968cb2c0aa115545e6766ee321ffd9eaa6d8d0/src/Avalonia.Controls/ControlExtensions.cs#L54
[14] FindNameScope: https://github.com/AvaloniaUI/Avalonia/blob/15968cb2c0aa115545e6766ee321ffd9eaa6d8d0/src/Avalonia.Styling/Controls/NameScopeExtensions.cs#L108
[15] New C# Source Generator Samples: https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/
[16] XamlX: https://github.com/kekekeks/XamlX
[17] @kekekeks: https://habr.com/ru/users/kekekeks/
[18] NameDirectiveTransformer
: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs
[19] GitHub: https://github.com/AvaloniaUI/Avalonia.NameGenerator
[20] Avalonia.NameGenerator: https://github.com/avaloniaui/avalonia.namegenerator
[21] Контур: https://habr.com/ru/company/skbkontur/
[22] Источник: https://habr.com/ru/post/530454/?utm_source=habrahabr&utm_medium=rss&utm_campaign=530454
Нажмите здесь для печати.