:pointerover, :focus, :pressed, :disabled, :checked, :empty, :selected), PseudoClasses.Set(), PseudoClasses.Remove(), Custom Pseudo-class, x:DataType, Compiled Bindings, {CompiledBinding}.У статті 29 ми детально вивчили WPF Triggers — Property Trigger, DataTrigger, MultiTrigger, EventTrigger, VisualStateManager. Це потужна система, але вона несе значний XML-шум: навіть проста зміна кольору при hover вимагає окремого блоку <Style.Triggers> із вкладеними тегами.
Avalonia вирішила цю проблему радикально — відмовилась від Triggers взагалі. У Avalonia немає Trigger, DataTrigger, MultiTrigger, EventTrigger. Натомість — CSS pseudo-classes та селектори, які вже знайомі кожному веброзробнику.
Це рішення має принципові наслідки:
Що це дає: Лаконічність. Те, що у WPF займає 8 рядків <Style.Triggers>, в Avalonia — один <Style Selector="Button:pointerover">. Знайомий синтаксис для тих, хто знає CSS. Відсутність концепції «порядку тригерів» — замість неї Specificity (стаття 27a).
Що це означає для WPF-розробника: При портуванні WPF-проєкту на Avalonia всі Trigger-и потрібно переписати на Selector + Pseudo-class. Але ця задача значно менш болісна, ніж здається — пряма відповідність між концепціями існує майже завжди.
У цій статті ми побудуємо повне відображення WPF → Avalonia для кожного типу Trigger і відпрацюємо кастомні pseudo-classes для власних стилів.
Avalonia надає набір вбудованих pseudo-classes, прив'язаних до внутрішніх станів контролу. Їх не потрібно «вмикати» — вони активуються автоматично відповідно до стану елемента.
| Pseudo-class | WPF-аналог | Активний коли |
|---|---|---|
:pointerover | IsMouseOver=True Trigger | Курсор над елементом |
:pressed | IsPressed=True Trigger | Елемент натиснутий (ліва кнопка) |
:focus | IsFocused=True Trigger | Елемент має keyboard focus |
:focus-within | немає | Будь-який нащадок має фокус |
:disabled | IsEnabled=False Trigger | IsEnabled="False" |
:enabled | IsEnabled=True | IsEnabled="True" (явно) |
:checked | IsChecked=True Trigger | CheckBox, ToggleButton позначені |
:unchecked | IsChecked=False | Знято позначку |
:indeterminate | IsChecked=null | Проміжний стан |
:selected | IsSelected=True | ListBoxItem, TabItem — вибрані |
:empty | немає | Колекція ItemsControl порожня |
:nth-child(n) | немає прямого аналогу | n-й дочірній елемент |
:first-child | немає | Перший дочірній |
:last-child | немає | Останній дочірній |
:not(selector) | немає | Елемент, що НЕ відповідає selector |
Зверніть на :focus-within та :not() — ці pseudo-classes взагалі не мають аналогів у WPF Triggers. Нарешті :nth-child, :first-child, :last-child — можливості, недоступні у WPF взагалі.
WPF:
<Style TargetType="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3730A3"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#312E81"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
</Style.Triggers>
</Style>
Avalonia — той самий результат:
<Style Selector="Button">
<Setter Property="Background" Value="#4F46E5"/>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#3730A3"/>
</Style>
<Style Selector="Button:pressed">
<Setter Property="Background" Value="#312E81"/>
</Style>
<Style Selector="Button:disabled">
<Setter Property="Opacity" Value="0.5"/>
</Style>
WPF: 12+ рядків вкладеного XML. Avalonia: 4 плоских <Style>. Семантика ідентична.
У WPF Trigger автоматично відкочує зміни, коли умова перестає виконуватись. В Avalonia — CSS Specificity та механізм Selector-ів гарантують те саме: коли :pointerover неактивний — відповідний <Style> не застосовується, і базові значення повертаються автоматично. Поведінка ідентична, механізм — CSS-like.
Це найбільш значуща відмінність між WPF і Avalonia. У WPF DataTrigger дозволяє реагувати на Binding-значення безпосередньо у XAML без жодного C#:
<!-- WPF DataTrigger -->
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="Background" Value="#ECFDF5"/>
</DataTrigger>
В Avalonia прямого аналогу DataTrigger немає. Натомість використовуються два підходи:
Підхід 1: Style Classes (рекомендований)
Замість того, щоб реагувати на значення Binding прямо у стилях, динамічно додаємо/видаляємо CSS-клас у code-behind або через спеціальні Behaviors:
// У ViewModel або code-behind
isCompletedBinding.Subscribe(isCompleted =>
{
if (isCompleted)
border.Classes.Add("completed");
else
border.Classes.Remove("completed");
});
<!-- У XAML — стиль реагує на клас -->
<Style Selector="Border.completed">
<Setter Property="Background" Value="#ECFDF5"/>
</Style>
Підхід 2: Avalonia Binding Classes (декларативний)
Avalonia підтримує спеціальний синтаксис Classes.className="{Binding PropertyName}" прямо у XAML:
<Border>
<Border.Classes>
<!-- Клас "completed" активний коли IsCompleted = True -->
<Classes>completed</Classes>
</Border.Classes>
<!-- Або через атрибутний синтаксис Avalonia 11.1+: -->
<Border Classes.completed="{Binding IsCompleted}"
Classes.urgent="{Binding IsUrgent}">
Це найближчий аналог WPF DataTrigger — декларативно у XAML, без C#.
Classes.className="{Binding BoolProp}" — найпотужніший інструмент data-driven стилізації у Avalonia. Коли BoolProp = True — клас додається. Коли False — видаляється. Стилі реагують автоматично через CSS Selector.Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<StackPanel.Styles>
<!-- Базова картка -->
<Style Selector="Border.card">
<Setter Property="Background" Value="White"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="20"/>
<Setter Property="BorderBrush" Value="#E5E7EB"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Transitions">
<Setter.Value>
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.15"/>
<BrushTransition Property="BorderBrush" Duration="0:0:0.15"/>
</Transitions>
</Setter.Value>
</Setter>
</Style>
<!-- Hover: :pointerover -->
<Style Selector="Border.card:pointerover">
<Setter Property="Background" Value="#F5F3FF"/>
<Setter Property="BorderBrush" Value="#A5B4FC"/>
</Style>
<!-- Картка "completed" клас -->
<Style Selector="Border.card.completed">
<Setter Property="Background" Value="#ECFDF5"/>
<Setter Property="BorderBrush" Value="#6EE7B7"/>
</Style>
<!-- Картка "urgent" клас -->
<Style Selector="Border.card.urgent">
<Setter Property="Background" Value="#FEF2F2"/>
<Setter Property="BorderBrush" Value="#FCA5A5"/>
</Style>
<!-- TextBox: базовий стан -->
<Style Selector="TextBox">
<Setter Property="Background" Value="#F9FAFB"/>
<Setter Property="BorderBrush" Value="#D1D5DB"/>
<Setter Property="BorderThickness" Value="1.5"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="12,8"/>
<Setter Property="Transitions">
<Setter.Value>
<Transitions>
<BrushTransition Property="BorderBrush" Duration="0:0:0.15"/>
<BrushTransition Property="Background" Duration="0:0:0.15"/>
</Transitions>
</Setter.Value>
</Setter>
</Style>
<!-- TextBox hover -->
<Style Selector="TextBox:pointerover">
<Setter Property="BorderBrush" Value="#9CA3AF"/>
</Style>
<!-- TextBox focus: синя рамка -->
<Style Selector="TextBox:focus">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#4F46E5"/>
</Style>
<!-- CheckBox checked: зелений текст поруч -->
<Style Selector="CheckBox:checked /template/ ContentPresenter">
<Setter Property="Foreground" Value="#10B981"/>
</Style>
<!-- CheckBox disabled -->
<Style Selector="CheckBox:disabled">
<Setter Property="Opacity" Value="0.4"/>
</Style>
</StackPanel.Styles>
<!-- Картки з різними класами -->
<Border Classes="card">
<TextBlock Text="Звичайне завдання — hover для зміни фону"/>
</Border>
<Border Classes="card completed">
<TextBlock Text="✓ Завершено — завжди зелений"/>
</Border>
<Border Classes="card urgent">
<TextBlock Text="🔥 Терміново — завжди червоний"/>
</Border>
<!-- TextBox із focus -->
<TextBox Watermark="Клікни — рамка стане синьою"/>
<!-- CheckBox: checked колір через /template/ -->
<StackPanel Spacing="8">
<CheckBox Content="Підтверджено" IsChecked="True"/>
<CheckBox Content="Очікує" IsChecked="False"/>
<CheckBox Content="Заблоковано" IsEnabled="False"/>
</StackPanel>
</StackPanel>
Вбудовані pseudo-classes охоплюють більшість стандартних сценаріїв, але іноді потрібно визначити власний стан для кастомного контролу. Наприклад: :loading, :active, :selected, :dragging.
В Avalonia для цього використовується API PseudoClasses.Set() та PseudoClasses.Remove() на рівні коду контролу. Цей метод доступний у класах, що успадковують StyledElement (тобто будь-який UI-елемент).
// CustomControl.cs
public class StatusCard : ContentControl
{
public static readonly StyledProperty<bool> IsLoadingProperty =
AvaloniaProperty.Register<StatusCard, bool>(nameof(IsLoading));
public bool IsLoading
{
get => GetValue(IsLoadingProperty);
set => SetValue(IsLoadingProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == IsLoadingProperty)
{
// Встановлюємо або знімаємо pseudo-class :loading
PseudoClasses.Set(":loading", (bool)e.NewValue!);
}
}
}
<!-- Використання у XAML: -->
<local:StatusCard IsLoading="True">
<TextBlock Text="Контент картки"/>
</local:StatusCard>
<!-- Стиль реагує на :loading -->
<Style Selector="local|StatusCard:loading">
<Setter Property="Opacity" Value="0.6"/>
</Style>
<Style Selector="local|StatusCard:loading /template/ Border">
<Setter Property="Background" Value="#EEF2FF"/>
</Style>
local|StatusCard — для кастомних типів у Avalonia Selector використовується namespace|TypeName замість WPF TargetType="local:StatusCard". Це прямо відповідає CSS-синтаксису для namespace-qualified типів.WPF MultiTrigger вимагає ANDкомбінації умов. В Avalonia ця ж логіка виражається ланцюжком pseudo-classes в одному Selector:
<!-- WPF MultiTrigger: IsSelected AND IsMouseOver -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#FEF3C7"/>
</MultiTrigger>
<!-- Avalonia: ланцюжок pseudo-classes = AND -->
<Style Selector="ListBoxItem:selected:pointerover">
<Setter Property="Background" Value="#FEF3C7"/>
</Style>
Якщо потрібно AND по власних класах та pseudo-classes — просто об'єднуємо:
<Style Selector="Border.card.urgent:pointerover">
<Setter Property="Background" Value="#FEE2E2"/>
</Style>
WPF EventTrigger запускає Storyboard у відповідь на RoutedEvent. В Avalonia:
Transitions на елементі (найпростіше, без анімаційного коду).Animation + KeyFrames (аналог WPF Storyboard, але у Avalonia стилі).<!-- Avalonia: плавна зміна Background при :pointerover через Transition -->
<Style Selector="Button">
<Setter Property="Transitions">
<Setter.Value>
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.2"/>
</Transitions>
</Setter.Value>
</Setter>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#3730A3"/>
</Style>
Avalonia Transitions — значно менше коду порівняно з WPF EventTrigger + Storyboard + ColorAnimation.
| WPF концепція | Avalonia еквівалент | Примітка |
|---|---|---|
Trigger Property="IsMouseOver" | Style Selector="...:pointerover" | Пряма відповідність |
Trigger Property="IsPressed" | Style Selector="...:pressed" | Пряма відповідність |
Trigger Property="IsFocused" | Style Selector="...:focus" | Пряма відповідність |
Trigger Property="IsEnabled" Value="False" | Style Selector="...:disabled" | Пряма відповідність |
Trigger Property="IsChecked" Value="True" | Style Selector="...:checked" | Пряма відповідність |
Trigger Property="IsSelected" Value="True" | Style Selector="...:selected" | Пряма відповідність |
DataTrigger Binding Value="True" | Classes.myClass="{Binding BoolProp}" | Непряма відповідність — через Classes |
DataTrigger Binding Value="someString" | Behaviors або code-behind | Немає прямого XAML-аналогу |
MultiTrigger (AND) | Ланцюжок Type:class1:class2 | Пряма відповідність |
EventTrigger + Storyboard | Transitions або Animation | Значно лаконічніше |
VisualStateManager | ControlTheme + pseudo-classes | Архітектурна відповідність |
| Кастомний VSM стан | PseudoClasses.Set(":state", val) | API-рівень |
TargetName у Trigger | /template/ ElementName у Selector | Синтаксична відповідність |
DataTrigger Binding Value="someString" — реакція на рядкове або числове значення Binding — не має прямого XAML-аналогу в Avalonia. Це справжня «діра» у функціональності. Рекомендоване рішення: IValueConverter у ViewModel, що перетворює значення на bool, або Behaviors із пакета Avalonia.Xaml.Behaviors. У більшості реальних сценаріїв це не проблема, але варто знати заздалегідь при плануванні портингу.Ціль: Написати перший Avalonia
Завдання: Картки навичок у профілі — hover підсвічує картку.
Border із класом skill-card: назва технології, іконка (емодзі), рівень (Beginner/Intermediate/Expert).Style Selector="Border.skill-card": Background="White", BorderBrush="#E5E7EB", CornerRadius="8".Style Selector="Border.skill-card:pointerover": BorderBrush="#A5B4FC", Background="#F5F3FF".Transitions на BorderBrush та Background (250мс) — плавний hover.Перевірка: наведення — картка плавно змінює колір. Відведення — плавно повертається.
Ціль: Реалізувати кастомний pseudo-class через PseudoClasses.Set().
Завдання: Картка з кнопкою «Активувати» — при натисканні картка переходить у стан :active.
UserControl або ContentControl підклас ActiveCard.IsActive (bool, StyledProperty).OnPropertyChanged: PseudoClasses.Set(":active", IsActive).Style Selector="local|ActiveCard:active /template/ Border" → Background="#EEF2FF", BorderBrush="#4F46E5".ToggleActiveCommand змінює IsActive.Перевірка: натискаємо «Активувати» — картка миттєво змінює вигляд. Натискаємо ще раз — повертається.
Ціль: Повний портинг WPF DataTrigger-списку задач на Avalonia.
З статті 29 (DataTrigger для статусної картки TaskItem):
WPF-картка на DataTrigger:
IsCompleted=True → зелений фон, strike-through текст, зелена іконка.Priority="High" → червоний фон.Портинг:
IsCompleted, Priority, Title.Classes.completed="{Binding IsCompleted}", Classes.urgent="{Binding IsUrgent}".Border.task-card.completed → Background="#ECFDF5", BorderBrush="#6EE7B7".TextBlock.task-title.completed → TextDecorations="Strikethrough", Foreground="#9CA3AF".Border.task-card.urgent → Background="#FEF2F2", BorderBrush="#FCA5A5".ItemsControl.Перевірка: той самий візуальний результат, що і у WPF-версії, але через Classes.xxx="{Binding}" замість DataTrigger.
Avalonia замінила всю систему WPF Triggers на CSS pseudo-classes — рішення більш лаконічне, більш знайоме і більш CSS-like.
Вбудовані pseudo-classes автоматично відображають стан контролу: :pointerover (hover), :pressed, :focus, :focus-within, :disabled, :checked, :unchecked, :selected, :empty, :nth-child(). Деякі не мають аналогів у WPF взагалі.
DataTrigger → Classes.prop="{Binding bool}" — Avalonia 11.1+ підтримує прив'язку класів прямо у XAML. Це найближчий аналог WPF DataTrigger для boolean-умов.
MultiTrigger → ланцюжок pseudo-classes у одному Selector: Button:pressed:focus — AND-умова автоматично.
EventTrigger → Transitions — CSS-like transition на властивостях елемента замість складного Storyboard.
Custom pseudo-class: PseudoClasses.Set(":stateName", value) у класі контролу → стиль Selector="local|Type:stateName" реагує автоматично.
Sentinel limitation: відсутній прямий аналог DataTrigger для нe-boolean значень — потрібен Converter або Behaviors.
| Задача | WPF рядків | Avalonia рядків |
|---|---|---|
| Hover-ефект кнопки | ~8 | ~3 |
| 3 стани (hover/pressed/disabled) | ~12 | ~6 |
| DataTrigger на bool | ~5 | ~2 (Classes binding) |
| MultiTrigger (AND 2 умови) | ~8 | ~2 |
| Кастомний стан | VSM + GoToState (~20) | PseudoClasses.Set (~5) |
Наступна стаття — Data Binding: режими та конвертери. OneWay, TwoWay, IValueConverter, StringFormat, TargetNullValue, FallbackValue, UpdateSourceTrigger — деталі, що відрізняють поверхневе знайомство з Binding від справжнього розуміння.
Triggers та Visual State Manager у WPF
Property Triggers, DataTrigger, MultiTrigger, EventTrigger та Visual State Manager — повний посібник з реактивної стилізації у WPF. Порівняння зі стилями і Avalonia-підходом.
Теми та ресурсні словники у WPF
Побудова системи тематизації WPF-додатку. DynamicResource, MergedDictionaries, Light/Dark теми з runtime-перемиканням, SystemColors, огляд MaterialDesignInXamlToolkit, MahApps.Metro та HandyControl.