Русский
Русский
English
Статистика
Реклама

Avalonia

Из песочницы Эволюция Material Design для AvaloniaUI

18.11.2020 00:09:51 | Автор: admin

image


Material.Avalonia быстрый способ стилизовать под Material Desing приложение, написанное на AvaloniaUI кросс-платформенном XAML фреймворке для .NET.


Примерно с год назад на Харбе уже была статья о нашей библиотеке, однако с этого момента изменилось многое: мы избавились от встроенных тем, переделали стандартные и добавили свои элементы управления.


Сейчас наше демо приложение выглядит вот так:


image


Еще примеры приложений, использующих Material.Avalonia


Начало использования


Сначала установим необходимый Nuget пакет:


dotnet add package Material.Avalonia

После этого изменим файл App.xaml, если нам нужно стилизовать все приложение. Либо, если нужно изменить оформление только одного окна или другого элемента управления, то вместо Application.Styles (Application.Resources) у нас будут Window.Styles или UserControl.Styles соответственно.


<Application ...             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"             ...>    <Application.Resources>        <themes:BundledTheme BaseTheme="Light" PrimaryColor="Teal" SecondaryColor="Amber"/>    </Application.Resources>    <Application.Styles>        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />    </Application.Styles></Application>

Все, после этого все наше приложение будет использовать стили Material Design.
Однако, не все элементы управления уже стилизованы. Если некоторые из них не работают, то измените Application.Styles следующим образом:


<Application.Styles>    <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>    <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>    <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" /></Application.Styles>

Данное изменение добавит стандартные темы контролов Avalonia "под" темы Material.Avalonia.


Темы


За последние 2 месяца мы полностью переписали темы, и, избавились наконец от предварительно заготовленных наборов тем.


Теперь для настройки темы по умолчанию нужно модифицировать свойства BundledTheme в App.xaml.


Базовая тема может быть светлой Light, темной Dark и наследуемой Inherit. Последний вариант будет пытаться получить тему, используемую системой, но в данный момент это еще не реализовано.


BundledTheme поддерживает задание всех, доступных в Material Design, "стандартных" цветов.


Смена цвета, например на Teal, из кода происходит подобным образом:


var paletteHelper = new PaletteHelper();var theme = paletteHelper.GetTheme();theme.SetPrimaryColor(SwatchHelper.Lookup[(MaterialColor) PrimaryColor.Teal]);paletteHelper.SetTheme(theme);

Однако, через код можно задать вообще любой пользовательский цвет из RGBA.
Про работу с цветами темы я напишу отдельную статью.


Кастомные контролы


Card

Спецификация на сайте Material Design


image


Для Card можно менять размер тени используя Attached Property:


<styles:Card assists:ShadowAssist.ShadowDepth="Depth1">...</styles:Card>

ColorZone

Цветовая зона позволяет легко переключать цвета фона и переднего плана из выбранной палитры Material Design или пользовательских цветов.


image


<styles:ColorZone Margin="4" Padding="8" Mode="Accent">Accent</styles:ColorZone>

FloatingButton

image


<styles:FloatingButton Content="+"  /><styles:FloatingButton Content="My long text" />

Тени


В Avalonia поддержка теней "из коробки" ограничена заданием BoxShadows для Border.
Однако уже реализован AttachedProperty ShadowAssist, который может быть использован для Card, FloatingButton и Border.


Когда для Border задается ShadowAssist.ShadowDepth то он самостоятельно корректирует BoxShadows для соответствия выбранному уровню ShadowDepth.


Так-же есть ShadowAssist.Darken, позволяющий затемнять уже существующую тень и делающий это с анимацией. Таким образом сделано изменение тени, при наведение на кнопки.


Демонстрация Card с разными ShadowDepth


image


Многое уже сделано, еще больше запланировано.
Ознакомиться или помочь с разработкой можно на GitHub
Скачать пакет на NuGet


Issue/PR и просто отзывы категорически приветствуются.
Поддержку от разработчиков Avalonia и всех сочувствующих можно получить в Telegram (ru) и Gitter (en), а документация по стилизации элементов управления доступна тут.

Подробнее..

Авалония для самых маленьких

26.11.2020 12:17:06 | Автор: admin
В свежем превью Rider, помимо прочего, появилась поддержка Авалонии. Авалония это самый крупный .NET фреймворк для разработки кроссплатформенного UI, и его поддержка в IDE отличный повод наконец разобраться, как писать десктопные приложения для любых платформ.

В этой статье я на примере простой задачи по реализации калькулятора покажу:

  • как управлять разметкой,
  • как связывать функциональность с компонентами,
  • как управлять стилями.



Подготовка


Для работы я использовал:


