Desktop UI

XAML: декларативний інтерфейс

Розбираємо XAML як мову: зв'язок з C#-об'єктами, синтаксис XML, Property Element Syntax, Content Property, Type Converters та x:Name.

XAML: декларативний інтерфейс

Уявіть, що вам треба описати кімнату. Можна зробити це по-різному.

Перший спосіб — процедурний: "Зайдіть у кімнату. Підійдіть до лівої стіни. Поставте стіл. Поверніться і поставте стілець. На стіл покладіть лампу." Це — послідовність дій.

Другий спосіб — декларативний: "Кімната: стіл зі стільцем і лампою зліва." Ви описуєте що повинно бути, а не як це зробити. Результат той самий, але запис набагато лаконічніший і читабельніший.

XAML — це декларативний спосіб опису інтерфейсу. Замість того, щоб писати десятки рядків C#-коду для створення кнопок, текстових полів і панелей — ви описуєте готовий результат у вигляді розмітки. Але є ключова деталь, яку потрібно зрозуміти з самого початку.

Словник теми:XAML (Extensible Application Markup Language) — XML-подібна мова розмітки для опису об'єктного графу. Property Element Syntax — спосіб задати складне значення властивості через вкладений тег. Content Property — особлива властивість класу, яка отримує значення прямо між тегами без явного зазначення імені. Type Converter — клас, що перетворює рядок у потрібний об'єкт (наприклад, "Red"Brushes.Red). Attached Property — властивість, визначена в одному класі, але застосована до об'єкта іншого класу (наприклад, Grid.Row на Button).

XAML — це не HTML. XAML — це C#

Найпоширеніша помилка при першому знайомстві з XAML — думати, що це "щось схоже на HTML". Це не так, і ця помилка спричиняє непорозуміння.

HTML — це мова розмітки документів. Вона описує сторінку: заголовки, параграфи, посилання, таблиці. Браузер читає HTML і малює сторінку.

XAML — це серіалізований C#-об'єктний граф. Кожен тег XAML — це виклик конструктора. Кожен атрибут — це присвоєння властивості. Ніякої "магії" браузера. Ніякого прощення помилок.

Подивіться на цю паралель:

<Button Content="OK" Width="100" IsDefault="True"/>

Це буквально те саме, що:

var button = new Button();
button.Content = "OK";
button.Width = 100;
button.IsDefault = true;

Ніякої різниці в результаті. XAML — це просто інший, коротший синтаксис для створення тих самих C#-об'єктів. Якщо ви завжди пам'ятатимете цей факт — XAML ніколи не буде для вас магічною чорною скринькою.


XAML як XML: суворі правила

XAML побудований на основі XML. А XML — на відміну від HTML — є суворою мовою. Браузер HTML "вибачає" незакриті теги, відсутні лапки та інші помилки. Парсер XML — ні. Одна помилка — і XAML взагалі не завантажиться.

Вам потрібно знати ці правила напам'ять.

Правило 1: Регістр має значення

XML є case-sensitive. <Button> і <button> — це різні теги. Content і content — різні атрибути.

<!-- ✅ Правильно -->
<Button Content="OK"/>

<!-- ❌ Помилка: 'button' не є відомим тегом -->
<button Content="OK"/>

<!-- ❌ Помилка: 'content' не є властивістю Button -->
<Button content="OK"/>

У WPF всі класи та властивості починаються з великої літери (PascalCase) — Button, StackPanel, HorizontalAlignment. Дотримуйтесь цього при написанні XAML.

Правило 2: Кожен тег повинен бути закритий

У HTML можна написати <br> без закриваючого тегу. В XML — ні. Кожен відкритий тег повинен мати закриваючий.

<!-- ✅ Самозакривний тег (якщо немає вмісту) -->
<Button Content="OK"/>

<!-- ✅ Явний закриваючий тег (якщо є вміст) -->
<StackPanel>
    <Button Content="OK"/>
</StackPanel>

<!-- ❌ Помилка: тег не закритий -->
<Button Content="OK">

Правило 3: Атрибути завжди в лапках

В XML кожне значення атрибута повинно бути в лапках — одинарних або подвійних. В HTML деякі значення можна писати без лапок.

<!-- ✅ Правильно -->
<Button Width="100" Content="OK"/>

<!-- ❌ Помилка: значення без лапок -->
<Button Width=100 Content=OK/>

У XAML прийнято використовувати подвійні лапки. Одинарні — лише коли значення містить подвійні лапки всередині (що трапляється рідко).

Правило 4: Вкладеність має бути правильною

Теги не можуть перекриватися. Якщо тег <A> відкрито всередині <B>, він повинен бути закритий до закриття <B>.

