Desktop UI

CSS-like стилі Avalonia

Система стилізації Avalonia — CSS-селектори, Style Classes, Nesting, Combinators та Specificity. Порівняння з WPF Style і практичні приклади hover-ефектів, zebra-рядків та динамічних класів.
Нові терміни у цій статті: CSS-like Selector, Style Classes (Classes), Pseudo-class (:pointerover, :focus, :pressed), Combinator (>, пробіл), :nth-child, Specificity, element.Classes.Add(), /template/ selector, inline style, ^ (ancestor selector).

Коли Avalonia вирішила не копіювати WPF

Якщо попередня стаття була присвячена системі стилів WPF — солідній, перевіреній, але спроектованій у 2006 році — то ця стаття про принципово інший підхід. Avalonia не просто перенесла WPF-стилі на нову платформу. Команда Avalonia зробила ставку на мову, яку вже знають мільйони розробників: CSS.

Це не метафора «схоже на CSS». Це буквально CSS-синтаксис селекторів, CSS-концепція специфічності, CSS-псевдокласи — але у межах .NET-фреймворку для десктопних додатків. Якщо ви колись стилізували елементи через button:hover { background: blue; } у браузері — ви вже розумієте 70% системи стилів Avalonia.

Чому це важливо? По-перше, нижчий поріг входу для веброзробників, що переходять у десктоп. По-друге, значно більша виразність — замість одного Implicit Style на тип ти маєш повну потужність CSS-селекторів: псевдокласи, комбінатори, nth-child, вкладені селектори. По-третє, менше коду для досягнення тих самих або складніших ефектів.

Але важливо розуміти: це не стаття про «Avalonia краща за WPF». Це стаття про те, що Avalonia вирішила іншу задачу іншими засобами. WPF стилі — зрілі, передбачувані, добре інтегровані з MVVM і ControlTemplate. Avalonia стилі — виразніші, знайомі веброзробникам, але з іншою кривою навчання. Порівняємо обидва підходи чесно.


Базова структура Style в Avalonia

Стиль в Avalonia, як і у WPF, є XAML-об'єктом класу Style. Але ключова відмінність — замість TargetType використовується атрибут Selector, і синтаксис цього селектора повністю позичений з CSS.

Розміщення стилів — у <Application.Styles> (глобально) або у <Window.Styles> / <UserControl.Styles>. Це перша відмінність від WPF: у WPF ресурси розміщуються у Resources, в Avalonia — у Styles.

Базовий синтаксис:

<Window.Styles>
    <Style Selector="Button">
        <Setter Property="Background" Value="#3B82F6"/>
        <Setter Property="Foreground" Value="White"/>
        <Setter Property="Padding"    Value="14,8"/>
    </Style>
</Window.Styles>

Виглядає майже як WPF, правда? Але Selector="Button" — це CSS-рядок, і він може бути набагато складнішим, ніж просто назва типу.

Де розміщувати стилі в Avalonia

МісцеСинтаксисScope
Глобальний<Application.Styles> у App.axamlВесь додаток
Вікно<Window.Styles> у MainWindow.axamlОдне вікно
Компонент<UserControl.Styles>Один UserControl
Окремий файл<StyleInclude Source="avares://..."/>Підключається де завгодно

Зверніть на <StyleInclude> — аналог WPF MergedDictionaries. Але замість ResourceDictionary Avalonia використовує окремі Styles-файли з розширенням .axaml.


Селектор за типом: повний аналог WPF Implicit Style

Найпростіший CSS-like селектор — це просто ім'я типу. Button — всі кнопки. TextBox — всі текстові поля. Поведінка ідентична WPF Implicit Style:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

У Avalonia <StackPanel.Styles> — це окрема колекція стилів, не Resources. Синтаксис подібний, але семантично різний. Ресурси (Resources) та стилі (Styles) в Avalonia — дві окремі системи, що існують паралельно.

Style Classes: CSS-класи у XAML

Тут Avalonia робить свій найсміливіший відхід від WPF. У WPF для того, щоб застосувати різні стилі до різних кнопок, потрібно використовувати Explicit Style з x:Key. В Avalonia — Style Classes: атрибут Classes на елементі, що точнісінько відповідає атрибуту class у HTML.