Единственным обязательным инструментов в этом списке является сам дотнет. Остальное можете выбирать сами: любимую операционную систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся шаблонами .NET приложений для Авалонии. Для этого нам потребуется клонировать репозиторий с шаблонами, а затем установить скачанные шаблоны:

git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.gitdotnet new --install /path/avalonia-dotnet-templates/

Типы проектов Авалонии
Типы проектов

Теперь, когда шаблоны установлены, мы можем создать новый проект на основе MVVM шаблона Авалонии:

dotnet new avalonia.mvvm -o ACalc

Перейдем в директорию проекта и обновим все версии пакетов на самые новые (на момент написания статьи):

dotnet add package Avalonia --version 0.10.0-preview6dotnet add package Avalonia.Desktop --version 0.10.0-preview6dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6

Давайте внимательнее посмотрим на структуру проекта, сгенерированную шаблоном:

image

  • В папке Assets хранятся ресурсы, используемые нами в данном проекте. На текущий момент там лежит лого Авалонии, использующееся в качестве иконки приложения.
  • В папку Model мы будем складывать все общие модели, используемые в нашем приложении. На текущий момент она пуста.
  • Папка ViewModels предназначена для хранения логики, которая будет использоваться в каждом из окон. Прямо сейчас в этой папке хранится ViewModel главного окна и базовый класс для всех ViewModel.
  • В папке Views хранится разметка окон (а также code behind файл, в который хоть и можно положить логику, но лучше для этих целей использовать ViewModel). На текущий момент у нас есть только главное окно.
  • App.xaml общий конфиг приложения. Несмотря на то, что он и выглядит как еще одно окно, на самом деле, этот файл служит для задания общих настроек приложения.
  • ViewLocator нам в этот раз не пригодится, так как он используется для создания кастомных контролов. Подробнее о нем можно почитать в документации Авалонии.

Запустим наше приложение командой dotnet run.



Теперь все готово для разработки.

Разметка


Начнем с создания базовой разметки. Перейдем в файл Views/MainWindow.xaml там будет храниться разметка главного окна нашего калькулятора.



В данный момент наша разметка состоит из базовых параметров окна (размеров, иконки и заголовка) и одного блока с текстом. Давайте заменим этот блок с текстом на Grid, который будет служить скелетом нашей разметки. Этот контрол разложит все элементы по порядку, один за другим.

Итак, заменим TextBlock на пустой Grid:

<Grid></Grid>

А теперь подготовим основу нашей разметки. Для начала укажем, сколько строк нужно нашему приложению и какой они должны быть высоты:

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions></Grid>

Теперь заполним разметку основными компонентами добавим строку меню, базовый экран и вложенный Grid для блока клавиш:

<Grid>    <Grid.RowDefinitions>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="auto"></RowDefinition>        <RowDefinition Height="*"></RowDefinition>    </Grid.RowDefinitions>    <!--строка меню-->    <Menu>    </Menu>    <!--Импровизированный экран нашего калькулятора-->    <TextBlock>    </TextBlock>    <!--Grid для клавиш-->    <Grid></Grid></Grid>

Отдельно остановимся на расположении клавиш в сетке.
Для начала нужно описать количество строк и столбцов в нашем Grid. А после разложить кнопки по соответствующим им строкам и столбцам, указав их координаты.

<Grid>    <Grid.RowDefinitions>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>        <RowDefinition></RowDefinition>    </Grid.RowDefinitions>    <Grid.ColumnDefinitions>        <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>         <ColumnDefinition></ColumnDefinition>    </Grid.ColumnDefinitions>    <Button Grid.Row="0" Grid.Column="0">1</Button></Grid>

Стоит отметить, что элементы внутри Grid могут занимать несколько ячеек. Для этого используются параметры ColumnSpan и RowSpan:

 <Button Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="2">=</Button>

Остальные кнопки добавляются аналогично, поэтому готовую разметку можно посмотреть сразу в репозитории проекта.

Последнее, что нам осталось сделать это задать параметры окна. Установим стартовые и минимальные размеры окна (они задаются в корневом элементе Window).

MinHeight="300"MinWidth="250"Height="300"Width="250"

После добавления всех элементов разметки наше окно калькулятора будет выглядеть так:



Основной функционал


С разметкой закончили, пора реализовать логику!

Начнем с добавления в папку Models нового Enum, который описывает возможные операции:

public enum Operation{    Add,    Subtract,    Multiply,    Divide,    Result}

Теперь перейдем в класс ViewModel/MainWindowViewModel. Здесь будет храниться основная функциональность нашего приложения.

Добавим в файл несколько приватных полей, с которыми мы будем работать:

private double _firstValue;private double _secondValue;private Operation _operation = Operation.Add;

Теперь реализуем основные методы:

  • AddNumber добавляет новую цифру к числу.
  • ExecuteOperation выполняет одну из операций, описанных в енаме Operation.
  • RemoveLastNumber удаляет последнюю введенную цифру.
  • ClearScreen очищает экран калькулятора.