<!-- ✅ Правильна вкладеність -->
<StackPanel>
    <TextBlock>
        <Bold>Жирний текст</Bold>
    </TextBlock>
</StackPanel>

<!-- ❌ Помилка: перекриття тегів -->
<StackPanel>
    <TextBlock>
        <Bold>Жирний текст
    </TextBlock>
        </Bold>
</StackPanel>

Порівняння з HTML: де браузер "рятує", XAML — ні

СитуаціяHTML (браузер)XAML (WPF)
Незакритий тег✅ Виправляє сам❌ XmlException
Значення без лапок✅ Приймає❌ XmlException
Неправильний регістр тегу✅ Ігнорує❌ XamlParseException
Невідомий атрибут✅ Ігнорує❌ XamlParseException
Перекриті теги✅ Виправляє❌ XmlException

Це означає: коли у вашому XAML є синтаксична помилка — застосунок або не скомпілюється, або впаде при запуску з XamlParseException. Ніякого "а ну й ладно, спробуємо відобразити як є".

Visual Studio та Rider підсвічують XAML-помилки прямо в редакторі — червоним підкресленням. Якщо ви бачите таке підкреслення, не запускайте проєкт — спершу виправте помилку. Помилки компіляції XAML зазвичай описові і точно вказують на рядок з проблемою.

Дві форми запису властивостей

В XAML існують два синтаксично еквівалентних способи задати значення властивості. Перший — короткий і зручний там, де значення є простим рядком чи числом. Другий — розгорнутий, необхідний тоді, коли значення є складним об'єктом. Розуміння різниці між ними — одна з ключових навичок роботи з XAML.

Attribute Syntax: коротка форма

Коли значення властивості можна виразити одним рядком — використовується Attribute Syntax. Значення передається як атрибут XML прямо у відкриваючому тезі:

<Button Content="Натисни мене"
        Width="150"
        FontSize="16"
        IsEnabled="True"/>

Це зручно і читабельно. Більшість властивостей у XAML задається саме так: текст, числа, булеві значення, перерахування (HorizontalAlignment="Center").

Property Element Syntax: розгорнута форма

Але що, якщо значення властивості — це не рядок, а складний об'єкт? Наприклад, фон кнопки має бути не однорідним кольором, а градієнтом. Як записати об'єкт LinearGradientBrush як значення атрибуту? Ніяк — рядок цього не зможе передати.

Тут на допомогу приходить Property Element Syntax. Замість атрибута властивість записується як вкладений тег у форматі ТипОб'єкта.НазваВластивості:

<Button Width="150">
    <Button.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
            <GradientStop Color="#3b82f6" Offset="0"/>
            <GradientStop Color="#8b5cf6" Offset="1"/>
        </LinearGradientBrush>
    </Button.Background>
    <Button.Content>
        Натисни мене
    </Button.Content>
</Button>

Те саме в C# виглядало б так:

var button = new Button();
button.Width = 150;

var brush = new LinearGradientBrush();
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 0);
brush.GradientStops.Add(new GradientStop(Color.FromArgb(255, 59, 130, 246), 0));
brush.GradientStops.Add(new GradientStop(Color.FromArgb(255, 139, 92, 246), 1));

button.Background = brush;
button.Content = "Натисни мене";

Обидві форми дають однаковий результат. Property Element Syntax — це спосіб "помістити повноцінний об'єкт у властивість" через вкладений тег.

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Коли яку форму використовувати

Обидві форми можна змішувати в одному тезі. Загальне правило просте:

  • Attribute Syntax — для простих значень: рядки, числа, bool, перерахування, кольори у вигляді рядка ("#3b82f6").
  • Property Element Syntax — коли значення є складним об'єктом, який потребує власних атрибутів і вкладених елементів.
<!-- Змішаний підхід — цілком нормально -->
<Button Width="200"
        FontSize="14"
        Foreground="White">
    <Button.Background>
        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
            <GradientStop Color="#3b82f6" Offset="0"/>
            <GradientStop Color="#8b5cf6" Offset="1"/>
        </LinearGradientBrush>
    </Button.Background>
    Натисни мене
</Button>

Content Property: магія тексту між тегами

Погляньте на цей рядок XAML:

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

Тут є щось нелогічне. Button має властивість Content, але ми ніде не написали Content=. Просто помістили текст між тегами. І воно працює. Чому?

Атрибут [ContentProperty]

У WPF є атрибут [ContentProperty], яким клас може позначити одну зі своїх властивостей як властивість за замовчуванням для вмісту між тегами. Все що знаходиться між відкриваючим і закриваючим тегом — автоматично призначається саме цій властивості.

