.NET-рантайм обеспечивает переносимость, и один и тот же код нормально запускается на почти любой платформе. И если с бэкэндом и консольными утилитами всё более‑менее понятно, то приложения с графическим интерфейсом переносить традиционно считалось трудной задачей, ведь нативные элементы управления в каждой системе свои, и заменить одни на другие не так‑то просто.
Было несколько попыток это исправить, в их числе MAUI и Xamarin.Forms, но по‑настоящему популярным стал фреймворк Avalonia, авторы которого перестали полагаться на целевую систему и решили рисовать все элементы управления самостоятельно, что сильно упростило жизнь разработчикам.
С Avalonia любой интерфейс будет отрисован почти идентично независимо от системы, на которой работает, а менять темы оформления и другие подобные фишки можно буквально парой строчек кода.
Этот подход имеет свою цену: вместо использования готовых элементов управления, которые предоставляет целевая система, вместе с приложением нужно таскать библиотеку компонентов, которые оно использует. Их несколько, и Eremex — как раз такая библиотека.
Eremex Controls предоставляет набор контролов для построения современных и бизнес приложений для всех популярных платформ. Тут не только всякие кнопки и чекбоксы, но и продвинутые таблицы, редакторы свойств и другие готовые крупные узлы, которые больше не надо писать руками.

Поддерживается даже WebAssembly, то есть можно будет собрать всё как веб‑приложение и даже фронт реализовать на C#!
Avalonia работает по модели MVVM и поддерживает отрисовку по необходимости, как React. Эта архитектура позволяет развязать логику отображения от бизнес‑логики и гибко менять интерфейс, практически не трогая при этом код. Чуть позже я покажу, где это особенно полезно.
Лицензирование
Как известно, для любой проблемы (Problem) в C# существует три решения: Microsoft.Solution, публичный архив Solution.NET из 2016 года и Solution Pro за деньги. Библиотека Eremex, как можно догадаться, относится к третьему классу, зато работает как надо. К тому, как воспользоваться лицензией, мы вернемся чуть позже, а сейчас рассмотрим доступные варианты.
Стоимость официальной коммерческой лицензии, по информации разработчика, составляет от 100 тысяч рублей в год на разработчика.
Для того, чтобы оценить контролы в работе, предоставляется 60-дневная триальная лицензия. Для ее использования достаточно не указывать ключ – он будет сгенерирован автоматически, но приложение будет показывать сообщение о пробном режиме.

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

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

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

Выбор цвета, кстати, дает очень уже приятные глазу цвета, а не то, что давал системный селектор. Рекомендую!
Еще один гениальный контрол – это TreeList. Выглядит он очень похожим на DataGrid, но в отличие от него умеет строить иерархическую структуру со вложенными элементами и кастомными редакторами прямо в теле таблицы.

С помощью всего двух этих контролов уже можно построить какой‑то дашборд или демо‑приложение для своих экспериментов.
Демонстрацию кнопок и флажков пропустим, лучше покажу реализацию полноценного 3D-просмотрщика, который учитывает даже освещение!