Порівняймо: у HTML ви пишете <button class="primary large">, і CSS-правило .primary { background: blue; } застосовується. В Avalonia ви пишете <Button Classes="primary large"/>, і стиль із Selector="Button.primary" застосовується автоматично. Синтаксис відрізняється, концепція — ідентична.

Клас-селектор у Avalonia — це ім'я типу + крапка + ім'я класу: Button.primary. Можна комбінувати: Button.primary.large означає кнопку, що має обидва класи одночасно.

Перший приклад із Classes

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зупиніться і порівняйте з WPF-підходом зі статті 27. Там:

<!-- WPF: потрібен x:Key і явне Style={StaticResource} -->
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="...">
<Button Style="{StaticResource PrimaryButton}"/>

В Avalonia:

<!-- Avalonia: клас-атрибут, як HTML -->
<Style Selector="Button.primary">
<Button Classes="primary"/>

Код стає виразнішим і коротшим. І, що важливо, один елемент може мати кілька класів одночасно.

Комбінація класів на одному елементі

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть: Classes="large rounded" — це два класи на одному елементі. Avalonia застосує стиль Button.large (збільшить шрифт і padding) і стиль Button.rounded (заокруглить кути) одночасно. Це CSS BEM-модифікатори у XAML.

Псевдокласи: стани контролу без коду

Псевдокласи в Avalonia — це вбудований механізм реакції на стан елемента: навів курсор, натиснув, отримав фокус, заблокований. У WPF для цього потрібні Trigger-и всередині Style (стаття 29). В Avalonia — просто додати псевдоклас до CSS-селектора.

Синтаксис: ім'я типу + двокрапка + стан. Наприклад, Button:pointerover — кнопка, на яку навели курсор.

Вбудовані псевдокласи Avalonia

ПсевдокласАналог у WPFКоли активний
:pointeroverIsMouseOver=True TriggerКурсор над елементом
:pressedIsPressed=True TriggerЕлемент натиснутий
:focusIsFocused=True TriggerЕлемент у фокусі
:disabledIsEnabled=False TriggerIsEnabled="False"
:checkedIsChecked=True TriggerCheckBox, ToggleButton позначені
:uncheckedIsChecked=FalseЗнято позначку
:emptyнемає аналогуКолекція порожня
:selectedIsSelected=TrueListBoxItem вибраний
:nth-child(n)немає аналогуn-й дочірній елемент

Hover-ефект:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

У WPF аналогічний ефект вимагав би <Style.Triggers> з <Trigger Property="IsMouseOver" Value="True"> всередині кожного стилю. В Avalonia — просто ще один <Style> із псевдокласом у селекторі. Жодних вкладених Trigger-ів, жодного шуму.

Focus-стан: для TextBox

Псевдоклас :focus дозволяє змінити зовнішній вигляд поля введення, коли воно у фокусі. Це дуже поширений UX-паттерн: рамка поля виділяється кольором акценту при кліку на нього. У WPF це потребує ControlTemplate або складного Trigger. В Avalonia — один рядок:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Превью використовує Avalonia Fluent Theme. Деталі рамки фокусу (наприклад, подвійна синя рамка) можуть відрізнятися від скріншоту у реальному Avalonia-проєкті — тема Fluent вже вбудовує власні focus-стилі через ControlTheme. Але Setter для BorderBrush у :focus-стилі перевизначить theme-значення.

Combinators: відносини між елементами

Combinators — це один із найпотужніших механізмів CSS, якого повністю бракує у WPF Styles. Вони дозволяють будувати селектори на основі структурних відносин між елементами у дереві: батьківський/дочірній, предок/нащадок.

В Avalonia підтримуються такі комбінатори:

КомбінаторСинтаксисЗначення
НащадокStackPanel TextBlockБудь-який TextBlock усередині StackPanel
Прямий нащадокStackPanel > TextBlockЛише безпосередній нащадок
ПредокTextBlock ^ StackPanelStackPanel, що є предком TextBlock

Нащадок-комбінатор: пробіл

Пробіл між двома типами означає «будь-який нащадок». Наприклад, Border TextBlock стилізує всі TextBlock-и, що знаходяться де завгодно всередині Border, на будь-якій глибині:

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Зверніть на точність: стиль Border.card TextBlock спрацює тільки для TextBlock-ів всередині Border із класом card. TextBlock поза карткою — не стилізується. Це значно точніший і безпечніший підхід, ніж WPF Implicit Style, що б'є по всіх елементах у scope.

