Уявіть собі кнопку у вашому WPF-застосунку. Її колір фону (Background) може бути заданий у кількох місцях одночасно:
Background="Red"){Binding UserPreferredColor})Яке значення "виграє"? Як WPF вирішує цей конфлікт? Чому Button.Background — це не просто звичайна C#-властивість з getter/setter?
Відповідь криється у Dependency Property System — одній з найважливіших архітектурних особливостей WPF. Це система, що робить можливими прив'язку даних, стилізацію, анімації та успадкування значень по дереву елементів.
Перш ніж зануритися у DependencyProperty, розберемося, чому звичайні C#-властивості (CLR properties) не підходять для UI-фреймворку.
Розглянемо типову CLR-властивість:
public class SimpleButton
{
private Brush _background = Brushes.Gray;
public Brush Background
{
get => _background;
set => _background = value;
}
}
Що не так з цим підходом у контексті UI?
❌ Немає прив'язки даних
INotifyPropertyChanged для кожної властивості.❌ Немає стилізації
❌ Немає анімацій
❌ Немає успадкування
FontSize, задане на Window, не може автоматично "стекти" до всіх дочірніх елементів.❌ Неефективне зберігання
Припустимо, ми хочемо створити систему тем для додатку:
// ❌ Неправильний підхід з CLR-властивостями
public class ThemedButton
{
private Brush _background;
public Brush Background
{
get => _background ?? ThemeManager.Current.ButtonBackground; // Fallback до теми
set => _background = value;
}
}
Проблеми цього коду:
ThemeManager.Current змінився, кнопка не оновитьсяWPF вирішує всі ці проблеми через централізовану систему властивостей, де:
EffectiveValueEntry[])Dependency Property складається з трьох частин:
Розглянемо реальний приклад з WPF:
public class Button : ButtonBase
{
// 1️⃣ Статичне поле — ідентифікатор властивості
public static readonly DependencyProperty BackgroundProperty =
DependencyProperty.Register(
name: "Background", // Назва властивості
propertyType: typeof(Brush), // Тип значення
ownerType: typeof(Button), // Клас-власник
typeMetadata: new FrameworkPropertyMetadata(
defaultValue: null, // Значення за замовчуванням
flags: FrameworkPropertyMetadataOptions.AffectsRender
)
);
// 2️⃣ CLR-обгортка для зручності
public Brush Background
{
get => (Brush)GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
}
{PropertyName}Property. Це дозволяє легко знаходити DependencyProperty через рефлексію та забезпечує консистентність API.Назва "Dependency Property" означає, що значення властивості залежить від багатьох джерел. WPF автоматично визначає, яке джерело має найвищий пріоритет, і повертає відповідне значення.
// Коли ви пишете:
myButton.Background = Brushes.Red;
// Насправді відбувається:
myButton.SetValue(Button.BackgroundProperty, Brushes.Red);
// А коли читаєте:
var color = myButton.Background;
// WPF виконує складну логіку:
// 1. Перевіряє, чи є активна анімація
// 2. Перевіряє локальне значення
// 3. Перевіряє Style Triggers
// 4. Перевіряє Style Setters
// 5. Перевіряє успадковане значення
// 6. Повертає default value
Коли WPF потрібно отримати значення Dependency Property, він проходить через ланцюжок пріоритетів (Property Value Precedence). Це впорядкований список джерел значень — від найвищого пріоритету до найнижчого.
| Пріоритет | Джерело | Опис | Приклад |
|---|---|---|---|
| 1 | Animation (Active) | Активна анімація змінює значення | DoubleAnimation для Opacity |
| 2 | Local Value | Значення, задане напряму через код або XAML | Background="Red" або SetValue() |
| 3 | Style Triggers | Тригери у стилі елемента | <Trigger Property="IsMouseOver"> |
| 4 | Template Triggers | Тригери у ControlTemplate | Зміна кольору кнопки при натисканні |
| 5 | Style Setters | Setter'и у стилі елемента | <Setter Property="Background"> |
| 6 | Theme Style | Стиль з теми WPF (Aero, Luna) | Стандартний вигляд Button |
| 7 | Property Value Inheritance | Успадковане значення від батьківського елемента | FontSize від Window до TextBlock |
| 8 | Default Value | Значення з PropertyMetadata.DefaultValue | null для Background |
Розберемо кілька реальних сценаріїв, щоб зрозуміти, як працює система пріоритетів.
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
<Setter Property="Foreground" Value="White"/>
</Style>
</Window.Resources>
<StackPanel Margin="20">
<Button Content="Кнопка 1"/>
<Button Content="Кнопка 2" Background="Red"/>
</StackPanel>
Що відбувається:
Background = BlueBackground="Red" → воно має вищий пріоритет → Background = RedLoading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<Button Content="Кнопка зі стилем (синя)"/>
<Button Content="Кнопка з локальним значенням (червона)" Background="Red"/>
</StackPanel>
Що робити, якщо ми хочемо повернутися до стилю після встановлення локального значення?
// Встановлюємо локальне значення
myButton.Background = Brushes.Red;
// Тепер Background = Red (Local Value, пріоритет 2)
// Скидаємо локальне значення
myButton.ClearValue(Button.BackgroundProperty);
// Тепер Background = Blue (Style Setter, пріоритет 5)
ClearValue()не встановлює значення у null. Він видаляє локальне значення зі сховища, дозволяючи системі пріоритетів "провалитися" на наступний рівень (Style, Theme, Default).Деякі властивості автоматично "стікають" по дереву елементів від батька до дітей. Це називається Property Value Inheritance.
<Window FontSize="16" FontFamily="Segoe UI">
<StackPanel>
<TextBlock Text="Цей текст успадковує FontSize=16"/>
<TextBlock Text="І цей теж" FontWeight="Bold"/>
<Button Content="Кнопка теж успадковує шрифт"/>
</StackPanel>
</Window>
Які властивості підтримують Inheritance?
| Властивість | Успадковується? | Чому? |
|---|---|---|
FontSize | ✅ Так | Логічно, щоб весь текст у вікні мав однаковий розмір |
FontFamily | ✅ Так | Консистентність типографіки |
Foreground | ✅ Так | Колір тексту має бути єдиним для всього інтерфейсу |
Background | ❌ Ні | Кожен контрол має свій фон |
Width / Height | ❌ Ні | Розміри індивідуальні для кожного елемента |
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10" FontSize="18" Foreground="DarkBlue">
<TextBlock Text="Успадкований FontSize=18 та Foreground=DarkBlue"/>
<TextBlock Text="Теж успадкований" FontWeight="Bold"/>
<Button Content="Кнопка з успадкованим шрифтом"/>
</StackPanel>
FontSize у кожному TextBlock окремо. Якщо значення не задане локально — WPF піднімається по дереву до батька, потім до батька батька, і так далі, поки не знайде значення або не досягне кореня.Розберемо внутрішню реалізацію Dependency Property System (спрощено).
Кожен DependencyObject (базовий клас для всіх UI-елементів) містить масив EffectiveValueEntry[]:
public class DependencyObject
{
// Спрощена версія
private EffectiveValueEntry[] _effectiveValues;
public object GetValue(DependencyProperty dp)
{
// 1. Знайти entry для цієї властивості
int index = dp.GlobalIndex;
EffectiveValueEntry entry = _effectiveValues[index];
// 2. Якщо є локальне значення — повернути його
if (entry.HasLocalValue)
return entry.LocalValue;
// 3. Якщо є Style Setter — повернути його
if (entry.HasStyleValue)
return entry.StyleValue;
// 4. Перевірити Inheritance
if (dp.IsInherited)
{
DependencyObject parent = GetParent();
if (parent != null)
return parent.GetValue(dp);
}
// 5. Повернути Default Value
return dp.DefaultMetadata.DefaultValue;
}
}
🚀 Економія пам'яті
_effectiveValues. Масив розширюється динамічно лише для властивостей з non-default значеннями.⚡ Швидкий доступ
GlobalIndex дозволяє отримати значення за O(1) — прямий доступ до масиву без хешування чи пошуку.🔄 Автоматична інвалідація
PropertyChanged подій.Кожна Dependency Property має метадані (PropertyMetadata), що визначають її поведінку.
Найчастіше використовується FrameworkPropertyMetadata — розширена версія з прапорцями для WPF:
public static readonly DependencyProperty MyProperty =
DependencyProperty.Register(
"MyProperty",
typeof(string),
typeof(MyControl),
new FrameworkPropertyMetadata(
defaultValue: "Default",
flags: FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsMeasure,
propertyChangedCallback: OnMyPropertyChanged
)
);
private static void OnMyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (MyControl)d;
// Реакція на зміну значення
Console.WriteLine($"MyProperty змінилася: {e.OldValue} → {e.NewValue}");
}
| Прапорець | Опис |
|---|---|
AffectsRender | Зміна властивості вимагає перемалювання (re-render) |
AffectsMeasure | Зміна властивості вимагає перерахунку розмірів (measure pass) |
AffectsArrange | Зміна властивості вимагає перерахунку позиції (arrange pass) |
AffectsParentMeasure | Зміна властивості вимагає перерахунку розмірів батьківського елемента |
BindsTwoWayByDefault | Binding за замовчуванням має режим TwoWay (наприклад, TextBox.Text) |
Inherits | Властивість підтримує Property Value Inheritance |
Journal | Значення зберігається в історії навігації |
AffectsMeasure без необхідності. Measure pass — дорога операція, що може викликати каскадний перерахунок всього Layout Tree.Закріпимо знання через практику.
Завдання: Створіть WPF-застосунок, що демонструє конфлікт між Style та Local Value.
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="LightBlue"/>
<Setter Property="Padding" Value="10"/>
</Style>
</Window.Resources>
<StackPanel Margin="20">
<Button Content="Кнопка зі стилем" Margin="0,0,0,10"/>
<Button Content="Кнопка з локальним значенням" Background="Red" Margin="0,0,0,10"/>
<Button x:Name="DynamicButton" Content="Кнопка для експериментів"/>
</StackPanel>
// У code-behind
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Встановлюємо локальне значення
DynamicButton.Background = Brushes.Green;
// Через 2 секунди скидаємо його
Task.Delay(2000).ContinueWith(_ =>
{
Dispatcher.Invoke(() =>
{
DynamicButton.ClearValue(Button.BackgroundProperty);
});
});
}
Очікуваний результат: Кнопка спочатку зелена, потім стає світло-синьою (повертається до стилю).
Завдання: Визначте, які властивості підтримують Property Value Inheritance.
<Window x:Class="InheritanceTest.MainWindow"
FontSize="20"
FontFamily="Arial"
Foreground="DarkGreen"
Background="LightYellow">
<StackPanel Margin="20">
<TextBlock Text="Текст 1"/>
<TextBlock Text="Текст 2" FontSize="14"/>
<Button Content="Кнопка"/>
<Border BorderBrush="Black" BorderThickness="1" Padding="10">
<TextBlock Text="Текст у Border"/>
</Border>
</StackPanel>
</Window>
Відповідайте на питання:
FontSize у TextBlock?Background у StackPanel?Foreground у Button.Content?private void Window_Loaded(object sender, RoutedEventArgs e)
{
var textBlock = (TextBlock)((StackPanel)Content).Children[0];
// Отримуємо джерело значення
var valueSource = DependencyPropertyHelper.GetValueSource(textBlock, TextBlock.FontSizeProperty);
Console.WriteLine($"FontSize джерело: {valueSource.BaseValueSource}");
// Очікуваний вивід: Inherited
}
Завдання: Створіть інтерактивний демонстратор системи пріоритетів.
Вимоги:
DependencyPropertyHelper.GetValueSource)ClearValue() та запуску анімаціїПідказка: Використовуйте DoubleAnimation для демонстрації найвищого пріоритету:
var animation = new DoubleAnimation
{
From = 1.0,
To = 0.3,
Duration = TimeSpan.FromSeconds(2),
AutoReverse = true,
RepeatBehavior = RepeatBehavior.Forever
};
myButton.BeginAnimation(UIElement.OpacityProperty, animation);
У цій статті ми розібрали фундаментальну концепцію WPF — Dependency Property System:
UI/UX принципи десктопних застосунків
Фундаментальні принципи проєктування користувацьких інтерфейсів для десктопних застосунків. Розуміємо різницю між UI та UX, вивчаємо закони взаємодії, принципи візуальної ієрархії, типографіки, кольору та доступності. Застосовуємо теорію на практиці через WPF та Avalonia.
Avalonia Property System — StyledProperty та DirectProperty
Система властивостей Avalonia — еволюція WPF DependencyProperty з покращеннями продуктивності та типобезпеки