Не будем останавливаться на реализации этих методов, в этом нет никакой специфики для Авалонии (реализацию вы можете посмотреть в репозитории проекта). Единственное, что нас интересует это то, что помимо перечисленных выше приватных полей, эти методы также оперируют публичным свойством ShownValue. О нем чуть позже.

Связывание


Теперь, когда у нас готовы и разметка, и логика, пора связать их друг с другом.
В Авалонию по умолчанию включен Reactive UI это фреймворк, предназначенный как раз для связывания View и Model при использовании MVVM. Подробнее о нем вы сможете прочитать на официальном сайте и в документации Авалонии. Конкретно сейчас нас интересует возможность фреймворка обновлять View при изменении данных.

Для хранения актуального значения, выводимого на экране, реализуем свойство ShownValue:

public double ShownValue{    get => _secondValue;    set => this.RaiseAndSetIfChanged(ref _secondValue, value);}

Получаемое из этого свойства значение будет выводиться на дисплее нашего калькулятора, а метод RaiseAndSetIfChanged позаботится о вызове уведомления при изменении значения свойства.

Привяжем это свойство к созданному на этапе разметки текстовому полю:

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" />

Благодаря директиве Binding и методу RaiseAndSetIfChanged значение свойства Text в этом поле будет обновляться при каждом изменении значения свойства ShownValue.

Теперь добавим в MainWindowViewModel еще три публичных свойства для команд. Команды являются обертками вокруг функций, которые будут вызываться определенными действиями на UI.

public ReactiveCommand<int, Unit> AddNumberCommand { get; }public ReactiveCommand<Unit, Unit> RemoveLastNumberCommand { get; }public ReactiveCommand<Operation, Unit> ExecuteOperationCommand { get; }

Команды нужно инициализировать в конструкторе класса, связав их с соответствующими методами:

public MainWindowViewModel(){    AddNumberCommand = ReactiveCommand.Create<int>(AddNumber);    ExecuteOperationCommand = ReactiveCommand.Create<Operation>(ExecuteOperation);    RemoveLastNumberCommand = ReactiveCommand.Create(RemoveLastNumber);}

Теперь обновим разметку кнопок. Например, для клавиши Backspace новая разметка будет выглядеть так:

<Button Grid.Row="3" Grid.Column="2" Command="{Binding RemoveLastNumberCommand}"></Button>

Несколько сложнее дела обстоят с номерными кнопками и кнопками операций. Для них мы должны передать в качестве параметра вводимую цифру или операцию. Для этого в корневом теге Window нам нужно добавить пространство имен System:

xmlns:s="clr-namespace:System;assembly=mscorlib"

А затем обновить разметку кнопок, добавив в них связанный метод и параметр:

<Button Grid.Row="0" Grid.Column="0" Command="{Binding AddNumberCommand}">    <Button.CommandParameter>        <s:Int32>1</s:Int32>    </Button.CommandParameter>     1</Button>

После того, как мы аналогичным образом обновим все остальные кнопки, функциональность калькулятора будет полностью готова к работе.



Стили


Итак, логика нашего калькулятора полностью реализована, но его визуальная сторона оставляет желать лучшего. Самое время поиграться со стилями!

В Авалонии есть три способа управлять стилями:

  • настроить стили внутри компонента,
  • настроить стили в рамках окна,
  • подключить пакет стилей.

Пройдемся по каждому из них.

Начнем с настройки стилей внутри конкретного компонента. Очевидный претендент на точечные изменения это экран нашего калькулятора. Давайте увеличим для него размер шрифта и перенесем текст вправо.

<TextBlock Grid.Row="1" Text="{Binding ShownValue}" TextAlignment="Right" FontSize="30" />

Теперь поиграемся со стилями в рамках окна. Здесь мы можем изменить вид всех компонентов определенного типа. Например, можно немного раздвинуть кнопки.

<Window.Styles>    <Style Selector="Button">        <Setter Property="Margin" Value="5"></Setter>     </Style></Window.Styles>

Как видите, конкретные компоненты, к которым применяется стиль, можно выбирать при помощи селектора. Больше о селекторах вы можете прочитать в документации Авалонии.

После применения изменений выше наше окно будет выглядеть так



Чтобы упростить себе жизнь, можете воспользоваться готовым пакетом стилей. Давайте, к примеру, подключим для нашего калькулятора стиль Material. Для этого добавим соответствующий nuget пакет:

dotnet add package Material.Avalonia --version 0.10.3

А теперь обновим файл App.xaml и укажем в нем используемый пакет стилей и его параметры.