www
Eremex предоставляет подробную техническую документацию по всем контролам на русском и английском языках. Она доступна на сайте разработчика.
Теперь, когда ты примерно представляешь, чего ждать от библиотеки, давай пройдемся по простому приложению и посмотрим, как это реализовано. Кнопки и чекбоксы нам встретятся именно там.
Как писать с Avalonia
Приложения с Avalonia UI традиционно подразделяется на три части согласно паттерну MVVM (Model, View, ViewModel). В коде это описывается в нескольких разных файлах. View – это только интерфейс в чистом виде, Model – только данные, а ViewModel – это промежуточный слой, который связывает View и Model. В отличие от Windows Forms, внешний вид приложения (View) тут не накидывается в визуальном WYSIWYG-редакторе, а описывается в специальном файле XAML, где легко поменять интерфейс. Например, вот код, который помещает селектор цвета на текущее окно:
<UserControl x:Class="DemoCenter.Views.ColorEditorPageView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mx="https://schemas.eremexcontrols.net/avalonia" xmlns:mxe="https://schemas.eremexcontrols.net/avalonia/editors" xmlns:vm="using:DemoCenter.ViewModels" xmlns:mxei="clr-namespace:Eremex.AvaloniaUI.Controls.Editors.Visuals;assembly=Eremex.Avalonia.Controls" d:DesignHeight="750" d:DesignWidth="900" x:DataType="vm:ColorEditorPageViewModel" mc:Ignorable="d"> <Design.DataContext> <vm:ColorEditorPageViewModel /> </Design.DataContext> <Grid ColumnDefinitions="*, 250"> <ContentControl x:Name="DemoControl" Classes="DemoUserControl"> <Grid RowDefinitions="Auto, 60, Auto" ColumnDefinitions="Auto, 26, Auto" HorizontalAlignment="Center" Margin="30"> <!-- <Label Classes="EditorHeader" Content="POPUP COLOR EDITOR"/> --> <mxe:PopupColorEditor Grid.Row="2" x:Name="PopupColorEditor" VerticalAlignment="Top" ReadOnly="{Binding IsChecked, ElementName=ReadOnlySelector}" ShowAlphaChannel="{Binding IsChecked, ElementName=AlphaChannelSelector}" ColorsShowMode="{Binding ColorsShowMode}" Color="#FF00B7B8" CustomColors="{Binding CustomColors2, Mode=TwoWay}"> <mxe:PopupColorEditor.PopupFooterButtons> <Binding Path="IsChecked" ElementName="ShowConfirmationButtonsSelector"> <Binding.Converter> <mx:BoolToObjectConverter> <mx:BoolToObjectConverter.TrueValue> <mxe:PopupFooterButtons>OkCancel</mxe:PopupFooterButtons> </mx:BoolToObjectConverter.TrueValue> <mx:BoolToObjectConverter.FalseValue> <mxe:PopupFooterButtons>None</mxe:PopupFooterButtons> </mx:BoolToObjectConverter.FalseValue> </mx:BoolToObjectConverter> </Binding.Converter> </Binding> </mxe:PopupColorEditor.PopupFooterButtons> </mxe:PopupColorEditor> <mxe:ColorEditor x:Name="ColorEditor" VerticalAlignment="Top" ReadOnly="{Binding IsChecked, ElementName=ReadOnlySelector}" ShowAlphaChannel="{Binding IsChecked, ElementName=AlphaChannelSelector}" ColorsShowMode="{Binding ColorsShowMode}" ShowConfirmationButtons="{Binding IsChecked, ElementName=ShowConfirmationButtonsSelector}" Color="#FF37C47F" CustomColors="{Binding CustomColors1, Mode=TwoWay}"/> <Grid Grid.Column="2" RowDefinitions="Auto, Auto" ColumnDefinitions="Auto, *" MinWidth="135"> <Border Height="40" Width="40" VerticalAlignment="Top" HorizontalAlignment="Left" CornerRadius="{StaticResource EditorCornerRadius}" BorderThickness="{StaticResource EditorBorderThickness}" BorderBrush="{DynamicResource Outline/Neutral/Transparent/Medium}" Background="{Binding Color, ElementName=ColorEditor, Converter={mxei:SolidColorBrushConverter}}"/> <Label Grid.Column="1" Margin="{StaticResource leftGroupMargin}" VerticalAlignment="Center" Content="{Binding Color, ElementName=ColorEditor}"/> </Grid> </Grid> </ContentControl> <!--Options--> <StackPanel Grid.Column="1"> <mxe:GroupBox Header="Properties" Classes="PropertiesGroup"> <StackPanel> <mxe:CheckEditor x:Name="AlphaChannelSelector" Content="Use Alpha Channel" IsChecked="True" Classes="PropertyEditor"/> <mxe:CheckEditor x:Name="ReadOnlySelector" Content="Read Only" Classes="PropertyEditor"/> <mxe:CheckEditor x:Name="StandardColorsSelector" Content="Show Standard Colors" IsChecked="{Binding ShowStandardColors, Mode=TwoWay}" Classes="PropertyEditor"/> <mxe:CheckEditor x:Name="CustomColorsSelector" Content="Show Custom Colors" IsChecked="{Binding ShowCustomColors, Mode=TwoWay}" Classes="PropertyEditor"/> <mxe:CheckEditor x:Name="ShowConfirmationButtonsSelector" Content="Show Confirmation Buttons" IsEnabled="{Binding IsChecked, ElementName=CustomColorsSelector}" Classes="PropertyEditor"/> </StackPanel> </mxe:GroupBox> </StackPanel> </Grid></UserControl>Как видно из кода, все окно приложения делится по сетке, и каждый элемент прилипает к своим ячейкам, а нужный коэффициент масштаба приложение выбирает само. Сперва создается элемент выбора цвета, затем он настраивается, а затем уже готовый элемент помещается в только что созданную сетку. Этот подход позволяет писать приложения, которые не зависят от масштаба и разрешения экрана.
Чтобы связать интерфейс с логикой, нужна будет ViewModel. Вот как он может выглядеть:
public partial class ColorEditorPageViewModel : PageViewModelBase { [ObservableProperty] bool showStandardColors = true; [ObservableProperty] bool showCustomColors = true; [ObservableProperty] ColorsShowMode colorsShowMode = ColorsShowMode.StandardColors | ColorsShowMode.CustomColors; [ObservableProperty] ObservableCollection<Color> customColors1 = new ObservableCollection<Color>() { Color.FromRgb(0x7d, 0xd7, 0xab), Color.FromRgb(0xc5, 0x94, 0x88), Color.FromRgb(0x47, 0xfe, 0xff), Color.FromRgb(0xe9, 0xbf, 0x3f), }; public ColorEditorPageViewModel() { } partial void OnShowStandardColorsChanged(bool value) { if(value) ColorsShowMode |= ColorsShowMode.StandardColors; else ColorsShowMode &= ~ColorsShowMode.StandardColors; } partial void OnShowCustomColorsChanged(bool value) { if (value) ColorsShowMode |= ColorsShowMode.CustomColors; else ColorsShowMode &= ~ColorsShowMode.CustomColors; } }Давай разберем, что здесь происходит. Вместо того чтобы вручную реализовывать громоздкий интерфейс INotifyPropertyChanged для каждого свойства, можно использовать атрибут [ из библиотеки CommunityToolkit.. Это современный подход с использованием генератора кода: одна строчка атрибута – и компилятор сам дописывает всю необходимую обвязку, чтобы интерфейс узнавал об изменениях свойства. А строчка ColorsShowMode="{ из XAML-файла теперь связана со свойством ColorsShowMode в ViewModel. Тип ObservableCollection< используется для коллекции цветов сознательно, чтобы интерфейс узнавал об изменениях, даже если они внесены программно, а не пользователем.

Такая связка – XAML-разметка для декларативного описания самого интерфейса и ViewModel для придания ему жизни – это и есть суть MVVM, и библиотека Eremex (да и вообще весь фреймворк Avalonia) очень хорошо ложится на эту концепцию.
www
Этот и другие примеры можно самостоятельно запустить в репозитории Eremex на GitHub.
Запуск в Linux
Разумеется, когда мы имеем дело с кроссплатформенными приложениями, грех не проверить, как же в реальности работает эта кроссплатформенность. Для этого установим на Ubuntu зависимости: libice6, libsm6 и libfontconfig1, а еще сам .NET (dotnet-sdk-8.), если он еще не установлен.
Запустить можно по команде dotnet , если есть уже готовая сборка с прошлых экспериментов, а можно собрать новую. Для этого есть команда сборки с ключом -–self-contained, которая соберет самодостаточную версию, которая не будет требовать наличия .NET на целевой платформе:
dotnet publish -c Release -r linux-x64 --self-contained trueПосле этого в папке bin/ окажется готовый к миграции билд, который запускается чуть ли не в чистом поле.

Главное преимущество, в котором я сейчас убедился, – с феноменальной точностью был воссоздан тот же интерфейс, что Windows. А значит, разработчикам придется поддерживать меньше вариантов, и качество продукта выиграет.
Выводы
Библиотека от Eremex показала себя отлично. Если цена тебя не смущает, могу смело порекомендовать ее для любых проектов. Она открывает разные полезные возможности, а главное — основанный на ней код можно запускать в разных ОС без изменений, что избавляет от постоянной головной боли при разработке кроссплатформенных приложений.
Реклама. АО «ЭРЕМЕКС». ИНН 9723094014. Erid: 2SDnjdn1YQe
