Щоразу, коли ви пишете Background="{StaticResource PrimaryBrush}" або Text="{Binding UserName}" — ви бачите XAML Markup Extension. Фігурні дужки {...} — це не особливий синтаксис, не магія і не вбудована можливість XML. Це виклик методу.
Конкретно: {} у значенні атрибута — це виклик методу ProvideValue() відповідного класу, що наслідує абстрактний клас MarkupExtension. XAML-парсер побачив {StaticResource PrimaryBrush}, знайшов клас StaticResourceExtension, створив його екземпляр і викликав ProvideValue() — і те, що повернув ProvideValue(), і стало значенням Background.
Це нетривіальне розуміння, але воно відкриває двері до Power User рівня XAML. Ви більше не питаєте "чому це працює" — ви розумієте "як саме це працює".
MarkupExtension з єдиним абстрактним методом ProvideValue(IServiceProvider). IServiceProvider — інтерфейс через який MarkupExtension дізнається контекст: на якій властивості, якого об'єкта, в якому файлі він викликається. [MarkupExtensionReturnType] — атрибут що декларує тип, який поверне ProvideValue. x:Static — звернення до статичних членів (поля, властивості, enum-значення) з XAML. x:Type — передача типу (typeof(T)) як значення у XAML. x:Null — явний null як значення атрибуту. x:Array — створення масиву у XAML.{...}Розглянемо детально, як XAML-парсер обробляє рядок Background="{StaticResource PrimaryBrush}".
{XAML-парсер читає XML-атрибут. Якщо значення починається з { (і не починається з {}— це escape для літерального {) — це Markup Extension. Парсер не обробляє решту рядка як рядкове значення.
Перше слово після { — ім'я Markup Extension класу. WPF (і XAML взагалі) відшукує клас за таким алгоритмом:
StaticResourceStaticResourceExtension (конвенція: суфікс Extension може бути відкинутий у XAML)xmlns-маппінгах поточного файлу// WPF знаходить саме цей клас:
public class StaticResourceExtension : MarkupExtension
{
public StaticResourceExtension(object resourceKey) { ... }
public override object ProvideValue(IServiceProvider serviceProvider)
{
// Шукає ресурс по ключу у ResourceDictionary
return FindResource(resourceKey, serviceProvider);
}
}
ProvideValueВсе після імені класу — це параметри. PrimaryBrush — позиційний параметр (йде у конструктор). Для іменованих параметрів використовується синтаксис {ExtensionName Property=Value, OtherProp=Value2}:
<!-- Позиційний параметр: йде у конструктор(ключ) -->
{StaticResource PrimaryBrush}
<!-- Іменовані параметри: встановлюються через властивості -->
{Binding ElementName=mySlider, Path=Value, Mode=OneWay}
<!-- Комбінація: перший позиційний, далі іменовані -->
{DynamicResource {x:Type Button}}
Після парсингу параметрів — парсер викликає ProvideValue(serviceProvider). Результат присвоюється властивості (у прикладі — Background).
{}Щоб написати літеральну рядок що починається з { — використовується {} як escape:
<!-- Це Markup Extension (викликає ProvideValue): -->
<TextBlock Text="{Binding UserName}"/>
<!-- Це літеральний рядок "{Binding UserName}" — без виклику: -->
<TextBlock Text="{}{Binding UserName}"/>
<!-- Або через Property Element Syntax: -->
<TextBlock>
<TextBlock.Text>{Binding UserName}</TextBlock.Text>
</TextBlock>
IServiceProvider: контекст виконанняProvideValue(IServiceProvider serviceProvider) отримує serviceProvider — це не просто формальність. Через нього MarkupExtension може дізнатися:
public override object ProvideValue(IServiceProvider serviceProvider)
{
// Дізнатися цільовий об'єкт і властивість
var targetProvider = serviceProvider.GetService<IProvideValueTarget>();
var targetObject = targetProvider.TargetObject; // наприклад, Button
var targetProperty = targetProvider.TargetProperty; // наприклад, Button.Background
// Дізнатися тип (для x:Type)
var typeProvider = serviceProvider.GetService<IXamlTypeResolver>();
// Дізнатися шлях до файлу (для дизайн-таймових перевірок)
var rootProvider = serviceProvider.GetService<IRootObjectProvider>();
return /* обчислене значення */;
}
StaticResourceExtension використовує targetObject щоб знайти ResourceDictionary через Visual Tree — начинаючи від цільового Button піднятись до Application.Resources.
{x:Static}: статичні члени у XAMLx:Static дозволяє посилатися на статичні поля, властивості та enum-значення прямо у XAML:
<!-- Системний колір: SystemColors.HighlightBrush -->
<TextBlock Foreground="{x:Static SystemColors.HighlightBrush}"
Text="Я системного кольору виділення"/>
<!-- Системний шрифт: SystemFonts.DefaultFontFamily -->
<TextBlock FontFamily="{x:Static SystemFonts.DefaultFontFamily}"
Text="Я у системному шрифті"/>
<!-- Системний розмір шрифту -->
<TextBlock FontSize="{x:Static SystemFonts.DefaultFontSize}"
Text="Розмір шрифту ОС"/>
<!-- Enum-значення (FlowDirection) -->
<TextBlock FlowDirection="{x:Static FlowDirection.RightToLeft}"
Text="Справа наліво (RTL)"/>
Для власних статичних членів — потрібен xmlns:local:
<!-- Ваш статичний клас з константами -->
<Window xmlns:local="clr-namespace:MyApp">
<Border BorderThickness="{x:Static local:AppConstants.DefaultBorderThickness}"/>
<TextBlock FontSize="{x:Static local:AppTheme.BaseFontSize}"/>
</Window>
// У C#:
public static class AppConstants
{
public static readonly Thickness DefaultBorderThickness = new Thickness(1);
public static readonly double CardCornerRadius = 8.0;
}
public static class AppTheme
{
public static readonly double BaseFontSize = 14.0;
public static readonly double HeadingFontSize = 24.0;
public static readonly Brush AccentBrush = new SolidColorBrush(Color.FromRgb(37, 99, 235));
}
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="16" Spacing="12">
<TextBlock Text="Системний колір виділення (HighlightBrush):"
FontWeight="Bold"/>
<Border Padding="10,6" Background="{x:Static SystemColors.HighlightBrush}">
<TextBlock Foreground="{x:Static SystemColors.HighlightTextBrush}"
Text="Текст у кольорі виділення системи"/>
</Border>
<TextBlock Text="Системний колір посилань (HotTrackBrush):"
FontWeight="Bold"/>
<TextBlock Foreground="{x:Static SystemColors.HotTrackBrush}"
Text="Натисніть тут для переходу"
TextDecorations="Underline"
Cursor="Hand"/>
<TextBlock Text="Системний колір вікна (WindowBrush):"
FontWeight="Bold"/>
<Border Padding="10,6" Background="{x:Static SystemColors.WindowBrush}"
BorderBrush="{x:Static SystemColors.ActiveBorderBrush}" BorderThickness="1">
<TextBlock Foreground="{x:Static SystemColors.WindowTextBrush}"
Text="Фон та текст в кольорах системного вікна"/>
</Border>
<TextBlock Text="FlowDirection enum через x:Static:"
FontWeight="Bold"/>
<TextBlock Text="مرحباً: Арабський текст (RTL)"
FlowDirection="{x:Static FlowDirection.RightToLeft}"
Background="#fef9c3" Padding="8,4"/>
</StackPanel>
Корисне правило: якщо ви хочете щоб ваш застосунок виглядав нативно і враховував налаштування Windows (для людей з вадами зору, що змінюють кольори ОС) — використовуйте SystemColors та SystemFonts замість хардкодованих значень.
{x:Type}: тип як значенняx:Type — це XAML-аналог оператора typeof() у C#. Він повертає System.Type. Найчастіше використовується у стилях, темах та DataTemplate:
<!-- Без x:Type (було б помилкою — рядок не конвертується у Type): -->
<!-- <Style TargetType="Button"> --> <!-- Насправді XAML сам розуміє рядок тут -->
<!-- З x:Type (явна форма): -->
<Style TargetType="{x:Type Button}">
<Setter Property="Padding" Value="8,4"/>
</Style>
<!-- x:Type у DataTrigger: -->
<DataTrigger Binding="{Binding CurrentView}" Value="{x:Type local:HomeView}">
<Setter Property="Content" Value="Головна"/>
</DataTrigger>
<!-- x:Type для створення об'єкта через ObjectDataProvider: -->
<ObjectDataProvider ObjectType="{x:Type local:MyDataService}"
MethodName="GetAllUsers"/>
Чому TargetType="Button" і TargetType="{x:Type Button}" обидва працюють? Тому що TargetType має Type Converter (TypeTypeConverter), що перетворює рядок "Button" у typeof(Button). Але явний x:Type читабельніший і не залежить від Type Converter.
{x:Null}: явний nullВикористовується коли стандартний null неможливо передати рядковим значенням, або коли треба явно перевизначити успадковане значення:
<!-- Кнопка без фону (прозора) — явно null, а не просто "Transparent" -->
<Button Background="{x:Null}" Content="Без фону"/>
<!-- Скасування успадкованого курсору -->
<TextBox Cursor="{x:Null}" Text="Звичайний курсор (успадкований)"/>
<!-- Border без рамки (null відрізняється від Transparent!) -->
<Border BorderBrush="{x:Null}" BorderThickness="0" Background="#e2e8f0">
<TextBlock Text="Border без рамки"/>
</Border>
Різниця між {x:Null} та рядком-пустим значенням:
Background="" → помилка або пусте значенняBackground="{x:Null}" → null, повністю прозорий (хіт-тестинг теж відсутній)Background="Transparent" → #00FFFFFF — прозорий колір, але Background != null (хіт-тестинг працює){x:Array}: масив у XAMLДозволяє оголосити масив прямо у XAML:
<Window xmlns:sys="clr-namespace:System;assembly=mscorlib">
<Window.Resources>
<!-- Масив рядків -->
<x:Array x:Key="Countries" Type="{x:Type sys:String}">
<sys:String>Україна</sys:String>
<sys:String>Польща</sys:String>
<sys:String>Німеччина</sys:String>
<sys:String>Франція</sys:String>
</x:Array>
<!-- Масив кольорів -->
<x:Array x:Key="PaletteColors" Type="{x:Type Color}">
<Color>#2563eb</Color>
<Color>#7c3aed</Color>
<Color>#059669</Color>
<Color>#dc2626</Color>
</x:Array>
</Window.Resources>
<StackPanel>
<!-- Використання у ComboBox -->
<ComboBox ItemsSource="{StaticResource Countries}"/>
<!-- Або у ListBox -->
<ListBox ItemsSource="{StaticResource Countries}"/>
</StackPanel>
</Window>
x:Array рідко використовується у production (там зазвичай ItemsSource прив'язаний до ViewModel), але корисний для прототипування і демонстрацій.
{StaticResource} і {DynamicResource} — тепер очима Markup ExtensionМи вже знайомі з цими двома зі статті про ресурси. Але тепер, розуміючи механізм MarkupExtension, подивимось глибше.
StaticResourceExtension — найпростіший з Markup Extensions. Його ProvideValue викликається один раз і повертає значення з ResourceDictionary. Немає підписок, немає відслідковування — просто пошук і повернення:
// Спрощена реалізація StaticResourceExtension:
public class StaticResourceExtension : MarkupExtension
{
public object ResourceKey { get; set; }
public StaticResourceExtension(object resourceKey)
=> ResourceKey = resourceKey;
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = serviceProvider.GetService<IProvideValueTarget>();
var targetObject = target.TargetObject as FrameworkElement;
// Шукаємо по ResourceDictionary-ланцюжку:
return targetObject?.FindResource(ResourceKey)
?? throw new XamlParseException($"Ресурс '{ResourceKey}' не знайдено");
}
}
DynamicResourceExtension — складніший. Його ProvideValue не повертає значення одразу. Натомість він реєструє слухача на зміни ResourceDictionary і повертає this (або спеціальний placeholder). Коли ресурс з'являється або змінюється — слухач викликає SetValue безпосередньо на цільовій властивості:
// Псевдокод DynamicResourceExtension:
public override object ProvideValue(IServiceProvider serviceProvider)
{
var target = serviceProvider.GetService<IProvideValueTarget>();
// Підписуємось на зміни ресурсу:
var listener = new ResourceChangedListener(target.TargetObject, target.TargetProperty, ResourceKey);
ResourceDictionary.AddResourceChangedHandler(listener);
// Повертаємо поточне значення (може бути null якщо ресурс ще не існує):
return FindCurrentValue(ResourceKey, target.TargetObject);
}
Ось звідки така різниця у поведінці: StaticResource — одноразовий знімок, DynamicResource — підписка на зміни.
{Binding}: перший погляд{Binding} — найскладніший і найпотужніший Markup Extension у WPF. Ми детально вивчимо його у Блоці 7 (Data Binding). Але вже зараз — важливо зрозуміти: Binding теж є Markup Extension. Він підпорядковується тим самим правилам.
Прямо зараз розглянемо лише одну форму Binding: прив'язка до іншого UI-елемента через ElementName.
ElementName<!-- Slider → TextBlock: без жодного C# коду! -->
<StackPanel Spacing="12" Margin="16">
<Slider x:Name="volumeSlider" Minimum="0" Maximum="100" Value="50"/>
<!-- TextBlock.Text прив'язаний до volumeSlider.Value -->
<TextBlock Text="{Binding ElementName=volumeSlider, Path=Value}"/>
</StackPanel>
Що тут відбувається з точки зору Markup Extension:
{Binding ElementName=volumeSlider, Path=Value}BindingExtension (або Binding у WPF)ElementName = "volumeSlider", Path = new PropertyPath("Value")ProvideValue — той повертає BindingExpressionBindingExpression знаходить елемент volumeSlider за ім'ям і підписується на зміни його ValueTextBlock.TextLoading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="16" Spacing="16">
<TextBlock Text="Регулятор гучності" FontSize="14" FontWeight="SemiBold"
Foreground="#1e293b"/>
<Slider x:Name="volumeSlider" Minimum="0" Maximum="100" Value="65"
TickFrequency="10" TickPlacement="BottomRight"
IsSnapToTickEnabled="False"/>
<!-- Прив'язка до Value слайдера -->
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="🔊" FontSize="18"/>
<TextBlock FontSize="18" Foreground="#2563eb" FontWeight="Bold">
<TextBlock.Text>
<Binding ElementName="volumeSlider" Path="Value"
StringFormat="{}{0:0}%"/>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<!-- ProgressBar теж прив'язаний до того самого Slider -->
<ProgressBar Minimum="0" Maximum="100"
Value="{Binding ElementName=volumeSlider, Path=Value}"
Height="8" Background="#e2e8f0" Foreground="#2563eb"/>
<Separator/>
<TextBlock Text="Розмір шрифту" FontSize="14" FontWeight="SemiBold"
Foreground="#1e293b"/>
<Slider x:Name="fontSlider" Minimum="8" Maximum="48" Value="18"
TickFrequency="4" TickPlacement="BottomRight"/>
<!-- TextBlock змінює власний розмір шрифту через Binding! -->
<TextBlock FontSize="{Binding ElementName=fontSlider, Path=Value}"
Text="Зразок тексту: ABCDEF abcdef 123"
HorizontalAlignment="Center"
Foreground="#475569"/>
</StackPanel>
{Binding ElementName=..., Path=...} — це Element-to-Element Binding. Він не потребує ViewModel, DataContext чи INotifyPropertyChanged. Binding-механізм напряму підписується на DependencyProperty одного елемента і оновлює DependencyProperty іншого. У Блоці 7 ми розберемо DataContext Binding, конвертери значень, режими (OneWay/TwoWay/OneTime) і багато іншого.{...}Найпотужніша частина системи Markup Extensions — ви можете створити власний. Це звичайний C#-клас і кілька рядків коду.
using System.Windows.Markup;
// Суфікс Extension відкидається у XAML: {Multiply Value=..., Factor=...}
public class MultiplyExtension : MarkupExtension
{
// Властивості = іменовані параметри у XAML
public double Value { get; set; }
public double Factor { get; set; }
// Конструктор за замовчуванням (обов'язковий для іменованих параметрів)
public MultiplyExtension() { }
// Конструктор з позиційними параметрами (опційний)
public MultiplyExtension(double value, double factor)
{
Value = value;
Factor = factor;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Value * Factor;
}
}
<!-- Використання у XAML: -->
<Window xmlns:ext="clr-namespace:MyApp.Extensions">
<TextBlock FontSize="{ext:Multiply Value=4, Factor=4}"
Text="FontSize = 4 × 4 = 16"/>
<Border Width="{ext:Multiply 12, 8}"
Height="{ext:Multiply 4, 8}"
Background="#2563eb"/>
</Window>
{EnumDescription}Практичніший Custom Extension — той, що перетворює enum-значення у human-readable опис:
// Атрибут для enum-значень
public class DescriptionAttribute : Attribute
{
public string Description { get; }
public DescriptionAttribute(string description) => Description = description;
}
// Enum з описами
public enum UserStatus
{
[Description("Активний")] Active,
[Description("Заблокований")] Banned,
[Description("Очікує підтвердження")] PendingVerification,
[Description("Видалений")] Deleted
}
// Extension
public class EnumDescriptionExtension : MarkupExtension
{
public Enum Value { get; set; }
public EnumDescriptionExtension(Enum value) => Value = value;
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (Value is null) return string.Empty;
var field = Value.GetType().GetField(Value.ToString());
var attr = field?.GetCustomAttribute<DescriptionAttribute>();
return attr?.Description ?? Value.ToString();
}
}
<!-- Використання: -->
<xmlns:ext="clr-namespace:MyApp.Extensions"
xmlns:enums="clr-namespace:MyApp.Domain">
<!-- Замість Text="Active" → "Активний" -->
<TextBlock Text="{ext:EnumDescription {x:Static enums:UserStatus.Active}}"/>
<TextBlock Text="{ext:EnumDescription {x:Static enums:UserStatus.PendingVerification}}"/>
{LocalizedString}Найпоширеніший сценарій — локалізація рядків через Markup Extension:
[MarkupExtensionReturnType(typeof(string))]
public class LocalizedStringExtension : MarkupExtension
{
public string Key { get; set; }
public LocalizedStringExtension() { }
public LocalizedStringExtension(string key) => Key = key;
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (string.IsNullOrEmpty(Key))
return $"[Empty Key]";
// Шукаємо ресурс у ResourceManager або власному словнику
var value = Strings.ResourceManager.GetString(Key,
Thread.CurrentThread.CurrentUICulture);
return value ?? $"[{Key}]"; // Fallback: показуємо ключ у квадратних дужках
}
}
<!-- Тепер UI залишається декларативним при будь-якій локалізації: -->
<Button Content="{ext:LocalizedString SaveChanges}"/>
<TextBlock Text="{ext:LocalizedString WelcomeMessage}"/>
<Window Title="{ext:LocalizedString AppTitle}"/>
Перевага перед {StaticResource} для локалізації: LocalizedStringExtension може враховувати поточну культуру, перевантаження за певними правилами (fallback до базової мови), і при потребі — підписатися на зміни мови в runtime (якщо зробити DynamicResource-подібну логіку).
Extension, але у XAML цей суфікс відкидається: LocalizedStringExtension в XAML використовується як {ext:LocalizedString Key}. Це конвенція, а не вимога — ніщо не заважає назвати клас L10n і писати {ext:L10n Key}.Механізм
{...} у XAML = виклик ProvideValue() класу що наслідує MarkupExtension. IServiceProvider дає доступ до контексту: цільовий об'єкт, властивість, кореневий об'єкт.Вбудовані x:
{x:Static} → статичні поля/властивості/enum{x:Type} → typeof(T) у XAML{x:Null} → явний null{x:Array Type=...} → масив у XAMLStatic vs Dynamic
StaticResource.ProvideValue → одноразовий пошук і повернення. DynamicResource.ProvideValue → підписка на зміни ResourceDictionary, оновлення через SetValue при зміні.Custom Extension
MarkupExtension, реалізує ProvideValue. Властивості класу = іменовані параметри у {MyExt Prop=Value}. Конструктор = позиційні параметри. Нульова магія.Завдання: Використайте {x:Static} для підстановки системних кольорів, шрифтів та статичних значень DateTime у вигляд.
Частина 1: Системні теми (5 елементів):
TextBlock з Background="{x:Static SystemColors.HighlightBrush}" та Foreground="{x:Static SystemColors.HighlightTextBrush}"Border з BorderBrush="{x:Static SystemColors.ActiveBorderBrush}"TextBlock FontFamily="{x:Static SystemFonts.DefaultFontFamily}" — системний шрифтTextBlock з Foreground="{x:Static SystemColors.HotTrackBrush}" і TextDecorations="Underline" (стиль посилання)Частина 2: Власний static-клас:
static class AppConfig з полями BaseFontSize = 14.0, BorderRadius = 8.0, PrimaryColor = new SolidColorBrush(Color.FromRgb(37, 99, 235)){x:Static local:AppConfig.BaseFontSize} у TextBlock.FontSize{x:Static local:AppConfig.PrimaryColor} у Button.BackgroundПеревірка: Зміна системної теми (кольорів) у Windows → SystemColors-елементи підхоплюють нову тему.
Завдання: Реалізуйте інтерактивний UI з Binding між елементами — без жодного рядка C# у code-behind.
Три стовпці налаштувань (через Grid):
Колонка 1: Шрифт
Slider x:Name="fontSlider" від 10 до 72TextBlock що показує поточний розмір: {Binding ElementName=fontSlider, Path=Value, StringFormat="{}{0:0}px"}TextBlock з FontSize="{Binding ElementName=fontSlider, Path=Value}" і текстом "Зразок тексту"Колонка 2: Прозорість
Slider x:Name="opacitySlider" від 0 до 1 (з SmallChange="0.1")TextBlock що показує {0:P0} (відсоток)Rectangle Width="80" Height="80" Fill="#2563eb" з Opacity="{Binding ElementName=opacitySlider, Path=Value}"Колонка 3: Розмір
Slider x:Name="sizeSlider" від 20 до 200Ellipse чий Width і Height обидва прив'язані до sizeSlider.ValueПеревірка: Пересування будь-якого Slider миттєво оновлює відповідний елемент — без C# коду.
Завдання: Створіть два Custom Markup Extensions і використайте їх у XAML.
Extension 1: {Multiply Value, Factor}
// {ext:Multiply Value=8, Factor=3} → 24.0
public class MultiplyExtension : MarkupExtension { ... }
Використайте для: FontSize, Width, Height, BorderThickness.
Extension 2: {Clamp Value, Min, Max}
// {ext:Clamp Value=5.0, Min=1, Max=3} → 3.0
// {ext:Clamp Value=-1, Min=0, Max=100} → 0.0
public class ClampExtension : MarkupExtension { ... }
Використайте для обмеження FontSize або Opacity в визначеному діапазоні.
Extension 3 (опціонально): {IfElse Condition, TrueValue, FalseValue}
// Condition — статичне bool-поле
// {ext:IfElse {x:Static local:AppSettings.IsDarkMode}, "#1e293b", "White"}
public class IfElseExtension : MarkupExtension { ... }
Що треба знати: MarkupExtension, ProvideValue, конструктори з параметрами, [MarkupExtensionReturnType], реєстрація через xmlns:ext.
XAML в Avalonia: ключові відмінності від WPF
Порівнюємо XAML синтаксис WPF та Avalonia: namespace URI, розширення .axaml, using: замість clr-namespace, ресурси та Avalonia Previewer.
Панелі Layout: StackPanel, WrapPanel, DockPanel
Вивчаємо основні панелі розташування WPF: двопрохідна модель Measure/Arrange, StackPanel для лінійного вкладання, WrapPanel для автоматичного переносу та DockPanel для класичного інтерфейсу.