<Application ...             xmlns:themes="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"             ...>    <Application.Resources>        <themes:BundledTheme BaseTheme="Dark" PrimaryColor="Purple" SecondaryColor="Amber"/>    </Application.Resources>    <Application.Styles>        <StyleInclude Source="avares://Material.Avalonia/Material.Avalonia.Templates.xaml" />    </Application.Styles></Application>

Установленный пакет обновит визуальный стиль нашего приложения, и теперь оно будет выглядеть так:



Такие же пакеты стилей можно создавать самостоятельно их можно использовать внутри вашего проекта или распространять в виде пакета на nuget. Больше информации о стилях и способах управления ими можно найти в документации.

Заключение


В этой статье мы разобрали самый простой пример использования Авалонии, но функционал этого фреймворка куда шире, и он растет с каждым днем. Помимо неоднократно упомянутой мной документации, вы также можете спросить совета в русскоязычном чате, посвященном Авалонии, или прямо здесь в комментариях.
А еще много интересного про Авалонию и .NET UI можно будет послушать на онлайн-митапе от Контура, который пройдет сегодня, в пять по Москве.

Все исходники проекта вы можете найти в репозитории на Github.

На этом все! Оставайтесь на связи, мы вернемся со статьями о более продвинутых возможностях Авалонии.
Подробнее..

Генерация типизированных ссылок на элементы управления Avalonia с атрибутом xName с помощью C SourceGenerator

29.11.2020 20:05:10 | Автор: admin


В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# с помощью реализации интерфейса ISourceGenerator. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn. Генерировать код можно как с помощью Roslyn Compiler API, так и методом конкатенации обычных строк.


Постановка задачи


С помощью новых генераторов исходного кода может получиться элегантно решить широкий спектр задач, включая генерацию шаблонного кода, который было бы не очень интересно и совсем не продуктивно писать вручную. Например, в приложениях, использующих AvaloniaUI фреймворк для разработки кроссплатформенных приложений с графическим интерфейсом, о котором недавно вышла статья на Хабре нередко приходится писать следующий код для получения ссылки на элемент управления, объявленный в 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, 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.    private TextBox UserNameTextBox => this.FindControl<TextBox>("UserNameTextBox");    private TextBox PasswordTextBox => this.FindControl<TextBox>("PasswordTextBox");    private TextBlock CompoundValidation => this.FindControl<TextBlock>("CompoundValidation");}

Код геттеров свойств, позволяющих получить типизированный доступ к элементам управления из XAML-файла, соответствующего SignUpView, выглядит шаблонным. Было бы неплохо научиться генерировать этот код, чтобы, с одной стороны, избежать многословности, а с другой стороны чтобы избежать возможных ошибок и опечаток в имени элемента управления, объявленного в XAML, или в имени его типа.


Если мы будем всё время писать код, как в примере выше, вручную, нам придётся самостоятельно следить за изменениями имён и типов в XAML-файле, и в случае ошибки мы узнаем о том, что что-то пошло не так, только после запуска приложения. Если бы мы генерировали ссылки некоторым способом, об ошибках и опечатках мы бы узнавали уже на этапе компиляции, и тратили бы меньше времени на отладку нашего кроссплатформенного приложения (как, впрочем, и на написание таких геттеров).


Пример входных и выходных данных


Мы ожидаем, что на вход наш генератор исходного кода будет получать два файла. Для компонента представления с именем SignUpView, данными файлами будут являться XAML-разметка SignUpView.xaml, и code-behind файл SignUpView.xaml.cs, содержащий логику пользовательского интерфейса. Например, для файла разметки пользовательского интерфейса SignUpView.xaml:


<Window xmlns="http://personeltest.ru/aways/github.com/avaloniaui"        xmlns:x="http://personeltest.ru/away/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). Давайте, для начала, добавим в сборку проекта, ссылающегося на генератор, некоторый атрибут, с помощью которого пользователи нашего генератора будут помечать классы, для которых необходимо генерировать типизированные ссылки на элементы управления 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' проекта        // разработчика, который решит воспользоваться нашим ISourceGenerator.        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)    {        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 и один особенный NameDirectiveTransformer, тоже написанный @kekekeks, который умеет преобразовывать 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;    }}

