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

Подготовка
Для работы я использовал:
Единственным обязательным инструментов в этом списке является сам дотнет. Остальное можете выбирать сами: любимую операционную систему и IDE (например, тот же Rider).
Для инициализации проекта мы воспользуемся шаблонами .NET приложений для Авалонии. Для этого нам потребуется клонировать репозиторий с шаблонами, а затем установить скачанные шаблоны:
git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates.git
dotnet new --install /path/avalonia-dotnet-templates/
Типы проектов
Теперь, когда шаблоны установлены, мы можем создать новый проект на основе MVVM шаблона Авалонии:
dotnet new avalonia.mvvm -o ACalcПерейдем в директорию проекта и обновим все версии пакетов на самые новые (на момент написания статьи):
dotnet add package Avalonia --version 0.10.0-preview6
dotnet add package Avalonia.Desktop --version 0.10.0-preview6
dotnet add package Avalonia.ReactiveUI --version 0.10.0-preview6Давайте внимательнее посмотрим на структуру проекта, сгенерированную шаблоном:

- В папке 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 Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
</Grid>
Теперь заполним разметку основными компонентами — добавим строку меню, базовый экран и вложенный Grid для блока клавиш:
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <!--строка меню-->
    <Menu>
    </Menu>
    <!--Импровизированный экран нашего калькулятора-->
    <TextBlock>
    </TextBlock>
    <!--Grid для клавиш-->
    <Grid></Grid>
</Grid>
Отдельно остановимся на расположении клавиш в сетке.
Для начала нужно описать количество строк и столбцов в нашем Grid. А после — разложить кнопки по соответствующим им строкам и столбцам, указав их координаты.
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <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.
На этом все! Оставайтесь на связи, мы вернемся со статьями о более продвинутых возможностях Авалонии.
          
 

s_suhanov
Я выиграл в игре "Угадай технологию по UI"
KvanTTT
А что тут угадывать то, если ответ в заголовке?
worldbeater
А мне вот очень понравился калькулятор с тёмно-розовыми кнопками. Справедливости ради стоит заметить, что в Avalonia 0.10.0, который уже вот-вот зарелизят, сделали тему, основанную на Microsoft Fluent Design. А ещё есть набор стилей Citrus.Avalonia. С релизом 0.10 будет сложнее угадать эту технологию по UI!
s_suhanov
Мне UI тоже нравится, и очень-очень.
И особенно это цвет фиолетовый (или сиреневый, или темно-розовый) — фирменный цвет дот-нета, очень даже хорош.
То что на скрине у вас — тоже, кстати очень приятно выглядит.