Для класу ContentControl (від якого успадковує Button) це виглядає так у вихідному коді WPF:

[ContentProperty(Name = "Content")]
public class ContentControl : Control
{
    public object Content { get; set; }
    // ...
}

Тому <Button>Натисни мене</Button> і <Button Content="Натисни мене"/> — абсолютно еквівалентні записи. XAML-парсер бачить текст між тегами і автоматично надсилає його у властивість Content.

Content Property різних класів

Різні класи мають різні Content Property — залежно від своєї ролі в ієрархії:

КласContent PropertyЩо може містити
ContentControl (Button, Window...)ContentОдин довільний об'єкт
Panel (StackPanel, Grid...)ChildrenКолекція UIElement
ItemsControl (ListBox, ComboBox...)ItemsКолекція об'єктів
TextBlockInlinesКолекція Inline-елементів
BorderChildОдин UIElement

Це пояснює, чому StackPanel приймає кілька дочірніх елементів між тегами — його Content Property це Children, колекція:

<!-- Всі ці елементи потрапляють у StackPanel.Children -->
<StackPanel>
    <TextBlock Text="Перший рядок"/>
    <TextBlock Text="Другий рядок"/>
    <Button Content="Кнопка"/>
</StackPanel>

А Button — приймає лише один елемент між тегами, бо Content — не колекція, а object. Але цим одним елементом може бути ціла панель зі складним вмістом:

<!-- Кнопка з іконкою і текстом всередині -->
<Button>
    <StackPanel Orientation="Horizontal" Spacing="8">
        <TextBlock Text="💾" FontSize="16"/>
        <TextBlock Text="Зберегти"/>
    </StackPanel>
</Button>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Саме через Content Property ми можемо робити кнопки з іконками, зображеннями або будь-яким XAML-вмістом. Це не "трюк" — це фундаментальний принцип WPF Content Model: клас ContentControl свідомо спроектований для довільного вмісту, а не лише для тексту.

Type Converters: як рядок перетворюється на об'єкт

Поверніться на секунду до цього рядка XAML:

<Button Background="Red" Width="150" Margin="10,20,10,20"/>

Зверніть: атрибут Background отримує значення "Red" — рядок. Але в C# властивість Background очікує об'єкт типу Brush. Між рядком "Red" і об'єктом Brushes.Red — прірва типів. Як WPF її перетинає?

Через механізм Type Converters.

Що таке Type Converter

TypeConverter — це клас, що вміє перетворювати значення одного типу в інший. В контексті XAML: з рядка — у потрібний C#-тип. WPF включає десятки вже готових Type Converters для всіх стандартних типів.

Коли XAML-парсер зустрічає рядкове значення атрибута — він запитує: "Який тип очікує ця властивість? Чи є для нього TypeConverter?" Якщо є — TypeConverter викликається і перетворює рядок у потрібний об'єкт.

Ось які перетворення відбуваються непомітно для вас під час парсингу XAML:

XAML рядокОчікуваний типРезультат TypeConverter
"Red"BrushBrushes.Red (тобто new SolidColorBrush(Colors.Red))
"#3b82f6"Brushnew SolidColorBrush(Color.FromRgb(59, 130, 246))
"150"double150.0
"True"booltrue
"10,20,10,20"Thicknessnew Thickness(10, 20, 10, 20)
"10,20"Thicknessnew Thickness(10, 20, 10, 20) (h,v → all four)
"10"Thicknessnew Thickness(10) (uniform)
"Bold"FontWeightFontWeights.Bold
"Center"HorizontalAlignmentHorizontalAlignment.Center
"0,0 1,0"Point[]масив Point для GradientBrush
"Star"GridLengthnew GridLength(1, GridUnitType.Star)
"Auto"GridLengthGridLength.Auto

Особливо цікаво поведінка Thickness. Одне число "10" — відступ з усіх боків. Два числа "10,20" — horizontal (left+right) та vertical (top+bottom). Чотири числа "10,20,30,40" — left, top, right, bottom явно. Все це робить один TypeConverter.

Як WPF знаходить потрібний TypeConverter

Пошук TypeConverter відбувається через атрибут [TypeConverter] на типі або через атрибут [TypeConverter] на властивості. Наприклад, клас Thickness у вихідному коді WPF позначений:

[TypeConverter(typeof(ThicknessConverter))]
public struct Thickness
{
    public double Left;
    public double Top;
    public double Right;
    public double Bottom;
    // ...
}