Фабрика 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(    new RoslynTypeSystem(compilation), // 'compilation' имеет тип 'CSharpCompilation'    "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 namedProperty &&                    namedProperty.Name == "Name" &&                    propertyValueNode.Values.Count > 0 &&                    propertyValueNode.Values[0] is XamlAstTextNode text)                {                    var typeNamePair = ($@"{clrType.Namespace}.{clrType.Name}", 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(    new RoslynTypeSystem(compilation), // 'compilation' имеет тип 'CSharpCompilation'    "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, // Элемент из списка 'symbols', сформированного в самом начале.    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);    }}

Ссылки


Подробнее..

Генерация типизированных ссылок на элементы управления AvaloniaUI с атрибутом xName с помощью C Source Generators API

29.11.2020 22:13:53 | Автор: admin


В апреле 2020-го года разработчиками платформы .NET 5 был анонсирован новый способ генерации исходного кода на языке программирования C# с помощью реализации интерфейса ISourceGenerator. Данный способ позволяет разработчикам анализировать пользовательский код и создавать новые исходные файлы на этапе компиляции. При этом, API новых генераторов исходного кода схож с API анализаторов Roslyn. Генерировать код можно как с помощью Roslyn Compiler API, так и методом конкатенации обычных строк.


Постановка задачи


С помощью новых генераторов исходного кода может получиться элегантно решить широкий спектр задач, включая генерацию шаблонного кода, который было бы не очень интересно и совсем не продуктивно писать вручную. Например, в приложениях, использующих AvaloniaUI фреймворк для разработки кроссплатформенных приложений с графическим интерфейсом, о котором недавно вышла статья на Хабре нередко приходится писать следующий код для получения ссылки на элемент управления, объявленный в 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, 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="http://personeltest.ru/aways/github.com/avaloniaui"        xmlns:x="http://personeltest.ru/away/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";    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)    {        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 и один особенный NameDirectiveTransformer, тоже написанный @kekekeks, который умеет преобразовывать 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;    }}

Фабрика 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);    }}

Ссылки


Подробнее..

От WPF к Авалонии

16.02.2021 12:20:24 | Автор: admin

WPF любимый сообществом фреймворк для десктопной разработки, однако в то время как дотнет и вся его экосистема уже давно кроссплатформенные, WPF работает только под Windows. Сообщество решило эту проблему и теперь у нас есть Авалония фреймворк, во многом очень похожий на WPF, но работающий на разных платформах.

Под катом мы разберем отличия Авалонии от WPF. Что нужно знать людям, переходящим с WPF на Авалонию? В чем преимущества нового фреймворка, а в чем его недостатки по сравнению с WPF?

Стили

На первый взгляд, стили в Авалонии выглядят точно также, как и в WPF они задаются в блоке Styles с помощью селекторов и сеттеров. Первые выбирают набор блоков, к которым применяются стили, вторые задают непосредственно стили. Давайте сравним два одинаковых стиля в WPF и Авалонии:

<Style TargetType="TextBlock">  <Setter Property="HorizontalAlignment" Value="Center" />  <Setter Property="FontSize" Value="24"/></Style>
<Style Selector="TextBlock"><Setter Property="HorizontalAlignment" Value="Center" /><Setter Property="FontSize" Value="24"/></Style>

Как видите, в данном фрагменте различается только объявление тега Style в WPF для выбора целевого блока используется параметр TargetType, а в Авалонии - Selector. Однако селекторы в Авалонии куда мощнее, чем TargetType в WPF. Больше всего они напоминают селекторы из CSS с классами, псевдоклассами и кастомными обращениями.

Например, вот так мы можем задать размер шрифта для всех текстовых блоков с классом h1

<Styles>  <Style Selector="TextBlock.h1">    <Setter Property="FontSize" Value="24"/>  </Style></Styles><TextBlock Classes="h1">Header</TextBlock>

Псевдоклассы позволяют нам выбирать элементы в определенном состоянии когда они в фокусе, когда на них наведена мышь, когда чекбокс выбран и так далее. В WPF подобное реализовывалось через триггеры (стиль изменялся, когда активировалось некоторое событие), однако подход через псевдоклассы выглядит более удобным и современным.

<Styles>  <Style Selector="Button:pointerover">    <Setter Property="Button.Foreground" Value="Red"/>  </Style></Styles><Button>I will have red text when hovered.</Button>

И, конечно же, селекторы в Авалонии позволяют гибко выбирать целевые контролы для стилей через цепочки дочерних элементов, по совпадению нескольких классов, по шаблонам и по значениям определенных свойств. Опять же, это очень похоже на селекторы в CSS. Например, вот так мы можем выбрать кнопку, являющуюся прямым наследником элемента с классом block, имеющую значение свойства IsDefault = true:

.block > Button[IsDefault=true]

Полный список доступных селекторов и их описания вы можете найти в документации Авалонии.

Обновленный синтаксис XAML

Как и в случае со стилями, синтаксис XAML не слишком сильно отличается от WPF. Объявление контролов, параметры, биндинги все выглядит по-прежнему. Однако некоторые отличия все же есть синтаксис стал более емким и понятным, а где-то добавились новые возможности. Посмотрим на изменения по порядку.

Начнем с упрощений в синтаксисе. Самый простой пример такого упрощения это объявление строк и столбцов в Grid. Классическое, привычное с WPF объявление будет выглядеть следующим образом:

<Grid>  <Grid.RowDefinitions>    <RowDefinition Height="*"></RowDefinition>    <RowDefinition Height="Auto"></RowDefinition>    <RowDefinition Height="32"></RowDefinition>  </Grid.RowDefinitions></Grid>

