Avalonia Ui

Базові Концепції

Глибоке занурення в XAML, дерева візуалізації, систему властивостей та data binding в Avalonia UI

Базові Концепції Avalonia UI

Вступ: Чотири Стовпи Avalonia

Уявіть, що ви будуєте будинок. Вам потрібна мова для опису архітектури (креслення), структура для організації кімнат, система комунікацій (електрика, водопровід) та механізм синхронізації між кімнатами.

В Avalonia UI ці чотири фундаментальні компоненти є:

  1. XAML — декларативна мова для опису інтерфейсу
  2. Trees (Logical Tree та Visual Tree) — структура організації елементів
  3. Properties System — система властивостей для комунікації та стилізації
  4. Data Binding — механізм синхронізації між UI та даними

Без розуміння цих концепцій ви будете копіювати код, не розуміючи його. З цим розумінням — створювати складні UI з впевненістю.

Передумови

Перед вивченням цього матеріалу ви маєте:

  • ✅ Знати основи C# (класи, властивості, події)
  • ✅ Завершити розділ Вступ та Налаштування
  • ✅ Мати встановлене середовище розробки з Avalonia Templates

Цілі Розділу

Після завершення ви зможете:

  1. ✅ Писати XAML розмітку з повним розумінням синтаксису
  2. ✅ Пояснити різницю між Logical Tree та Visual Tree
  3. ✅ Обрати правильний тип властивості (Styled/Attached/Direct)
  4. ✅ Створювати reactive UI через Data Binding
  5. ✅ Використовувати Compiled Bindings для максимальної продуктивності

1. XAML Basics: Мова Опису Інтерфейсу

Проблема: Імперативний UI Код

Спочатку — навіщо XAML? Розгляньмо проблему.