Коли XAML-парсер зустрічає Margin="10,20" і бачить, що Margin має тип Thickness — він знаходить ThicknessConverter через цей атрибут і викликає ThicknessConverter.ConvertFrom("10,20").

Type Converters — це причина, чому "Red" і "#FF0000" і "#f00" і "Crimson" — всі чотири коректні значення для Background. Все це обробляє BrushConverter, знаючи про різні формати запису кольорів. Вам як розробнику не треба думати про це — просто пишіть зручний рядок.

Attached Properties: "позичена" властивість

Пам'ятаєте, як у WPF з Grid виставити кнопці рядок та стовпець?

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Button Grid.Row="1" Grid.Column="0" Content="Submit"/>
</Grid>

Тут є дещо дивне: Grid.Row і Grid.Column — це атрибути на елементі Button. Але клас Button нічого не знає про Grid! У нього немає властивості Row і немає властивості Column. Як це взагалі компілюється?

Це — Attached Properties (прикріплені властивості). Один з найелегантніших механізмів WPF.

Що таке Attached Property

Attached Property — це властивість, яка визначена в одному класі (наприклад, Grid), але може бути встановлена на об'єктах будь-якого іншого класу (наприклад, Button, TextBox, StackPanel).

Синтаксично у XAML: КласВласника.НазваВластивості="значення":

<Button Grid.Row="1"         <!-- Grid визначив Row, Button отримав значення -->
        Grid.Column="0"      <!-- Grid визначив Column, Button отримав значення -->
        DockPanel.Dock="Top" <!-- DockPanel визначив Dock, Button отримав значення -->
        Canvas.Left="100"    <!-- Canvas визначив Left, Button отримав значення -->
        Panel.ZIndex="5"/>   <!-- Panel визначив ZIndex, Button отримав значення -->

Це не конфлікт і не помилка. Кожна з цих властивостей зберігається у самому Button-об'єкті (в словнику його DependencyObject), але читає їх батьківський елемент під час Layout:

  • Коли Grid розташовує дочірні елементи — він читає Grid.Row та Grid.Column з кожного дочірнього
  • Коли DockPanel розташовує дочірні — він читає DockPanel.Dock з них
  • Коли Canvas розташовує — він читає Canvas.Left, Canvas.Top

Навіщо це потрібно

Альтернативний підхід — дати кожному елементу (Button, TextBox, тощо) властивості Row, Column, Dock, Left, Top, ZIndex... Але це 1) неможливо заздалегідь передбачити всі варіанти, 2) забруднює API кожного елемента властивостями, які йому не належать.

Attached Properties вирішують задачу елегантно: панель визначає потрібні їй властивості у собі, а дочірні елементи їх лише "несуть" — не знаючи і не турбуючись про значення та сенс.

Стандартні Attached Properties у WPF

Ось найчастіше вживані:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="200"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <Label Content="Ім'я:" Grid.Row="0" Grid.Column="0"/>
    <TextBox Grid.Row="0" Grid.Column="1"/>

    <!-- RowSpan: елемент займає 2 рядки -->
    <Button Content="OK" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"/>
</Grid>

Loading Avalonia WebAssembly...

Downloading .NET runtime (10MB)...

Attached Properties — лише концептуальне знайомство на цьому етапі. Глибокий механізм — як вони реалізовані через DependencyProperty.RegisterAttached(), як батьківський елемент їх читає через GetValue(), і як створити власну Attached Property — буде детально розібрано в статті 15. Attached Properties.

x:Name vs Name: яку використовувати

У XAML є два способи дати елементу ім'я для доступу з code-behind:

<TextBlock x:Name="myLabel" Text="Привіт"/>
<TextBlock Name="myLabel2" Text="Також привіт"/>

Обидва дають доступ до елемента з C#: myLabel.Text і myLabel2.Text. Виглядають схоже. Але між ними є принципова різниця.

Що таке Name

Name — це звичайна CLR-властивість типу string, визначена у класі FrameworkElement:

public class FrameworkElement : UIElement
{
    public string Name { get; set; }
}

Коли ви пишете Name="myLabel" у XAML — XAML-парсер робить присвоєння element.Name = "myLabel". Після цього ви можете знайти елемент через FindName("myLabel"). Власне ім'я — як ще одна властивість об'єкта.

Але є обмеження: Name існує тільки у FrameworkElement і його нащадків. Якщо ви намагаєтеся дати ім'я об'єкту, що не є FrameworkElement (наприклад, GradientStop, Style, RowDefinition) — Name там просто немає.

Що таке x:Name

x:Name — це XAML-директива, що належить самому XAML-парсеру. Вона не є властивістю C#-класу. Вона говорить парсеру: "Згенеруй поле з цим ім'ям у *.g.cs файлі і зв'яжи його з цим об'єктом".