Этот код будет отлично работать и в Авалонии, однако, помимо полного варианта объявления, добавился и сокращенный.

<Grid RowDefinitions="*,Auto,32,"/>

Упростилось и подключение зависимостей в XAML файлах. Теперь clr-namespace можно заменить на using. Такое изменение позволяет сделать подключение сторонних библиотек короче и читаемее.

Было: xmlns:styles="clr-namespace:Material.Styles;assembly=Material.Styles"

Стало: xmlns:styles="using=Material.Styles"

Другое любопытное изменение это вынесение DataTemplates и Styles в отдельные теги. Раньше они размещались внутри Resources.

<UserControl xmlns:viewmodels="clr-namespace:MyApp.ViewModels;assembly=MyApp">  <UserControl.DataTemplates>    <DataTemplate DataType="viewmodels:FooViewModel">      <Border Background="Red" CornerRadius="8">        <TextBox Text="{Binding Name}"/>      </Border>    </DataTemplate>  </UserControl.DataTemplates>  <UserControl.Styles>    <Style Selector="ContentControl.Red">      <Setter Property="Background" Value="Red"/>    </Style>  </UserControl.Styles><UserControl>

Важные изменения произошли и в биндингах. Авалония позволяет связывать между собой элементы разметки, прибегая только к свойствам XAML. Достаточно обратиться к источнику зависимости, используя # и имя элемента. Например, вот такой код привяжет значение поля other к значению поля source.

<TextBox Name="source"/><!-- Binds to the Text property of the "source" control --><TextBlock Name=other Text="{Binding #source.Text}"/>

Конструкция $parent позволяет обращаться к родительским компонентам.

<Border Tag="Hello World!">  <TextBlock Text="{Binding $parent.Tag}"/></Border>

Кстати, такое обращение поддерживает индексирование. Иначе говоря, конструкция $parent[1] позволит вам обратиться к родителю родителя вашего компонента. А конструкция $parent[0] эквивалентна $parent.

Помимо индексов здесь также можно использовать обращение по типу. $parent[Border] позволит вам обратиться к первому предку с типом Border. А еще такое обращение можно совместить с индексированием.

<Border Tag="Hello World!">  <Border>    <Decorator>      <TextBlock Text="{Binding $parent[Border;1].Tag}"/>    </Decorator>  </Border></Border>

В WPF аналогичную функцию выполняет свойство RelativeSource, однако RelativeSource завязано на визуальном дереве элементов, а не на логическом. Подробнее почитать о разнице вы можете в этой статье.

Небольшие изменения также коснулись конвертеров. Один из самых популярных конвертеров, логическое отрицание, был реализован в виде синтаксиса в XAML коде.

<StackPanel>  <TextBox Name="input" IsEnabled="{Binding AllowInput}"/>  <TextBlock IsVisible="{Binding !AllowInput}">Sorry, no can do!</TextBlock></StackPanel>

Кстати, этот конвертер использует метод Convert.ToBoolean для преобразования значений, что позволяет писать код такого вида:

<Panel>  <ListBox Items="{Binding Items}"/>  <TextBlock IsVisible="{Binding !Items.Count}">No results found</TextBlock></Panel>

А еще конвертер поддерживает множественное использование, что позволяет преобразовывать не-булевские значения в boolean с помощью двойного отрицания.

<Panel>  <ListBox Items="{Binding Items}" IsVisible="{Binding !!Items.Count}"/></Panel>

Я бы не рекомендовал использовать такой трюк, однако множественное применение конвертеров может пригодиться, когда в Авалонии реализуют синтаксическую поддержку для других конвертеров. Теоретически вы можете сделать это сами, но на практике это достаточно сложно и никак не документировано.

Помимо синтаксической поддержки, Авалония дает пользователю несколько дефолтных конвертеров. Для них не реализован никакой специфический XAML синтаксис, так что они используются как обычные конвертеры.

<TextBlock Text="{Binding MyText}" IsVisible="{Binding MyText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>

И последняя возможность, которую хочется упомянуть, относится к проблемам кроссплатформенности. Видели вот это меню в macOS?

Подобные элементы это вечная головная боль кроссплатформенных приложений. На маках такое меню есть, а на винде нет. А в Linux есть, но не везде, только в некоторых оконных менеджерах.

Для работы с такими меню в девятой версии Авалонии реализовали компонент NativeMenu. По умолчанию, даже без добавления NativeMenu в приложение, Авалония будет генерировать меню с базовыми пунктами как раз они и представлены на скриншоте. А для добавления новых пунктов как раз и используется NativeMenu.

<Application>  <NativeMenu.Menu>    <NativeMenu>      <NativeMenuItem Header="About MyApp" Command="{Binding AboutCommand}" />    </NativeMenu>  </NativeMenu.Menu></Application>

