Property Trigger, DataTrigger, MultiTrigger, MultiDataTrigger, EventTrigger, VisualStateManager (VSM), VisualStateGroup, VisualState, GoToState(), VisualTransition, Style.Triggers, ControlTemplate.Triggers.У статтях 27–28 ми навчились застосовувати стилі та шаблони — але ці стилі завжди статичні: Background="#4F46E5" — і навіки синій. Реальний UI живий: кнопка темніє при наведенні, банер з'являється коли форма невалідна, панель змінює колір коли тасок завершено. Для такої реактивної поведінки у WPF існує механізм Triggers.
Trigger — це умова: «якщо значення певної властивості дорівнює X — застосуй ці Setter-и. Якщо умова перестала виконуватися — автоматично відкоти зміни назад». Це «автоматичне відкочування» є принциповою особливістю Trigger: ви не пишете логіку «прибрати стиль» — WPF робить це сам, коли умова вже не виконується.
У WPF є чотири типи Trigger:
| Тип | Де використовується | Умова |
|---|---|---|
Trigger | Style.Triggers, ControlTemplate.Triggers | Властивість DependencyProperty контролу |
DataTrigger | Style.Triggers, DataTemplate.Triggers | Binding-значення (із ViewModel, DataContext) |
MultiTrigger | Style.Triggers | Кілька Trigger-умов одночасно (AND) |
MultiDataTrigger | Style.Triggers | Кілька DataTrigger-умов одночасно (AND) |
EventTrigger | Style.Triggers, ControlTemplate.Triggers | Подія (RoutedEvent), запускає Storyboard |
Кожен тип вирішує свою задачу. Розберемо їх по черзі.
Trigger — найпростіший і найчастіше вживаний тип. Він слідкує за значенням DependencyProperty контролу і застосовує Setter-и, коли це значення збігається з Value.
<Style TargetType="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Style.Triggers>
<!-- Коли IsMouseOver = True: темніший фон -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3730A3"/>
</Trigger>
<!-- Коли IsPressed = True: ще темніший -->
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#312E81"/>
</Trigger>
<!-- Коли IsEnabled = False: прозорий -->
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
</Style.Triggers>
</Style>
Зверніть на принципово важливу деталь: у цьому прикладі ми не пишемо «коли IsMouseOver = False — повернути фон назад». WPF робить це автоматично. Коли умова перестає виконуватися, всі Setter-и Trigger автоматично відкочуються до базового значення. Це ключова різниця між Trigger і ручним обробником події.
Trigger можна розміщувати у двох місцях:
Style.Triggers — змінює властивості самого контролу (ті, що задані через Setter у стилі або безпосередньо). Доступ тільки до властивостей хост-контролу.
ControlTemplate.Triggers — всередині ControlTemplate. Може змінювати властивості Named елементів шаблону через TargetName. Більш потужний, але вимагає написання (або модифікації) шаблону.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Padding" Value="16,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3730A3"/>
<Setter Property="Cursor" Value="Hand"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#312E81"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Cursor" Value="Arrow"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<Button Content="Hover → прес → відпусти"
Command="{Binding ShowMessageCommand}"
CommandParameter="Клік!"/>
<Button Content="Ще одна кнопка"/>
<Button Content="Заблокована" IsEnabled="False"/>
</StackPanel>
Style.Triggers дають ідентичний результат. Важлива деталь: IsPressed спрацьовує лише якщо натиснути і тримати мишу безпосередньо над кнопкою — при відведенні миші під час натиснення IsPressed стає False.Якщо кілька Trigger-ів застосовують одну й ту саму властивість — останній у списку виграє. Тому порядок Trigger-ів у Style.Triggers важливий: більш специфічні умови мають йти після менш специфічних. IsPressed варто розміщувати після IsMouseOver, тому що IsPressed=True завжди супроводжується IsMouseOver=True — і якщо IsMouseOver стоїть пізніше, він перекриє IsPressed.
<!-- ✅ Правильний порядок: спочатку менш специфічне -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3730A3"/>
</Trigger>
<Trigger Property="IsPressed" Value="True"> <!-- Цей виграє при натисканні -->
<Setter Property="Background" Value="#312E81"/>
</Trigger>
<!-- ❌ Неправильний порядок: IsPressed буде перекрито IsMouseOver -->
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#312E81"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True"> <!-- Перекриє IsPressed! -->
<Setter Property="Background" Value="#3730A3"/>
</Trigger>
DataTrigger — найважливіший тип для MVVM-архітектури. На відміну від Trigger, що слідкує за DependencyProperty контролу, DataTrigger стежить за значенням Binding. Це означає, що умовою може бути будь-яка властивість ViewModel, результат конвертера, або навіть статичне значення через {x:Static}.
Синтаксис: <DataTrigger Binding="{Binding PropertyName}" Value="TargetValue">. Коли значення Binding дорівнює Value — Setter-и застосовуються. Як тільки значення змінюється — відкочуються.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="12">
<StackPanel.Resources>
<!-- DataTemplate для відображення одного завдання -->
<DataTemplate x:Key="TaskItemTemplate">
<Border CornerRadius="8" Padding="14,10" Margin="0,0,0,4">
<!-- DataTrigger: колір залежить від Status -->
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F9FAFB"/>
<Style.Triggers>
<!-- IsCompleted = true → зелений фон -->
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="Background" Value="#ECFDF5"/>
</DataTrigger>
<!-- Priority = High → червоний фон -->
<DataTrigger Binding="{Binding Priority}" Value="High">
<Setter Property="Background" Value="#FEF2F2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock VerticalAlignment="Center" FontSize="18">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="○"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="Text" Value="✓"/>
<Setter Property="Foreground" Value="#10B981"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding Title}"
FontSize="14" VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCompleted}" Value="True">
<Setter Property="TextDecorations" Value="Strikethrough"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<Border CornerRadius="4" Padding="6,2" VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Priority}" Value="High">
<Setter Property="Background" Value="#FEE2E2"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Priority}" FontSize="11"
Foreground="#EF4444" FontWeight="SemiBold"/>
</Border>
</StackPanel>
</Border>
</DataTemplate>
</StackPanel.Resources>
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentPresenter ContentTemplate="{StaticResource TaskItemTemplate}"
Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Items>
<!-- Симульовані дані через прямий ItemsControl -->
<local:TaskItem Title="Написати документацію"
IsCompleted="True" Priority="Normal"/>
<local:TaskItem Title="Виправити критичний баг"
IsCompleted="False" Priority="High"/>
<local:TaskItem Title="Code Review"
IsCompleted="False" Priority="Normal"/>
<local:TaskItem Title="Розгорнути на staging"
IsCompleted="True" Priority="High"/>
</ItemsControl.Items>
</ItemsControl>
</StackPanel>
ItemsControl прив'язується до ObservableCollection<TaskViewModel>, а DataTrigger реагує на властивість IsCompleted з ViewModel через INPC. Важливо: DataTriggerне потребує INotifyPropertyChanged безпосередньо — достатньо, щоб Binding оновлювався при зміні.DataTrigger дуже часто застосовується у двох контекстах:
Style.Triggers на Border, TextBlock тощо — стилізація елементів у шаблоні.DataTemplate.Triggers — на весь DataTemplate. Рідше, але корисно для зміни вигляду всього рядка.MultiTrigger — це AND-комбінація кількох Trigger.Conditions. Setter-и застосовуються лише тоді, коли всі умови виконуються одночасно.
<Style TargetType="ListBoxItem">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected" Value="True"/>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#FEF3C7"/> <!-- Жовтий -->
<Setter Property="Foreground" Value="#92400E"/>
</MultiTrigger>
<!-- Звичайне виділення (тільки IsSelected) -->
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#EEF2FF"/>
<Setter Property="Foreground" Value="#3730A3"/>
</Trigger>
</Style.Triggers>
</Style>
MultiTrigger (AND-умова — більш специфічна) стоїть перед звичайним Trigger. Але оскільки умови не перетинаються (MultiTrigger = Is_Selected AND IsMouseOver, а Trigger = лише IsSelected) — порядок тут менш критичний. Однак для надійності — завжди ставте складніші умови перед простішими.MultiDataTrigger аналогічна MultiTrigger, але умови задаються через DataTrigger.Binding:
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<StackPanel.Resources>
<Style TargetType="Border">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Style.Triggers>
<!-- Одна умова: IsUrgent -->
<DataTrigger Binding="{Binding IsUrgent}" Value="True">
<Setter Property="Background" Value="#FEF3C7"/>
</DataTrigger>
<!-- AND: IsUrgent AND IsOverdue → критичний стан -->
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsUrgent}" Value="True"/>
<Condition Binding="{Binding IsOverdue}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FEE2E2"/>
<Setter Property="BorderBrush" Value="#EF4444"/>
<Setter Property="BorderThickness" Value="2"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<!-- Нормальний -->
<Border DataContext="{Binding NormalTask}">
<TextBlock Text="Звичайне завдання"/>
</Border>
<!-- Термінове (IsUrgent = true) -->
<Border DataContext="{Binding UrgentTask}">
<TextBlock Text="⚡ Термінове завдання"/>
</Border>
<!-- Термінове + прострочене (AND) -->
<Border DataContext="{Binding OverdueTask}">
<TextBlock Text="🔥 Термінове + прострочене!"/>
</Border>
</StackPanel>
EventTrigger — особливий вид тригера. Він не задає умову (значення = X), а реагує на RoutedEvent. Єдина дія, яку він може виконати — запустити Storyboard (анімацію). Setter-ів у EventTrigger немає.
У цій статті ми лише познайомимося з синтаксисом — детально анімації розглядаються у Блоці 11.
<Style TargetType="Button">
<Style.Triggers>
<!-- Коли відбувається подія MouseEnter: запустити анімацію -->
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.7" Duration="0:0:0.15"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="1.0" Duration="0:0:0.15"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
EventTrigger не має автоматичного відкочування, на відміну від Trigger. Він просто запускає анімацію при події. Щоб повернути стан — потрібен окремий EventTrigger на зворотну подію (наприклад, MouseLeave). Це потребує парної обробки кожного переходу.Visual State Manager (VSM) — це більш структурований і масштабований підхід до управління станами контролу, запозичений з Silverlight і прийнятий у WPF. Замість набору розрізнених Trigger-ів VSM описує явні поіменовані стани контролу та переходи між ними.
У VSM контрол має групи станів (VisualStateGroup), де кожна група містить взаємовиключні стани (VisualState). Наприклад, група CommonStates може містити стани Normal, MouseOver, Pressed, Disabled. Контрол може перебувати одночасно в одному стані з кожної групи.
Кожен VisualState містить Storyboard, що опискує як виглядає контрол у цьому стані. Коли клас контролу викликає VisualStateManager.GoToState(this, "MouseOver", useTransitions: true) — VSM плавно переводить контрол із поточного стану у новий.
<ControlTemplate TargetType="Button">
<Border x:Name="RootBorder"
Background="{TemplateBinding Background}"
CornerRadius="8"
Padding="{TemplateBinding Padding}">
<VisualStateManager.VisualStateGroups>
<!-- Група CommonStates: взаємовиключні стани Normal/MouseOver/Pressed -->
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/> <!-- Стан за замовчуванням: нічого не анімується -->
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation
Storyboard.TargetName="RootBorder"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#3730A3" Duration="0:0:0.15"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ColorAnimation
Storyboard.TargetName="RootBorder"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#312E81" Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="RootBorder"
Storyboard.TargetProperty="Opacity"
To="0.5" Duration="0:0:0.1"/>
</Storyboard>
</VisualState>
<!-- Переходи між станами: плавний вхід до MouseOver -->
<VisualStateGroup.Transitions>
<VisualTransition To="MouseOver" GeneratedDuration="0:0:0.15"/>
<VisualTransition To="Normal" GeneratedDuration="0:0:0.2"/>
<VisualTransition To="Pressed" GeneratedDuration="0:0:0.05"/>
</VisualStateGroup.Transitions>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
У стандартних контролах WPF VSM викликається внутрішньо (клас Button сам викликає GoToState). Для кастомних контролів потрібно викликати вручну:
public class MyCustomButton : Button
{
protected override void OnMouseEnter(MouseEventArgs e)
{
base.OnMouseEnter(e);
VisualStateManager.GoToState(this, "MouseOver", useTransitions: true);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
VisualStateManager.GoToState(this, "Normal", useTransitions: true);
}
}
| Аспект | Triggers | Visual State Manager |
|---|---|---|
| Простота | Вищий поріг входу (менше коду) | Більше XAML, але більш явна структура |
| Анімація | EventTrigger + Storyboard (складніше) | VisualState.Storyboard + VisualTransition (простіше) |
| Взаємовиключність | Ручне управління пріоритетом | Гарантована: лише один стан в групі |
| Масштабованість | Складно для багатьох станів | Легко — кожен стан незалежний |
| Де застосовується | Style, DataTemplate, ControlTemplate | Тільки ControlTemplate |
| Аналог у Avalonia | Немає прямого аналогу | Псевдокласи у ControlTheme (^:pointerover) |
| Рекомендація | Для простих умов у Style/DataTemplate | Для ControlTemplate із анімаціями |
DataTrigger та Trigger у Style.Triggers є достатніми і значно лаконічнішими. VSM варто використовувати коли: (1) потрібні плавні анімації між станами, (2) стан контролу змінюється з C#, (3) ви пишете повноцінний кастомний контрол, сумісний із темами.Реалізуємо Toggle-кнопку — ToggleButton, що при натисканні змінює свій стан IsChecked між True і False. Через Trigger ми змінимо і текст, і колір залежно від стану. Це класичний патерн для UI типу «Підписатись / Відписатись», «Увімкнути / Вимкнути».
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="16">
<StackPanel.Resources>
<Style TargetType="ToggleButton">
<Setter Property="Padding" Value="16,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<!-- Базове значення: кнопка не натиснута = "вимкнено" -->
<Setter Property="Background" Value="#6B7280"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Content" Value="○ Вимкнено"/>
<Style.Triggers>
<!-- Коли IsChecked = True: зелений, текст "Увімкнено" -->
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#10B981"/>
<Setter Property="Content" Value="● Увімкнено"/>
</Trigger>
<!-- Hover (базовий стан) -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="False"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#4B5563"/>
</MultiTrigger>
<!-- Hover (увімкнений стан) -->
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsChecked" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" Value="#059669"/>
</MultiTrigger>
</Style.Triggers>
</Style>
<!-- Окремий стиль для Subscribe/Unsubscribe кнопки -->
<Style x:Key="SubscribeToggle" TargetType="ToggleButton">
<Setter Property="Padding" Value="20,10"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Background" Value="#4F46E5"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Content" Value="🔔 Підписатись"/>
<Style.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#374151"/>
<Setter Property="Content" Value="✓ Підписаний"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="0.9"/>
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<TextBlock Text="Сповіщення:" FontSize="12" Foreground="#6B7280"/>
<ToggleButton/>
<TextBlock Text="Канал:" FontSize="12" Foreground="#6B7280" Margin="0,8,0,0"/>
<ToggleButton Style="{StaticResource SubscribeToggle}"
Command="{Binding ShowMessageCommand}"
CommandParameter="Статус підписки змінено!"/>
</StackPanel>
Зверніть на Content у Setter всередині Trigger: ця можливість є важливою і часто недооціненою. Trigger може змінювати не лише кольори та стани — він може повністю замінити вміст (Content) контролу. Для ToggleButton з двома станами це надзвичайно зручно: один контрол, один стиль, два потоки відображення — без жодного C#.
Ціль: Написати перший Property Trigger у Style.Triggers.
Завдання: Картка продукту зі зміною тіні при наведенні.
Border із CornerRadius="12", Padding="20", Background="White".Style.Triggers з Trigger Property="IsMouseOver" Value="True".Background="#F5F3FF", Cursor="Hand".Перевірка: наведіть на картку — фон змінюється. Відведіть — автоматично повертається до White.
Ціль: DataTrigger реагує на властивість ViewModel.
Завдання: Форма входу з динамічним статусним банером.
IsLoading (bool), HasError (bool), ErrorMessage (string).Border банер:
Visibility="Collapsed".DataTrigger Binding="{Binding IsLoading}" Value="True" → Visibility="Visible", Background="#EEF2FF", TextBlock Text="Завантаження...".DataTrigger Binding="{Binding HasError}" Value="True" → Visibility="Visible", Background="#FEF2F2", TextBlock.Text="{Binding ErrorMessage}".HasError = true.IsLoading = true на 2 секунди.Перевірка: банер з'являється і зникає реактивно при зміні HasError/IsLoading без жодної логіки у code-behind.
Ціль: MultiTrigger + DataTrigger + ToggleButton у комплексному сценарії.
Завдання: Панель «Режим роботи» із чотирма ToggleButton-ами (Radio-style — лише один активний).
ToggleButton з Trigger на IsChecked=True змінює: Background, Foreground, Content (іконка + текст).MultiTrigger (IsChecked=True AND IsMouseOver=True) → ще одне перевизначення (darker hover на активній).ActiveMode — рядок. DataTrigger на окремому TextBlock показує опис поточного режиму.IsChecked у інших трьох (через CommandParameter + ViewModel).Ключова перевірка: жодної логіки стилів у C# — всі зміни кольорів/тексту через Trigger, тільки бізнес-логіка (яка кнопка активна) у ViewModel.
Triggers завершують тріаду механізмів стилізації WPF: Style (статичні значення) → Trigger (умовні значення) → ControlTemplate (повна заміна вигляду).
Property Trigger (<Trigger Property="..." Value="...">) — слідкує за DependencyProperty. При збігу — застосовує Setter-и. При розбіжності — автоматично відкочує. Порядок важливий: специфічніші умови після загальніших.
DataTrigger (<DataTrigger Binding="{Binding ...}" Value="...">) — умова на будь-яке Binding-значення. Ключовий інструмент MVVM-стилізації. Без жодного C# — UI реагує на властивості ViewModel.
MultiTrigger та MultiDataTrigger — AND-комбінація умов. Setter-и спрацьовують лише при виконанні всіх умов одночасно.
EventTrigger — реагує на RoutedEvent, запускає Storyboard. Немає автоматичного відкочення. Повний розгляд — у Блоці 11 (Анімації).
Visual State Manager (VSM) — структурована система явних іменованих станів із Storyboard-анімаціями та VisualTransition. Гарантує взаємовиключність станів у групі. Кращий вибір для анімованих кастомних контролів. Реалізується через VisualStateGroups у ControlTemplate.
Triggers vs VSM: Triggers — простіші, менше коду для базових сценаріїв. VSM — масштабованіший, із вбудованою підтримкою анімацій між станами.
Avalonia-аналог: ControlTheme із CSS Pseudo-class Selectors (^:pointerover, ^:pressed) — замінює і Trigger, і базовий VSM одночасно, у значно лаконічнішому синтаксисі.
Наступна стаття — Data Binding у деталях: OneWay, TwoWay, OneWayToSource, OneTime, Converter, StringFormat, UpdateSourceTrigger, FallbackValue. Data Binding — серцевина MVVM, і без глибокого розуміння цих механізмів неможливо будувати складні реактивні інтерфейси.
Control Themes в Avalonia — нова ера стилізації
ControlTheme в Avalonia 11+ — CSS-like селектори замість Triggers, декларативна система стилізування контролів. Порівняння з WPF ControlTemplate та практичний портинг кастомної кнопки.
Pseudo-classes в Avalonia — замість WPF Triggers
Avalonia замінила WPF Triggers на CSS pseudo-classes. Вбудовані :pointerover, :focus, :pressed, :checked, кастомні PseudoClasses.Set(), data-driven стилізація через compiled bindings — повний посібник.