<!-- Дає ім'я GradientStop — неможливо через Name -->
<LinearGradientBrush>
    <GradientStop x:Name="firstStop" Color="Blue" Offset="0"/>
</LinearGradientBrush>

Для всіх FrameworkElement-нащадків x:Name і Nameфункціонально рівнозначні. На рівні XAML-парсера x:Name фактично синхронізується з Name для них.

Рекомендація: завжди x:Name

Практичне правило — завжди використовуйте x:Name. Причини:

  1. Працює для будь-якого типу об'єкта в XAML, не тільки FrameworkElement
  2. Це явна XAML-директива — зрозуміло, що ми говоримо парсеру, а не встановлюємо CLR-властивість
  3. IDE Intellisense краще підказує x:Name у правильних контекстах
  4. Консистентно виглядає в коді — читач одразу бачить "це ім'я для code-behind"
<!-- ✅ Рекомендовано -->
<TextBlock x:Name="statusLabel" Text="Готово"/>
<Button x:Name="submitButton" Content="Надіслати"/>
<Grid x:Name="mainGrid">
    <Grid.RowDefinitions>
        <RowDefinition x:Name="headerRow" Height="Auto"/>
    </Grid.RowDefinitions>
</Grid>

<!-- ⚠️ Працює, але не рекомендується -->
<TextBlock Name="statusLabel" Text="Готово"/>

XAML ↔ C# шпаргалка

Ось зведена таблиця всіх XAML-конструкцій та їхніх C#-еквівалентів. Вона стане вашим орієнтиром кожного разу, коли XAML здаватиметься магічним.

XAMLC#-еквівалентМеханізм
<Button/>new Button()Виклик конструктора
<Button Content="OK"/>btn.Content = "OK"Attribute Syntax
<Button Width="150"/>btn.Width = 150.0TypeConverter: string → double
<Button Background="Red"/>btn.Background = Brushes.RedTypeConverter: string → Brush
<Button Margin="10,20"/>btn.Margin = new Thickness(10, 20, 10, 20)TypeConverter: string → Thickness
<Button IsEnabled="True"/>btn.IsEnabled = trueTypeConverter: string → bool
<Button HorizontalAlignment="Center"/>btn.HorizontalAlignment = HorizontalAlignment.CenterTypeConverter: string → Enum
<Button>Text</Button>btn.Content = "Text"Content Property
<Button><Image.../></Button>btn.Content = new Image(...)Content Property (складний об'єкт)
<Button.Background><LinearGradientBrush/></Button.Background>btn.Background = new LinearGradientBrush(...)Property Element Syntax
<StackPanel><Button/><TextBox/></StackPanel>panel.Children.Add(btn); panel.Children.Add(tb)Content Property (колекція)
<Button x:Name="myBtn"/>internal Button myBtn; (у *.g.cs)XAML directive → поле класу
<Button Grid.Row="1"/>Grid.SetRow(btn, 1)Attached Property
<Button Click="Btn_Click"/>btn.Click += Btn_ClickEvent subscription
<Button Command="{Binding SaveCmd}"/>btn.SetBinding(Button.CommandProperty, "SaveCmd")Markup Extension (Binding)

Кілька рядків у цій таблиці використовують {Binding} — це Markup Extension, окрема тема, яку повністю розберемо у статті 08. Data Binding. Поки просто зафіксуйте: фігурні дужки у значенні атрибута {...} — це Markup Extension, не звичайний рядок.


Підсумок: що ми дізналися

XAML — це C#

Кожен тег = new Type(). Кожен атрибут = присвоєння властивості. XAML — це не HTML, а серіалізований об'єктний граф. Будь-яке XAML можна написати на чистому C# і навпаки.

XML — суворий

Закривайте теги, пишіть значення в лапках, дотримуйтесь регістру. Одна помилка — XamlParseException замість прощення помилок як у браузері HTML.

Дві форми запису

Attribute Syntax для простих значень. Property Element Syntax — коли значення є складним об'єктом. Можна змішувати в одному тезі.

Content Property

Текст між тегами іде у "головну" властивість класу. ButtonContent, StackPanelChildren. Завдяки цьому кнопки можуть містити довільний XAML-вміст.

Type Converters

Рядки автоматично перетворюються у потрібні типи: "Red"Brush, "10,20"Thickness, "Bold"FontWeight. Це прозоро і автоматично.

x:Name для code-behind

Завжди використовуйте x:Name (не Name) для елементів, до яких треба звертатися з C#. Це XAML-директива, яка генерує поле у *.g.cs.

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