Декларативный UI via F#

Несмотря на все улучшения и упрощения для XAML, многим он кажется громоздким и избыточным, поэтому все большую популярность набирают разнообразные декларативные DSL.

Сообщество Авалонии разработало отличную библиотеку Avalonia.FuncUI, позволяющую вам писать UI на Авалонии в декларативном стиле. Получающийся код напоминает реализацию UI с помощью Elm или Jetpack Compose.

module Counter =    type CounterState = {    count : int  }  let init = {    count = 0  }  type Msg =  | Increment  | Decrement      let update (msg: Msg) (state: CounterState) : CounterState =   match msg with    | Increment -> { state with count =  state.count + 1 }    | Decrement -> { state with count =  state.count - 1 }  let view (state: CounterState) (dispatch): IView =    DockPanel.create [      DockPanel.children [        Button.create [          Button.onClick (fun _ -> dispatch Increment)          Button.content "click to increment"        ]        Button.create [          Button.onClick (fun _ -> dispatch Decrement)          Button.content "click to decrement"         ]        TextBlock.create [          TextBlock.dock Dock.Top          TextBlock.text (sprintf "the count is %i" state.count)        ]      ]    ]

Такая стилистика пока не стала чем-то общепринятым кому-то привычнее XAML, а кто-то считает FuncUI громоздким по сравнению с конкурентами. В любом случае, это интересная опция, которая для многих может быть более удобной и понятной.

Недостатки

Перейдем к самому интересному а что же не так с Авалонией в сравнении с WPF?

Если смотреть на функциональность, то Авалония не только приобрела новые фичи, но и потеряла некоторые возможности WPF. Например, в Авалонии из коробки отсутствуют триггеры (обсуждение этой фичи можно найти в репозитории Авалонии, а пока триггеры можно использовать с помощью пакета AvaloniaBehaviors), не работают биндинги через стили и так далее.

Другая проблема это экосистема. Так как Авалония куда моложе WPF и имеет значительно меньшую аудиторию, вокруг Авалонии пока не выстроилась богатая экосистема со множеством стилей и кастомных контролов. Да и гигантских платных UI фреймворков вроде DevExpress пока не появилось. Аналогичные сложности и с инструментами разработки они есть, и поддержка становится все лучше, однако пока что отстает по качеству от WPF.

Ну и самая заметная проблема, о которой говорят многие это отсутствие гарантий. Авалония это инструмент, создаваемый исключительно сообществом, что не слишком-то привычно для .NET разработчиков. За ним не стоит Microsoft или какая-то другая крупная компания. В этих условиях многим не хочется рисковать, надеясь на open source продукт кто знает, вдруг завтра мейнтейнер потеряет интерес, и разработка встанет? Однако это же дает вам возможность заметно влиять на развитие Авалонии, исправляя существующие проблемы и предлагая новые фичи.

Заключение

Подводя итог можно сказать, что Авалония это осовремененная версия WPF. Синтаксис чуть полегче, стили современнее, да и в целом фреймворк подталкивает к использованию современных подходов вроде реактивного программирования и декларативного DSL.

Компенсация за это типичный набор проблем молодого опенсорсного инструмента. Кое-какие фичи не реализованы, экосистема бедновата, да и качественная поддержка в будущем не гарантирована. Так или иначе, это несет и преимущества в виде возможности заметно влиять на будущее Авалонии.

Если вам нравится этот фреймворк не стесняйтесь активно участвовать в его разработке. Пишите предложения в репозитории, участвуйте в дискуссиях, правьте найденные баги и общайтесь с сообществом в телеграме Авалонии.

И, конечно же, подписывайтесь на наш блог на Хабре мы уже готовим следующую статью про Авалонию. А на сегодня все, оставайтесь на связи.

Подробнее..

По пути в Авалонию

15.02.2021 12:06:03 | Автор: admin

Здравствуй дорогой наш читатель. Это третья статья из серии переезда нашего ПО на кроссплатформенные рельсы. Она затронет визуальную часть продукта, а именно диалоговое окно.

В рамках первой статьи упоминалось, что данное приложение вызывается через пункт ПКМ, в связи с чем были проблемы при развертывании в разных ОС. Самое интересное заключалось не только в том, как реализовать вызов визуальщины, но и в том, как без сильных потерь реализовать диалоговое окно, которое мы сможем спокойно использовать в Linux, а возможно в дальнейшем и в Windows. Для самых нетерпеливых забежим вперёд и скажем, что в данной задаче нам помог framework Avalonia. Ну, а теперь, по порядку.

Мы имеем WinForms приложение, работающее, разумеется, только в Windows. Его задача - выводить диалоговое окно с небольшим количеством контролов, фактически, главными из которых являются листбокс и кнопка.