Імперативний підхід (C# код):

ImperativeUI.cs
public class MainWindow : Window
{
    public MainWindow()
    {
        var grid = new Grid();
        grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
        grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
        
        var header = new TextBlock 
        { 
            Text = "Заголовок",
            FontSize = 24,
            FontWeight = FontWeight.Bold,
            Margin = new Thickness(10)
        };
        Grid.SetRow(header, 0);
        grid.Children.Add(header);
        
        var content = new TextBox 
        { 
            Margin = new Thickness(10) 
        };
        Grid.SetRow(content, 1);
        grid.Children.Add(content);
        
        Content = grid;
    }
}

Проблеми:

  • ❌ Багато коду для простого UI
  • ❌ Важко уявити візуальну структуру
  • ❌ Змішування UI і логіки
  • ❌ Немає дизайнерів/превью
  • ❌ Складно підтримувати

Декларативний підхід (XAML):

DeclarativeUI.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid RowDefinitions="Auto,*">
        <TextBlock Text="Заголовок" 
                   FontSize="24" 
                   FontWeight="Bold" 
                   Margin="10" />
        <TextBox Grid.Row="1" Margin="10" />
    </Grid>
</Window>

Переваги:

  • ✅ 8 рядків замість 30
  • ✅ Структура очевидна з першого погляду
  • ✅ Розділення UI та логіки
  • ✅ Live Preview в IDE
  • ✅ Легко читати та редагувати
Аналогія: XAML відноситься до C# як HTML до JavaScript. Можна створити весь сайт через document.createElement(), але HTML набагато зручніший для розмітки.

Що Таке XAML?

XAML (eXtensible Application Markup Language, вимовляється "zammel") — це XML-based декларативна мова розмітки для опису об'єктів .NET.

Ключові факти:

АспектОпис
ФорматXML (теги, атрибути, вкладеність)
КомпіляціяПеретворюється в C# код під час build
ЕквівалентністьВсе, що можна написати в XAML, можна написати в C# (і навпаки)
Розширення.axaml в Avalonia (.xaml в WPF/MAUI)
ІнтеграціяCode-behind файл .axaml.cs для логіки

Фундаментальний Синтаксис

Елементи як Об'єкти

Кожен XML елемент створює екземпляр .NET класу:

<Button Content="Натисни мене" />

Атрибути як Властивості

XML атрибути встановлюють властивості об'єктів:

<TextBlock Text="Hello" 
           FontSize="16" 
           Foreground="Blue" />
Type Conversion: XAML автоматично конвертує рядки в відповідні типи. "16" стає double, "Blue" стає IBrush через TypeConverter.

Property Element Syntax (Складні Властивості)

Коли значення не можна виразити простим рядком:

PropertyElement.axaml
<Button>
    <!-- Property Element Syntax для Content -->
    <Button.Content>
        <StackPanel>
            <Image Source="/Assets/icon.png" />
            <TextBlock Text="Іконка + Текст" />
        </StackPanel>
    </Button.Content>
</Button>

Правило: <ClassName.PropertyName> використовується для складних значень.

Вкладеність та Content Property

Багато контролів мають Content Property — властивість за замовчуванням для вкладеного контенту:

<Button>
    <TextBlock Text="Контент" />
</Button>

Для Button Content Property це Content, для PanelChildren.

Namespaces в XAML

XML namespaces визначають, звідки беруться класи:

Namespaces.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:MyApp.ViewModels"
        xmlns:local="clr-namespace:MyApp.Views">
    
    <!-- xmlns - default namespace (Avalonia контроли) -->
    <TextBlock Text="Це Avalonia.Controls.TextBlock" />
    
    <!-- xmlns:vm - ViewModels з нашого проєкту -->
    <vm:MainViewModel />
    
    <!-- xmlns:local - Наші кастомні контроли -->
    <local:CustomUserControl />
    
</Window>

Розбір namespaces:

ДеклараціяЗначення
xmlns="https://github.com/avaloniaui"Default namespace — всі Avalonia контроли (Button, TextBlock, тощо)
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"XAML runtime features — директиви як x:Name, x:Class
xmlns:vm="using:MyApp.ViewModels"CLR namespace mapping — доступ до класів з C# namespace
Best Practice: Використовуйте короткі префікси (vm, local, m) для кращої читабельності.

x:Name vs Name: Критична Різниця

Один з найбільш заплутаних моментів для новачків.

Name Property (Avalonia-specific):

NameProperty.axaml
<TextBox Name="UserNameBox" />

x:Name Directive (XAML Directive):

XNameDirective.axaml
<TextBox x:Name="UserNameBox" />
Важливо: В Avalonia ці дві концепції різні, хоча часто працюють однаково.

Порівняння:

КритерійName Propertyx:Name Directive
ПриродаЗвичайна властивість класуXAML compiler directive
Доступ в C#Через FindControl<T>("Name")Прямий доступ як поле класу
Доступ в XAMLЧерез {Binding #Name}Через {Binding #Name}
Compiled BindingsНе працюєПрацює ✅
ВидимістьЛокальна (в межах Name Scope)Глобальна (в межах класу)

Практичний приклад:

<Window x:Class="MyApp.MainWindow">
    
    <!-- x:Name створює поле в класі -->
    <TextBox x:Name="InputBox" />
    
    <!-- Name використовується для XAML bindings -->
    <TextBlock Text="{Binding #InputBox.Text}" />
    
</Window>
Рекомендація: Завжди використовуйте x:Name для елементів, до яких потрібен доступ з C# або Compiled Bindings. Name резервуйте для випадків, коли потрібна лише локальна XAML видимість.

Як XAML Компілюється

Цікавий момент: XAML — це не інтерпретована мова. Вона компілюється в C# код!

Loading diagram...
graph LR
    XAML[MainWindow.axaml\u003cbr/\u003eXAML файл]
    Compiler[Avalonia\u003cbr/\u003eXAML Compiler]
    Generated[MainWindow.g.cs\u003cbr/\u003eGenerated код]
    Final[Скомпільований\u003cbr/\u003eAssembly]
    
    XAML -->|Build Time| Compiler
    Compiler -->|Генерує| Generated
    Generated -->|C# Compiler| Final
    
    style XAML fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Compiler fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Generated fill:#64748b,stroke:#334155,color:#ffffff
    style Final fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Приклад генерованого коду:

<Window x:Class="MyApp.MainWindow"
        x:Name="RootWindow">
    <TextBox x:Name="InputBox" />
</Window>
Практичне значення: Коли ви використовуєте x:Name, компілятор створює поле в класі. Тому ви можете звертатись до InputBox безпосередньо в code-behind.

Markup Extensions

Markup Extensions — це спеціальний синтаксис {ExtensionName} для динамічних значень:

MarkupExtensions.axaml
<!-- Binding до властивості -->
<TextBlock Text="{Binding UserName}" />

<!-- StaticResource (стилі, кольори) -->
<Button Background="{StaticResource PrimaryBrush}" />

<!-- DynamicResource (може змінюватись в runtime) -->
<TextBlock Foreground="{DynamicResource ThemeTextColor}" />

<!-- Compiled Binding (type-safe, швидші) -->
<TextBlock Text="{CompiledBinding User.FullName}" />

<!-- x:Static (статичні поля класів) -->
<Rectangle Fill="{x:Static Brushes.Red}" />

Вони вирішують проблему: як передати не літеральне значення, а посилання на ресурс, binding чи статичний член.


2. Trees: Logical Tree vs Visual Tree

Проблема: Чому Одного Дерева Недостатньо?

Коли ви пишете XAML, ви описуєте Logical Tree — логічну структуру вашого UI:

<StackPanel>
    <Button Content="Click" />
    <TextBox />
</StackPanel>

Але! Коли Avalonia рендерить цей UI, кожен контрол складається з багатьох внутрішніх елементів:

Button
├── Border (фон та рамка)
│   └── ContentPresenter (для Content властивості)
│       └── TextBlock (текст "Click")
└── ...інші internal елементи для hover/pressed states

Якби у нас було лише одне дерево, ми не змогли б:

  • ❌ Відокремити вашу логіку від внутрішніх деталей контролів
  • ❌ Застосовувати стилі та теми до внутрішніх частин
  • ❌ Навігувати по логічній структурі без знання внутрішньої реалізації кожного контролу

Рішення: Avalonia має дві паралельні ієрархії.

Logical Tree: Що Ви Написали

Logical Tree — це структура, яку ви визначили в XAML або C#.

LogicalStructure.axaml
<Window>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        
        <TextBlock Text="Header" Grid.Row="0" />
        <Button Content="Action" Grid.Row="1" />
    </Grid>
</Window>

Logical Tree:

Window
└── Grid
    ├── TextBlock
    └── Button

Для чого використовується:

  • Navigation — пошук батьківських/дочірніх елементів
  • Data Binding — inheritance context (DataContext)
  • Events — routing (bubbling/tunneling)
  • Resources — пошук ресурсів вгору по дереву
Практичне правило: Якщо ви створили елемент в XAML, він в Logical Tree.

Visual Tree: Що Насправді Рендериться

Visual Tree включає всі візуальні елементи, включно з внутрішніми частинами контролів.

Visual Tree для того ж UI:

Window
└── Border (Window template)
    └── ContentPresenter (Window.Content)
        └── Grid
            ├── TextBlock
            │   └── TextPresenter (internal renderer)
            └── Button
                └── ContentPresenter (Button.Content)
                    └── TextBlock ("Action" text)
                        └── TextPresenter

Для чого використовується:

  • Rendering — що малювати на екрані
  • Styling — застосування Control Templates
  • Hit Testing — визначення, на який елемент клікнули
  • Animations — анімації застосовуються до visual elements
Loading diagram...
graph TD
    L1[Logical Tree]
    L2[Window]
    L3[StackPanel]
    L4[Button]
    L5[TextBox]
    
    V1[Visual Tree]
    V2[Window]
    V3[Border]
    V4[ContentPresenter]
    V5[StackPanel]
    V6[Button]
    V7[Border]
    V8[ContentPresenter]
    V9[TextBlock]
    V10[TextBox]
    V11[Border]
    V12[TextPresenter]
    
    L1 -.->|Ваш код| L2
    L2 --> L3
    L3 --> L4
    L3 --> L5
    
    V1 -.->|Runtime| V2
    V2 --> V3
    V3 --> V4
    V4 --> V5
    V5 --> V6
    V6 --> V7
    V7 --> V8
    V8 --> V9
    V5 --> V10
    V10 --> V11
    V11 --> V12
    
    style L1 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style L2 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style L3 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style L4 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style L5 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    
    style V1 fill:#f59e0b,stroke:#b45309,color:#ffffff
    style V2 fill:#64748b,stroke:#334155,color:#ffffff
    style V3 fill:#64748b,stroke:#334155,color:#ffffff
    style V4 fill:#64748b,stroke:#334155,color:#ffffff
    style V5 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style V6 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style V10 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

VisualTreeHelper: Робота з Visual Tree

Avalonia надає API для навігації по Visual Tree:

VisualTreeNavigation.cs
using Avalonia.VisualTree;

public class TreeExamples
{
    public void NavigateVisualTree(Control control)
    {
        // Отримати візуального батьківського елемента
        var parent = control.GetVisualParent();
        
        // Отримати всіх візуальних дітей
        var children = control.GetVisualChildren();
        
        // Знайти батьківський елемент певного типу
        var window = control.FindAncestorOfType<Window>();
        
        // Знайти всіх нащадків певного типу
        var allButtons = control.GetVisualDescendants()
            .OfType<Button>();
        
        // Перевірка, чи елемент є нащадком іншого
        bool isChild = control.IsVisualAncestorOf(otherControl);
    }
    
    public void FindControlInTree(Panel panel)
    {
        // Пошук по Logical Tree (швидше, для ваших контролів)
        var logicalButton = panel.LogicalChildren
            .OfType<Button>()
            .FirstOrDefault();
        
        // Пошук по Visual Tree (для внутрішніх елементів)
        var visualTextBlock = panel.GetVisualDescendants()
            .OfType<TextBlock>()
            .FirstOrDefault();
    }
}
Performance: Навігація по Visual Tree дорожча, ніж по Logical Tree. Використовуйте Logical Tree, коли це можливо.

Практичний Приклад: Пошук Елементів

Задача: Знайти всі TextBox в Window, які мають помилки валідації.

public IEnumerable<TextBox> FindInvalidTextBoxes(Window window)
{
    return window.GetLogicalDescendants()
        .OfType<TextBox>()
        .Where(tb => DataValidationErrors.GetHasErrors(tb));
}

// Швидко, оскільки TextBox в вашому XAML (Logical Tree)

Коли Використовувати Кожне Дерево

СценарійЯке деревоAPI
Пошук контролів, які ви створилиLogical TreeGetLogicalChildren(), LogicalChildren
Data Context inheritanceLogical TreeАвтоматично через Logical Tree
Resources lookupLogical TreeTryFindResource()
Styling внутрішніх частин контролівVisual TreeGetVisualChildren(), GetVisualDescendants()
Hit testing (визначення кліку)Visual TreeAvalonia робить автоматично
Animations на template elementsVisual TreeVisualTreeHelper
Золоте правило: Починайте з Logical Tree. Переходьте до Visual Tree тільки коли потрібно працювати з Control Templates чи внутрішньою структурою контролів.

3. Properties System: Три Типи Властивостей

Проблема: Чому Звичайні C# Properties Недостатні?

У звичайному C# класі властивість виглядає так:

public class SimpleClass
{
    private string _name;
    public string Name
    {
        get => _name;
        set => _name = value;
    }
}

Що не так для UI framework?

Немає підтримки styling — як застосувати тему? ❌ Немає data binding — як auto-sync з ViewModel? ❌ Немає change notification — як оновити UI при зміні? ❌ Немає inheritance — як передати FontSize від Window до всіх дітей? ❌ Немає animation — як анімувати значення? ❌ Пам'ять: якщо у вас 1000 контролів з 50 властивостями, це 50,000 полів (багато неви використовуються)

Рішення: Avalonia використовує Styled Properties — спеціальну систему властивостей з усіма цими можливостями.

Styled Properties: Основа Avalonia

Styled Property (в WPF називається Dependency Property) — це властивість з розширеними можливостями.

Декларація:

StyledPropertyExample.cs
public class MyControl : Control
{
    // 1. Реєстрація Styled Property
    public static readonly StyledProperty<string> TitleProperty =
        AvaloniaProperty.Register<MyControl, string>(
            nameof(Title),
            defaultValue: "Default Title");
    
    // 2. CLR Wrapper (зручний доступ)
    public string Title
    {
        get => GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
}

Використання:

Usage.axaml
<!-- Property Syntax -->
<local:MyControl Title="Hello Avalonia" />

<!-- Binding -->
<local:MyControl Title="{Binding CurrentTitle}" />

<!-- Style -->
<Style Selector="local|MyControl">
    <Setter Property="Title" Value="Styled Value" />
</Style>
Важливо:StyledProperty<T> — це статичне поле, яке зберігає метадані, але не значення. Реальні значення зберігаються в internal dictionary всередині кожного контролу.

Три Типи Properties

Avalonia має три типи властивостей для різних сценаріїв:

Loading diagram...
graph TD
    Props[Avalonia Properties]
    Styled[Styled Properties\u003cbr/\u003eStyled, Inheritable, Bindable]
    Attached[Attached Properties\u003cbr/\u003eLayout, Metadata]
    Direct[Direct Properties\u003cbr/\u003ePerformance-critical]
    
    Props --> Styled
    Props --> Attached
    Props --> Direct
    
    Styled -.->|Examples| StyledEx["Background\u003cbr/\u003eFontSize (inheritable)\u003cbr/\u003eDataContext"]
    Attached -.->|Examples| AttachedEx["Grid.Row\u003cbr/\u003eGrid.Column\u003cbr/\u003eDockPanel.Dock"]
    Direct -.->|Examples| DirectEx["Bounds\u003cbr/\u003eIsPointerOver\u003cbr/\u003eVisualChildren"]
    
    style Props fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Styled fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Attached fill:#10b981,stroke:#059669,color:#ffffff
    style Direct fill:#64748b,stroke:#334155,color:#ffffff

1. Styled Properties (Найпоширеніші)

Призначення: Властивості, які можуть бути стилізовані, анімовані, bindable.

Характеристики:

  • ✅ Підтримка styling через <Style> або themes
  • ✅ Data binding (OneWay, TwoWay)
  • ✅ Value inheritance (наприклад, FontSize від батьківського до дочірнього)
  • ✅ Animations
  • ✅ Default values та value coercion

Приклад з інфраструктурою:

FullStyledProperty.cs
public class ProgressControl : Control
{
    // Реєстрація з додатковими опціями
    public static readonly StyledProperty<double> ProgressProperty =
        AvaloniaProperty.Register<ProgressControl, double>(
            nameof(Progress),
            defaultValue: 0.0,
            coerce: CoerceProgress,  // Обмеження значення
            validate: ValidateProgress); // Валідація
    
    public double Progress
    {
        get => GetValue(ProgressProperty);
        set => SetValue(ProgressProperty, value);
    }
    
    // Coercion: обмежуємо 0-100
    private static double CoerceProgress(AvaloniaObject obj, double value)
    {
        return Math.Clamp(value, 0, 100);
    }
    
    // Validation
    private static bool ValidateProgress(double value)
    {
        return !double.IsNaN(value) && !double.IsInfinity(value);
    }
    
    protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
    {
        base.OnPropertyChanged(change);
        
        if (change.Property == ProgressProperty)
        {
            // Реакція на зміну Progress
            InvalidateVisual(); // Перемалювати контрол
        }
    }
}

2. Attached Properties (Метадані для Інших Контролів)

Проблема: Як Grid каже Button, в якому рядку він має бути? Button не знає про Grid!

Рішення: Attached Properties — властивості, які один контрол "прикріплює" до іншого.

Декларація:

AttachedPropertyExample.cs
public class Grid : Panel
{
    // Attached Property для Row
    public static readonly AttachedProperty<int> RowProperty =
        AvaloniaProperty.RegisterAttached<Grid, Control, int>(
            "Row",
            defaultValue: 0);
    
    // Getter (статичний метод)
    public static int GetRow(Control control)
    {
        return control.GetValue(RowProperty);
    }
    
    // Setter (статичний метод)
    public static void SetRow(Control control, int value)
    {
        control.SetValue(RowProperty, value);
    }
}

Використання в XAML:

AttachedPropertyUsage.axaml
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    
    <!-- Button не має властивості Row, але Grid прикріплює її -->
    <Button Grid.Row="0" Content="Row 0" />
    <TextBox Grid.Row="1" />
</Grid>

Використання в C#:

AttachedPropertyCode.cs
var button = new Button();

// Встановлення через статичний метод
Grid.SetRow(button, 1);

// Читання
int row = Grid.GetRow(button);

// Альтернативно (lower-level API)
button.SetValue(Grid.RowProperty, 1);

Поширені Attached Properties:

КласAttached PropertiesПризначення
GridRow, Column, RowSpan, ColumnSpanПозиціонування в сітці
DockPanelDock (Top/Bottom/Left/Right)Докування до сторін
CanvasLeft, Top, Right, BottomАбсолютне позиціонування
ToolTipTip, ShowDelayTooltip для будь-якого контролу

3. Direct Properties (Для Продуктивності)

Проблема: Styled Properties зберігають значення в dictionary — це overhead для часто змінюваних властивостей.

Рішення: Direct Properties — працюють як стандартні C# properties, але з підтримкою binding.

Характеристики:

  • Швидкість — прямий доступ до backing field (без dictionary lookup)
  • ✅ Data Binding підтримка
  • ❌ Немає styling
  • ❌ Немає animation
  • ❌ Немає value inheritance

Використання:

DirectPropertyExample.cs
public class FastControl : Control
{
    // Backing field
    private bool _isPointerOver;
    
    // Direct Property Registration
    public static readonly DirectProperty<FastControl, bool> IsPointerOverProperty =
        AvaloniaProperty.RegisterDirect<FastControl, bool>(
            nameof(IsPointerOver),
            o => o.IsPointerOver);  // Getter
    
    // Property з manual notification
    public bool IsPointerOver
    {
        get => _isPointerOver;
        private set => SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value);
    }
}
Коли використовувати Direct Properties:
  • Властивості, які змінюються дуже часто (наприклад, mouse position, selection state)
  • Властивості, які ніколи не стилізуються
  • Read-only властивості, обчислювані з інших даних

Порівняльна Таблиця

КритерійStyled PropertyAttached PropertyDirect Property
Styling✅ Так✅ Так❌ Ні
Data Binding✅ Так✅ Так✅ Так
Animation✅ Так✅ Так❌ Ні
Value Inheritance✅ Так❌ Ні❌ Ні
ПродуктивністьСередняСередняВисока ✅
Пам'ятьDictionaryDictionaryField
Прикріплення до інших❌ Ні✅ Так (це їх мета)❌ Ні
Типовий Use CaseUI propertiesLayout metadataState, frequently changed

Практичний Приклад: Створення Кастомного Контролу

Давайте створимо контрол RatingControl з кількістю зірок:


4. Data Binding: Синхронізація UI та Даних

Проблема: Manual Synchronization

Уявіть застосунок без data binding:

WithoutBinding.cs
public partial class MainWindow : Window
{
    private string _userName;
    
    public MainWindow()
    {
        InitializeComponent();
        
        // Оновлення UI при зміні даних (MANUAL!)
        NameTextBox.PropertyChanged += (s, e) =>
        {
            if (e.Property == TextBox.TextProperty)
            {
                _userName = NameTextBox.Text;
                GreetingLabel.Text = $"Привіт, {_userName}!";
            }
        };
    }
    
    public void LoadUser()
    {
        _userName = "Іван";
        NameTextBox.Text = _userName; // MANUAL sync!
        GreetingLabel.Text = $"Привіт, {_userName}!"; // MANUAL sync!
    }
}

Проблеми:

  • ❌ Потрібно вручну синхронізувати всі залежні UI елементи
  • ❌ Код швидко стає спагетті при збільшенні логіки
  • ❌ Тестування складне (UI та логіка змішані)
  • ❌ Дублювання коду

Рішення: Data Binding автоматично синхронізує UI та дані.

Що Таке Data Binding?

Data Binding — це механізм автоматичної синхронізації між:

  • Source — джерело даних (ViewModel, Property, Resource)
  • Target — UI властивість (Text, IsEnabled, ItemsSource)
Loading diagram...
graph LR
    Source[Source\u003cbr/\u003eViewModel.UserName]
    Target[Target\u003cbr/\u003eTextBox.Text]
    Binding[Binding Engine]
    
    Source <-.->|Binding\u003cbr/\u003eMode| Binding
    Binding <-.->|Updates| Target
    
    style Source fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Target fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Binding fill:#64748b,stroke:#334155,color:#ffffff

Приклад:

<TextBox Text="{Binding UserName}" />

Що відбувається:

  1. При зміні UserName в ViewModel → викликається PropertyChanged
  2. Binding Engine слухає цю подію
  3. Автоматично оновлює TextBox.Text
  4. При редагуванні TextBox → автоматично оновлює UserName (якщо TwoWay binding)

Binding Modes: Напрямок Синхронізації

Avalonia підтримує кілька режимів binding:

ModeНапрямокТиповий Use Case
OneWaySource → TargetВідображення даних (Labels, Read-only)
TwoWaySource ⇄ TargetРедагування (TextBox, Checkbox)
OneTimeSource → Target (один раз)Статичні дані, що не змінюються
OneWayToSourceTarget → SourceРідко (зворотній зв'язок)

Схема:

Loading diagram...
graph TD
    Source[Source Property]
    Target[Target Property]
    
    OneWay[OneWay]
    TwoWay[TwoWay]
    OneTime[OneTime]
    OneWayToSource[OneWayToSource]
    
    Source -->|Update| OneWay
    OneWay -->|Display| Target
    
    Source <-->|Sync| TwoWay
    TwoWay <-->|Sync| Target
    
    Source -->|Once| OneTime
    OneTime -.->|No updates| Target
    
    Target -->|Update| OneWayToSource
    OneWayToSource -->|Set| Source
    
    style Source fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Target fill:#f59e0b,stroke:#b45309,color:#ffffff

Приклади:

BindingModes.axaml
<!-- OneWay - дані тільки читаються -->
<TextBlock Text="{Binding UserName, Mode=OneWay}" />

<!-- TwoWay - користувач може редагувати -->
<TextBox Text="{Binding UserName, Mode=TwoWay}" />

<!-- OneTime - встановлюється при завантаженні, не оновлюється -->
<TextBlock Text="{Binding AppTitle, Mode=OneTime}" />

<!-- Default Mode (залежить від Property) -->
<TextBox Text="{Binding UserName}" />
<!-- TextBox.TextProperty має default mode = TwoWay -->

<TextBlock Text="{Binding UserName}" />
<!-- TextBlock.TextProperty має default mode = OneWay -->
Оптимізація: Використовуйте OneTime для статичних даних (titles, labels), щоб уникнути overhead binding у runtime.

DataContext: Джерело Даних

DataContext — це властивість, яка зберігає джерело даних для bindings.

Ключові правила:

  1. DataContext успадковується вниз по Logical Tree
  2. Дочірні елементи використовують DataContext батьківського
  3. Можна перевизначити DataContext для конкретного елемента

Приклад:

DataContextInheritance.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:vm="using:MyApp.ViewModels">
    
    <!-- Window DataContext - корінь -->
    <Window.DataContext>
        <vm:MainViewModel />
    </Window.DataContext>
    
    <!-- Binding до MainViewModel.Title -->
    <TextBlock Text="{Binding Title}" />
    
    <StackPanel>
        <!-- Успадковує DataContext від Window -->
        <TextBlock Text="{Binding UserName}" />
        
        <!-- Nested ViewModel -->
        <UserControl DataContext="{Binding UserDetailsViewModel}">
            <!-- Тепер bindings до UserDetailsViewModel -->
            <TextBlock Text="{Binding Email}" />
        </UserControl>
    </StackPanel>
    
</Window>
Loading diagram...
graph TD
    Window[Window\u003cbr/\u003eDataContext = MainViewModel]
    StackPanel[StackPanel\u003cbr/\u003eDataContext = inherited]
    TextBlock1[TextBlock\u003cbr/\u003eBinding UserName]
    UserControl[UserControl\u003cbr/\u003eDataContext = UserDetailsViewModel]
    TextBlock2[TextBlock\u003cbr/\u003eBinding Email]
    
    Window --> StackPanel
    StackPanel --> TextBlock1
    StackPanel --> UserControl
    UserControl --> TextBlock2
    
    Window -.->|inherits| TextBlock1
    UserControl -.->|new context| TextBlock2
    
    style Window fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style UserControl fill:#f59e0b,stroke:#b45309,color:#ffffff

Binding to Commands: MVVM Pattern

В MVVM логіка кнопок реалізується через Commands замість event handlers.

Традиційний підхід (Code-Behind):

<Button Content="Save" Click="SaveButton_Click" />
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
    // Logic here - НЕ testable!
}

MVVM підхід (Command Binding):

<Button Content="Save" 
        Command="{Binding SaveCommand}"
        CommandParameter="{Binding CurrentUser}" />

Переваги Commands:

  • Testability — можна тестувати логіку без UI
  • Separation — логіка відокремлена від View
  • CanExecute — автоматичне Enable/Disable кнопки
  • Reusability — одна команда для кількох кнопок

Compiled Bindings: Продуктивність + Type Safety

Проблема Reflection Bindings:

<TextBlock Text="{Binding UserName}" />
  • ❌ Використовує Reflection в runtime (повільно)
  • ❌ Немає compile-time перевірки типів
  • ❌ Помилки в property names виявляються тільки в runtime

Рішення: Compiled Bindings

CompiledBindings.axaml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:vm="using:MyApp.ViewModels"
        x:Class="MyApp.MainWindow"
        x:DataType="vm:MainViewModel">
    
    <!-- Compiled Binding - type-safe! -->
    <TextBlock Text="{CompiledBinding UserName}" />
    
    <!-- Nested properties -->
    <TextBlock Text="{CompiledBinding User.FullName}" />
    
    <!-- Collections -->
    <ListBox ItemsSource="{CompiledBinding Users}"
             SelectedItem="{CompiledBinding SelectedUser}" />
    
</Window>

Ключові відмінності:

АспектReflection BindingCompiled Binding
Синтаксис{Binding PropertyName}{CompiledBinding PropertyName} + x:DataType
ПродуктивністьПовільніше (Reflection)Швидше (Generated code)
Type Safety❌ Runtime errors✅ Compile-time errors
IntelliSense❌ Немає✅ Так
Refactoring❌ Breaks silently✅ Compiler error
Важливо:x:DataType має вказувати на тип ViewModel, а не на View!

Включення Compiled Bindings глобально:

App.axaml
<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:CompileBindings="True">
    <!-- Всі bindings в застосунку тепер compiled -->
</Application>
Best Practice: Завжди використовуйте Compiled Bindings для нових проєктів. Це швидше і безпечніше.

Повний MVVM Приклад

Створимо простий застосунок з Data Binding:

Як це працює:

Користувач вводить текст у TextBox

  • TwoWay binding автоматично оновлює MainViewModel.UserName
  • OnUserNameChanged викликає GreetCommand.NotifyCanExecuteChanged()
  • Кнопка Enable, якщо UserName не пустий

Натискання кнопки "Привітати"

  • Викликається GreetCommand (через Command Binding)
  • Greet() метод оновлює Greeting property
  • Додає message до Messages collection

UI автоматично оновлюється

  • TextBlock.Text оновлюється через OneWay binding до Greeting
  • ListBox додає новий елемент (ObservableCollection notify)


Перевірка Знань

Закріпіть отримані знання, пройшовши короткий тест:

Інструкція для проходження тесту:
  1. Перейдіть на Tally.so та створіть нову форму
  2. Додайте 7-10 питань з варіантами відповідей, що охоплюють:
    • XAML синтаксис та namespaces
    • Різницю між Logical Tree та Visual Tree
    • Типи Properties (Styled, Attached, Direct)
    • Binding Modes та їх застосування
    • Compiled vs Reflection Bindings
  3. Налаштуйте підрахунок балів (Scoring):
    • Calculated field "Score" (початкове значення: 0)
    • Для правильних відповідей: Add 10 to Score
  4. Створіть Thank You pages:
    • Score >= 70: "Вітаємо! Ви чудово засвоїли матеріал"
    • Score < 70: "Рекомендуємо повторити матеріал"
  5. Опублікуйте форму та замініть YOUR_FORM_ID на ваш Embed ID
Примітка: Якщо тест не відображається, перейдіть за прямим посиланням.

Підсумок

У цьому розділі ви вивчили чотири фундаментальні концепції Avalonia UI:

1. XAML Basics

  • ✅ XAML — декларативна мова для опису UI (XML-based)
  • ✅ Елементи = Об'єкти, Атрибути = Властивості
  • ✅ Namespaces визначають джерела класів
  • x:Name створює поля в класі для доступу з C#
  • ✅ XAML компілюється в C# код (не інтерпретується)

2. Trees (Logical vs Visual)

  • Logical Tree — ваша структура (що ви описали в XAML)
  • Visual Tree — повна структура рендерингу (включає template elements)
  • ✅ Logical Tree для навігації, Data Binding, Resources
  • ✅ Visual Tree для styling, animations, hit testing
  • VisualTreeHelper для навігації по Visual Tree

3. Properties System

  • Styled Properties — основні властивості (styling, binding, animation)
  • Attached Properties — метадані від інших контролів (Grid.Row)
  • Direct Properties — performance-critical властивості (backing field)
  • ✅ Використовуйте Styled для більшості UI properties
  • ✅ Direct для часто змінюваних/read-only властивостей

4. Data Binding

  • ✅ Автоматична синхронізація між UI та даними
  • Binding Modes: OneWay (display), TwoWay (edit), OneTime (static)
  • DataContext успадковується вниз по дереву
  • Commands замість event handlers для MVVM
  • Compiled Bindings для type safety та продуктивності

Що Далі?

Тепер, маючи розуміння базових концепцій, ви готові до:

Практика: Створіть простий застосунок "Todo List" використовуючи всі вивчені концепції:
  • XAML для UI
  • Styled Properties для кастомних контролів
  • Data Binding для синхронізації
  • Commands для дій (Add/Remove/Complete)
Це закріпить ваше розуміння!