:nth-child: зебра-рядки без коду

:nth-child(n) — псевдоклас, що вибирає кожен n-й дочірній елемент батьківського контейнера. Це дозволяє реалізувати класичний UI-паттерн «зебра» (чергування кольорів рядків) без жодного C#-коду та без AlternationCount, що потребує WPF.

Синтаксис:

  • :nth-child(2n) — кожен парний елемент (2-й, 4-й, 6-й, ...)
  • :nth-child(2n+1) або :nth-child(odd) — кожен непарний
  • :nth-child(3) — рівно третій елемент

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

У WPF зебра-рядки реалізуються через ListView.ItemContainerStyle з Trigger на AlternationIndex — і потребує встановлення AlternationCount на ListView. В Avalonia — один CSS-рядок :nth-child(2n) без жодних додаткових налаштувань.

Динамічні класи: Classes з C#

Один із найпрактичніших аспектів Style Classes — можливість керувати ними програматично з C# у реальному часі. Це відкриває сценарій, недоступний у WPF без Triggers або Value Converters: UI реагує на дані, перемикаючи класи.

В Avalonia кожен AvaloniaObject має властивість Classes типу Classes (колекція рядків). Методи:

  • element.Classes.Add("active") — додати клас
  • element.Classes.Remove("active") — прибрати клас
  • element.Classes.Set("active", isActive) — встановити або прибрати залежно від bool

Приклад: Toggle-кнопка зі зміною класу

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Стиль Border.status-card.active спрацює тільки коли елемент має обидва класи: status-card і active. Додавання або видалення класу active через C# миттєво перемикає зовнішній вигляд — без Dispatcher, без ручного оновлення UI, без Value Converters.


Specificity: пріоритет селекторів

Avalonia успадкувала також CSS-концепцію специфічності (Specificity) — правило вирішення конфліктів, коли декілька стилів намагаються встановити одну й ту саму властивість.

Специфічність обчислюється за трьома компонентами, аналогічно CSS:

КомпонентЩо враховуєтьсяВага
AКількість імен класів, псевдокласів і атрибутів10 за кожен
BКількість імен типів1 за кожен
CПорядок оголошення (при рівній специфічності)Останній виграє

Приклади конкретних специфічностей:

СелекторСпецифічністьПояснення
Button(0, 1)1 тип
Button.primary(10, 1)1 клас + 1 тип
Button.primary.large(20, 1)2 класи + 1 тип
Border.card Button(10, 2)1 клас + 2 типи
Button:pointerover(10, 1)Псевдоклас рівносильний класу
Button.primary:pointerover(20, 1)2 класо-подібних + 1 тип

Практичний наслідок: порядок має значення

Якщо два стилі мають однакову специфічність, виграє той, що оголошений пізніше. Тому завжди оголошуйте псевдокласи після базових стилів:

<!-- ✅ Правильний порядок: базовий → псевдокласи -->
<Style Selector="Button">
  <Setter Property="Background" Value="#4F46E5"/>
</Style>
<Style Selector="Button:pointerover">
  <Setter Property="Background" Value="#3730A3"/>     <!-- перекриє базовий -->
</Style>

<!-- ❌ Неправильний порядок: :pointerover першим -->
<Style Selector="Button:pointerover">
  <Setter Property="Background" Value="#3730A3"/>
</Style>
<Style Selector="Button">
  <Setter Property="Background" Value="#4F46E5"/>     <!-- перекриє :pointerover! -->
</Style>
На відміну від CSS у браузері, де .btn:hover має вищу специфічність ніж .btn (і порядок менш критичний), в Avalonia псевдоклас :pointerover рівносильний класу — специфічність Button:pointerover дорівнює Button.someClass. Тому порядок оголошення дуже важливий: псевдокласи мають йти після базових стилів.

/template/ selector: стилізація всередині шаблону

Один із найпотужніших і найспецифічніших для Avalonia CSS-розширень — /template/ selector. Він дозволяє прицільно стилізувати внутрішні елементи ControlTemplate контролу з зовнішнього стилю.

Синтаксис: Button /template/ BorderBorder всередині шаблону Button.

Це дозволяє коригувати деталі вигляду вбудованих контролів без повної заміни їх ControlTemplate. Наприклад, заокруглити кути TextBox:

<Style Selector="TextBox /template/ Border">
  <Setter Property="CornerRadius" Value="6"/>
</Style>
Використання /template/ selector потребує знання внутрішньої структури ControlTemplate конкретного контролу — які PART_ елементи там є. Для цього рекомендується переглядати вихідний код Avalonia (Themes/Fluent) або використовувати DevTools (F12 у Avalonia-додатку). У реальних проєктах /template/ найчастіше використовується для дрібного косметичного тюнінгу вбудованих контролів без необхідності писати повний ControlTheme.

Порівняння WPF Style vs Avalonia Style

Час підвести підсумок і порівняти обидва підходи систематично у зведеній таблиці. Ця таблиця — відповідь на запитання «що обрати і коли?»

АспектWPF StyleAvalonia Style (CSS-like)
СинтаксисTargetType="Button"Selector="Button"
Implicit (по типу)Style без x:KeyStyle Selector="Button"
CSS-класx:Key + Style="{StaticResource}"Classes="primary", Selector="Button.primary"
Hover-ефект<Trigger Property="IsMouseOver">Selector="Button:pointerover"
Focus-стан<Trigger Property="IsFocused">Selector="TextBox:focus"
НащадокНеможливо в XAMLSelector="Border TextBlock"
nth-childНеможливо (AlternationIndex)Selector="Border:nth-child(2n)"
СпецифічністьПріоритет: локальне > Trigger > SetterCSS Specificity (клас > тип, порядок)
Динамічний станTrigger + DependencyPropertyClasses.Add/Remove з C#
Розміщення<Control.Resources><Control.Styles>
Файл стилівResourceDictionary (.xaml)StyleInclude (.axaml)
Успадкування стилівBasedOnНема BasedOn, але selector вищої специфічності перекриває
Простота для веброзробниківСередняВисока (знайомий CSS)
Простота для .NET розробниківВисока (ресурсний підхід)Середня (потрібно знати selector-синтаксис)
Контроль над ControlTemplate<Style … BasedOn=…><Setter Property="Template">ControlTheme + /template/ selector

Стратегічний висновок

Ці два підходи не конкурують — вони просто виросли з різних традицій. WPF Style виріс з XAML-ресурсного підходу, де стиль — це іменований об'єкт, який передається по дереву ресурсів. Avalonia Style виріс з ідеї «нехай розробник пише CSS-рядки, які він вже знає».

Для команди, що переходить з React/Angular/Vue у десктоп — Avalonia Style буде природнішою. Для .NET-команди з WPF-досвідом — WPF Style звичніша. Програма курсу побудована так, щоб ви добре знали обидва підходи.


Практичні завдання


Підсумок

Що ми вивчили у цій статті

Ця стаття показала, що Avalonia підійшла до задачі стилізації з принципово іншого боку, ніж WPF — і результат вийшов виразнішим і лаконічнішим для сценаріїв, де потрібна складна логіка вибору елементів.

CSS-like Selector замінює TargetType: рядок у форматі CSS (Button, Button.primary, Button:pointerover, Border TextBlock) — значно виразніший за один TargetType.

Style Classes (Classes="primary large") — повний аналог HTML-атрибута class. Дозволяє застосовувати кілька стилів до одного елемента без BasedOn. Динамічне додавання/видалення через Classes.Add/Remove/Set.

Псевдокласи (:pointerover, :focus, :pressed, :disabled, :nth-child) — замінюють Trigger-и у WPF для реакції на стан. Значно коротший синтаксис.

Combinators (StackPanel TextBlock, StackPanel > TextBlock) — дозволяють стилізувати елементи у контексті батьківського. Відсутні у WPF.

Specificity — CSS-правило пріоритетів: більш специфічний селектор перемагає. При рівній специфічності — останній у порядку оголошення.

/template/ selector — унікальний для Avalonia спосіб стилізувати внутрішні елементи ControlTemplate без його повної заміни.

Що далі

Наступна стаття — Тригери у WPF (Style.Triggers, DataTrigger, MultiTrigger). Ви побачите, що WPF-тригери вирішують ту саму задачу зміни стану, що й Avalonia-псевдокласи, але через власний XAML-механізм. Після вивчення обох підходів ви зможете свідомо обирати платформу та інструменти для конкретної задачі.