Как раз только выходит .Net Core 3, с предположением о работе в Linux. Собираем, проверяем и ничего не получаем. В действительности в Linux не добавили визуальную часть. Ну что же, ищем другие варианты.

Самым часто встречающимся фреймворком по данному вопросу является Avalonia. Мы решили опробовать её. Взяли простейший пример, собрали в Linux, запустили и - да, диалоговое оно отобразилось. Теперь дело за малым - перенести WinForms проект на Avalonia.

В качестве проекта был выбран простой шаблон Avalonia Application. При условии, что логика и DTO уже вынесены в отдельные библиотеки, шаблон MVVM здесь был бы избыточен.

Перенос дизайна основного окна с WinForms на XAML прошёл без проблем, несмотря на то, что разработчик в знакомстве с WPF ранее замечен не был.

Логика осталась практически без изменений (BackgroundWorker, файлы, DTO...).

Реализация окон сообщений, на первый взгляд, также проблем не вызвала.

В проекте WinForms в качестве диалогового окна сообщения использовался System.Windows.Forms.MessageBox.

MessageBox.Show("Метка установлена", "Crosstech DSS",                 MessageBoxButtons.OK, MessageBoxIcon.Information);

Для нового проекта можно было использовать окно, производное от Avalonia.Controls.Window, предварительно оформленное как окно сообщения с иконками, текстом, стилями и кнопками, но...

в процессе изучения форумов выяснилось, что за нас уже подумали:). Существует проект MessageBox.Avalonia с иконками и вот этим вот всем. Проект немедленно был установлен.

Сначала это выглядело так:

private void Form_Load(object sender, EventArgs e){  ...    _backgroundWorker.DoWork += DoLoadForm;    _backgroundWorker.RunWorkerAsync();...}private void DoLoadForm(object sender, DoWorkEventArgs e){...MessageBoxHelper.ShowErrorBox("Нет разрешённых действий для данного документа. Пожалуйста, обратитесь к администратору.");...}internal static class MessageBoxHelper{  public static async void ShowBox(MessageBoxStandardParams MBSparams)    {    Action showMSBDialog = () => MessageBox.Avalonia.MessageBoxManager.GetMessageBoxStandardWindow(MBSparams).ShowDialog(new MainWindow());      await Dispatcher.UIThread.InvokeAsync(showMSBDialog);    }public static void ShowErrorBox(string message)    {    var MBSparams = new MessageBoxStandardParams      {      ButtonDefinitions = ButtonEnum.Ok,        ContentTitle = $"Ошибка доступа",        ShowInCenter = true,        ContentMessage = message,        Icon = MessageBox.Avalonia.Enums.Icon.Error,        Style = Style.Windows      };      ShowBox(MBSparams);    }...}

Окно сообщения всё-таки выводилось на экран. Для начала неплохо. Проблема была в том, что это окно сообщения существовало независимо от главного окна приложения, они все были доступны и закрывать их можно было в любом порядке, результат диалога не анализировался. Такое положение вещей нас более не устраивало, внесли небольшие изменения, на данном этапе получилось так:

private async void DoLoadForm(object sender, DoWorkEventArgs e){var rezult = await MessageBoxHelper.ShowErrorBox("Нет разрешённых действий для данного документа. Завершить работу с документом?", this);  if (rezult == MessageBox.Avalonia.Enums.ButtonResult.Yes)  {  Environment.Exit(0);  }...}internal static class MessageBoxHelper{public static async Task<ButtonResult> ShowBox(MessageBoxStandardParams MBSparams, Window owner = null)  {if (owner == null){owner = new MainWindow();}Func<Task<ButtonResult>> www = new Func<Task<ButtonResult>>(() => MessageBox.Avalonia.MessageBoxManager.GetMessageBoxStandardWindow(MBSparams).ShowDialog(owner));ButtonResult result = await Dispatcher.UIThread.InvokeAsync(www);return result;}public static async Task<ButtonResult> ShowErrorBox(string message, Window owner = null)  {  var MBSparams = new MessageBoxStandardParams    {    ButtonDefinitions = ButtonEnum.YesNo,      ContentTitle = $"Ошибка доступа",      ShowInCenter = true,      ContentMessage = message,      Icon = MessageBox.Avalonia.Enums.Icon.Error,      Style = Style.Windows     };     return await ShowBox(MBSparams, owner);   } ...}

Диалоговое окно выводится на переднем плане, не даёт переключить фокус на основное окно, возвращает результат. Уже лучше.

В процессе тестирования столкнулись с багом: на Windows 8 x86 при включенном масштабировании, наше окно отображается с чёрной рамкой. Меняли большинство параметров отображения в XAML, не помогло.

Хотим отдельно отметить прекрасную поддержку проекта Avalonia.

Ссылки на использованные источники:

Статьи на Хабре:

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru