Classes), Pseudo-class (:pointerover, :focus, :pressed), Combinator (>, пробіл), :nth-child, Specificity, element.Classes.Add(), /template/ selector, inline style, ^ (ancestor selector).Якщо попередня стаття була присвячена системі стилів 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 стилі — виразніші, знайомі веброзробникам, але з іншою кривою навчання. Порівняємо обидва підходи чесно.
Стиль в 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-рядок, і він може бути набагато складнішим, ніж просто назва типу.
| Місце | Синтаксис | Scope |
|---|---|---|
| Глобальний | <Application.Styles> у App.axaml | Весь додаток |
| Вікно | <Window.Styles> у MainWindow.axaml | Одне вікно |
| Компонент | <UserControl.Styles> | Один UserControl |
| Окремий файл | <StyleInclude Source="avares://..."/> | Підключається де завгодно |
Зверніть на <StyleInclude> — аналог WPF MergedDictionaries. Але замість ResourceDictionary Avalonia використовує окремі Styles-файли з розширенням .axaml.
Найпростіший CSS-like селектор — це просто ім'я типу. Button — всі кнопки. TextBox — всі текстові поля. Поведінка ідентична WPF Implicit Style:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="14,8"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</StackPanel.Styles>
<Button Content="Перша кнопка"/>
<Button Content="Друга кнопка"/>
<Button Content="Третя кнопка"/>
</StackPanel>
<StackPanel.Styles> — це окрема колекція стилів, не Resources. Синтаксис подібний, але семантично різний. Ресурси (Resources) та стилі (Styles) в Avalonia — дві окремі системи, що існують паралельно.Тут 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 означає кнопку, що має обидва класи одночасно.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<StackPanel.Styles>
<!-- Базовий стиль для всіх кнопок -->
<Style Selector="Button">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="16,9"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<!-- .primary клас: синій фон -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<!-- .secondary клас: сірий фон -->
<Style Selector="Button.secondary">
<Setter Property="Background" Value="#6B7280"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<!-- .danger клас: червоний фон -->
<Style Selector="Button.danger">
<Setter Property="Background" Value="#EF4444"/>
<Setter Property="Foreground" Value="White"/>
</Style>
<!-- .outline клас: прозорий фон, кольорова рамка -->
<Style Selector="Button.outline">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="#4F46E5"/>
<Setter Property="BorderBrush" Value="#4F46E5"/>
<Setter Property="BorderThickness" Value="1.5"/>
</Style>
</StackPanel.Styles>
<Button Content="Primary" Classes="primary"
Command="{Binding ShowMessageCommand}"
CommandParameter="Primary натиснуто!"/>
<Button Content="Secondary" Classes="secondary"/>
<Button Content="Danger" Classes="danger"/>
<Button Content="Outline" Classes="outline"/>
</StackPanel>
Зупиніться і порівняйте з 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)...
<StackPanel Margin="20" Spacing="12">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="12,8"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<!-- .large модифікатор: більший розмір -->
<Style Selector="Button.large">
<Setter Property="Padding" Value="24,14"/>
<Setter Property="FontSize" Value="16"/>
</Style>
<!-- .small модифікатор -->
<Style Selector="Button.small">
<Setter Property="Padding" Value="8,4"/>
<Setter Property="FontSize" Value="11"/>
</Style>
<!-- .rounded модифікатор -->
<Style Selector="Button.rounded">
<Setter Property="CornerRadius" Value="20"/>
</Style>
</StackPanel.Styles>
<Button Content="Normal" Classes=""/>
<Button Content="Large" Classes="large"/>
<Button Content="Small" Classes="small"/>
<Button Content="Large + Rounded" Classes="large rounded"/>
<Button Content="Small + Rounded" Classes="small rounded"/>
</StackPanel>
Classes="large rounded" — це два класи на одному елементі. Avalonia застосує стиль Button.large (збільшить шрифт і padding) і стиль Button.rounded (заокруглить кути) одночасно. Це CSS BEM-модифікатори у XAML.Псевдокласи в Avalonia — це вбудований механізм реакції на стан елемента: навів курсор, натиснув, отримав фокус, заблокований. У WPF для цього потрібні Trigger-и всередині Style (стаття 29). В Avalonia — просто додати псевдоклас до CSS-селектора.
Синтаксис: ім'я типу + двокрапка + стан. Наприклад, Button:pointerover — кнопка, на яку навели курсор.
| Псевдоклас | Аналог у WPF | Коли активний |
|---|---|---|
:pointerover | IsMouseOver=True Trigger | Курсор над елементом |
:pressed | IsPressed=True Trigger | Елемент натиснутий |
:focus | IsFocused=True Trigger | Елемент у фокусі |
:disabled | IsEnabled=False Trigger | IsEnabled="False" |
:checked | IsChecked=True Trigger | CheckBox, ToggleButton позначені |
:unchecked | IsChecked=False | Знято позначку |
:empty | немає аналогу | Колекція порожня |
:selected | IsSelected=True | ListBoxItem вибраний |
:nth-child(n) | немає аналогу | n-й дочірній елемент |
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="16,9"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
<!-- Hover: темніший відтінок при наведенні -->
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#3730A3"/>
</Style>
<!-- Pressed: ще темніший при натисканні -->
<Style Selector="Button:pressed">
<Setter Property="Background" Value="#312E81"/>
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.97" ScaleY="0.97"/>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<Button Content="Наведи та натисни (hover + pressed)"/>
<Button Content="Ще одна кнопка з тим самим ефектом"/>
</StackPanel>
<Style.Triggers> з <Trigger Property="IsMouseOver" Value="True"> всередині кожного стилю. В Avalonia — просто ще один <Style> із псевдокласом у селекторі. Жодних вкладених Trigger-ів, жодного шуму.Псевдоклас :focus дозволяє змінити зовнішній вигляд поля введення, коли воно у фокусі. Це дуже поширений UX-паттерн: рамка поля виділяється кольором акценту при кліку на нього. У WPF це потребує ControlTemplate або складного Trigger. В Avalonia — один рядок:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="14">
<StackPanel.Styles>
<Style Selector="TextBox">
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="BorderBrush" Value="#D1D5DB"/>
<Setter Property="Background" Value="#F9FAFB"/>
</Style>
<!-- Focus: акцентна рамка при фокусі -->
<Style Selector="TextBox:focus">
<Setter Property="BorderBrush" Value="#4F46E5"/>
<Setter Property="Background" Value="White"/>
</Style>
<!-- disabled: приглушений вигляд -->
<Style Selector="TextBox:disabled">
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Background" Value="#F3F4F6"/>
</Style>
</StackPanel.Styles>
<TextBlock Text="Ім'я" FontSize="12" Foreground="Gray"/>
<TextBox/>
<TextBlock Text="Email" FontSize="12" Foreground="Gray"/>
<TextBox/>
<TextBlock Text="Заблоковане поле" FontSize="12" Foreground="Gray"/>
<TextBox Text="Тільки читання" IsEnabled="False"/>
</StackPanel>
BorderBrush у :focus-стилі перевизначить theme-значення.Combinators — це один із найпотужніших механізмів CSS, якого повністю бракує у WPF Styles. Вони дозволяють будувати селектори на основі структурних відносин між елементами у дереві: батьківський/дочірній, предок/нащадок.
В Avalonia підтримуються такі комбінатори:
| Комбінатор | Синтаксис | Значення |
|---|---|---|
| Нащадок | StackPanel TextBlock | Будь-який TextBlock усередині StackPanel |
| Прямий нащадок | StackPanel > TextBlock | Лише безпосередній нащадок |
| Предок | TextBlock ^ StackPanel | StackPanel, що є предком TextBlock |
Пробіл між двома типами означає «будь-який нащадок». Наприклад, Border TextBlock стилізує всі TextBlock-и, що знаходяться де завгодно всередині Border, на будь-якій глибині:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<StackPanel.Styles>
<!-- Всі TextBlock всередині Border.card — білі -->
<Style Selector="Border.card TextBlock">
<Setter Property="Foreground" Value="White"/>
</Style>
<!-- Заголовок картки — більший шрифт -->
<Style Selector="Border.card TextBlock.title">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<!-- Підзаголовок — прозоріший -->
<Style Selector="Border.card TextBlock.subtitle">
<Setter Property="Opacity" Value="0.75"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Margin" Value="0,4,0,0"/>
</Style>
</StackPanel.Styles>
<!-- Картка 1: темний фон — текст стане білим автоматично -->
<Border Classes="card" Background="#1E1B4B"
CornerRadius="12" Padding="20">
<StackPanel>
<TextBlock Classes="title" Text="Pro Plan"/>
<TextBlock Classes="subtitle" Text="Необмежена кількість проєктів"/>
</StackPanel>
</Border>
<!-- Картка 2 -->
<Border Classes="card" Background="#14532D"
CornerRadius="12" Padding="20">
<StackPanel>
<TextBlock Classes="title" Text="Business Plan"/>
<TextBlock Classes="subtitle" Text="Командний доступ до 50 учасників"/>
</StackPanel>
</Border>
<!-- Звичайний TextBlock поза карткою — колір за замовчуванням -->
<TextBlock Text="Цей текст поза Border.card — колір стандартний"/>
</StackPanel>
Зверніть на точність: стиль Border.card TextBlock спрацює тільки для TextBlock-ів всередині Border із класом card. TextBlock поза карткою — не стилізується. Це значно точніший і безпечніший підхід, ніж WPF Implicit Style, що б'є по всіх елементах у scope.
: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)...
<StackPanel Margin="20">
<StackPanel.Styles>
<!-- Базовий стиль для рядків таблиці -->
<Style Selector="Border.row">
<Setter Property="Padding" Value="12,10"/>
<Setter Property="Background" Value="White"/>
</Style>
<!-- Парні рядки (2-й, 4-й, 6-й...) — світло-сірий фон -->
<Style Selector="Border.row:nth-child(2n)">
<Setter Property="Background" Value="#F3F4F6"/>
</Style>
<!-- Hover будь-якого рядка -->
<Style Selector="Border.row:pointerover">
<Setter Property="Background" Value="#EEF2FF"/>
</Style>
<Style Selector="Border.row TextBlock">
<Setter Property="FontSize" Value="13"/>
</Style>
</StackPanel.Styles>
<Border Classes="row"><TextBlock Text="Іван Коваленко — Kyiv"/></Border>
<Border Classes="row"><TextBlock Text="Oksana Marchenko — Lviv"/></Border>
<Border Classes="row"><TextBlock Text="Петро Шевченко — Kharkiv"/></Border>
<Border Classes="row"><TextBlock Text="Тетяна Бондаренко — Odessa"/></Border>
<Border Classes="row"><TextBlock Text="Олексій Мельник — Dnipro"/></Border>
<Border Classes="row"><TextBlock Text="Світлана Кравченко — Zaporizhzhia"/></Border>
</StackPanel>
ListView.ItemContainerStyle з Trigger на AlternationIndex — і потребує встановлення AlternationCount на ListView. В Avalonia — один CSS-рядок :nth-child(2n) без жодних додаткових налаштувань.Один із найпрактичніших аспектів 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) — встановити або прибрати залежно від boolLoading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<StackPanel.Styles>
<Style Selector="Border.status-card">
<Setter Property="Padding" Value="16"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Background" Value="#F3F4F6"/>
</Style>
<Style Selector="Border.status-card.active">
<Setter Property="Background" Value="#ECFDF5"/>
</Style>
<Style Selector="Border.status-card.error">
<Setter Property="Background" Value="#FEF2F2"/>
</Style>
<Style Selector="Border.status-card TextBlock.label">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#6B7280"/>
</Style>
<Style Selector="Border.status-card.active TextBlock.label">
<Setter Property="Foreground" Value="#059669"/>
</Style>
<Style Selector="Border.status-card.error TextBlock.label">
<Setter Property="Foreground" Value="#DC2626"/>
</Style>
<Style Selector="Button">
<Setter Property="Padding" Value="12,7"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
</Style>
</StackPanel.Styles>
<Border x:Name="statusCard" Classes="status-card">
<TextBlock Classes="label" Text="● Статус: очікування"/>
</Border>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="✓ Активний"
Command="{Binding ShowMessageCommand}"
CommandParameter="active"/>
<Button Content="✗ Помилка"
Command="{Binding ShowMessageCommand}"
CommandParameter="error"/>
<Button Content="○ Скинути"
Command="{Binding ShowMessageCommand}"
CommandParameter="reset"/>
</StackPanel>
</StackPanel>
// Code-behind: керуємо класами програматично
private void Button_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
var action = button.CommandParameter?.ToString();
// Скидаємо всі стани
statusCard.Classes.Remove("active");
statusCard.Classes.Remove("error");
// Встановлюємо новий стан
switch (action)
{
case "active":
statusCard.Classes.Add("active");
statusLabel.Text = "● Статус: активний";
break;
case "error":
statusCard.Classes.Add("error");
statusLabel.Text = "● Статус: помилка";
break;
case "reset":
statusLabel.Text = "● Статус: очікування";
break;
}
}
Стиль Border.status-card.active спрацює тільки коли елемент має обидва класи: status-card і active. Додавання або видалення класу active через C# миттєво перемикає зовнішній вигляд — без Dispatcher, без ручного оновлення UI, без Value Converters.
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>
.btn:hover має вищу специфічність ніж .btn (і порядок менш критичний), в Avalonia псевдоклас :pointerover рівносильний класу — специфічність Button:pointerover дорівнює Button.someClass. Тому порядок оголошення дуже важливий: псевдокласи мають йти після базових стилів.Один із найпотужніших і найспецифічніших для Avalonia CSS-розширень — /template/ selector. Він дозволяє прицільно стилізувати внутрішні елементи ControlTemplate контролу з зовнішнього стилю.
Синтаксис: Button /template/ Border — Border всередині шаблону 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 | Avalonia Style (CSS-like) |
|---|---|---|
| Синтаксис | TargetType="Button" | Selector="Button" |
| Implicit (по типу) | Style без x:Key | Style 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" |
| Нащадок | Неможливо в XAML | Selector="Border TextBlock" |
| nth-child | Неможливо (AlternationIndex) | Selector="Border:nth-child(2n)" |
| Специфічність | Пріоритет: локальне > Trigger > Setter | CSS Specificity (клас > тип, порядок) |
| Динамічний стан | Trigger + DependencyProperty | Classes.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 звичніша. Програма курсу побудована так, щоб ви добре знали обидва підходи.
Ціль: Відчути різницю між WPF x:Key і Avalonia Classes.
Завдання: Реалізуйте пагінацію — рядок кнопок з цифрами сторінок.
Selector="Button": Padding="8,6", FontSize="13", Background="Transparent", Foreground="#374151".Selector="Button.active": Background="#4F46E5", Foreground="White", FontWeight="Bold".Selector="Button:pointerover": Background="#F3F4F6".Selector="Button.active:pointerover": Background="#3730A3" (темніший акцент при наведенні на активну).← 1 2 3 ... 10 →, де 3 — активна (Classes="active").Ключова перевірка: змініть Classes="active" з кнопки 3 на кнопку 5 — всього один атрибут.
Ціль: Реалізувати повний hover-ефект через CSS-like Selectors.
Завдання: Сітка карток послуг (3 картки горизонтально або вертикально).
Selector="Border.service-card": Background="White", CornerRadius="12", Padding="20", легка тінь через BoxShadow.Selector="Border.service-card:pointerover": Background="#F5F3FF", тінь сильніша.Selector="Border.service-card:pointerover TextBlock.icon": Foreground="#4F46E5"..icon), TextBlock-заголовок (.title), TextBlock-опис (.description).Перевірка: при наведенні на картку фон змінюється і іконка перефарбовується без єдиного рядка C#.
Ціль: Поєднати :nth-child, :pointerover і програматичне Classes.Set.
Завдання: Таблиця користувачів із виділенням вибраного рядка.
Border.row стиль із зебра-забарвленням через :nth-child(2n).Border.row:pointerover.Border.row.selected: Background="#EEF2FF", BorderBrush="#4F46E5", BorderThickness="0,0,0,2".Classes.Set("selected", true/false):
private Border? _selected;
private void Row_Tapped(object? sender, TappedEventArgs e)
{
_selected?.Classes.Set("selected", false);
_selected = (Border)sender!;
_selected.Classes.Set("selected", true);
}
.badge, де .badge.active — зелений, .badge.inactive — сірий).Ключова перевірка: зебра не зламалась після вибору рядка, hover працює на всіх рядках, у наступній сесії можна переписати те саме у MVVM через IsSelected binding.
Ця стаття показала, що 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-механізм. Після вивчення обох підходів ви зможете свідомо обирати платформу та інструменти для конкретної задачі.
Стилі WPF — CSS для десктопу
Глибоке занурення в систему стилів WPF. Implicit та Explicit стилі, BasedOn успадкування, область дії та пріоритети. Від аналогії з CSS до повної стилізації форми.
Control Templates — Частина 1. Концепція та TemplateBinding
Повне розуміння ControlTemplate у WPF. Як замінити зовнішній вигляд будь-якого контролу повністю — від мінімального прикладу до кастомної кнопки з TemplateBinding.