Якщо ви щойно опанували Dependency Properties у WPF, у вас може виникнути питання: "Чи працює це так само в Avalonia?" Відповідь — майже так само, але краще.
Avalonia успадкувала концепцію системи властивостей від WPF, але переосмислила її з урахуванням сучасних вимог:
Avalonia зберегла фундаментальні концепції WPF:
✅ Централізоване сховище
✅ Система пріоритетів
✅ Метадані та callbacks
✅ Attached Properties
Grid.Row, Canvas.Left — працюють так само.🆕 Два типи властивостей
🆕 Generic API
AvaloniaProperty.Register<TOwner, TValue>() — типобезпечна реєстрація без boxing.🆕 Compile-time перевірки
🆕 Оптимізація пам'яті
StyledProperty — це основний тип властивостей в Avalonia, що підтримує стилізацію, binding, анімації та всі інші можливості системи властивостей.
Порівняємо синтаксис WPF та Avalonia:
public class MyButton : Button
{
// Статичне поле
public static readonly DependencyProperty TitleProperty =
DependencyProperty.Register(
"Title", // Назва як рядок ⚠️
typeof(string), // Тип як Type
typeof(MyButton), // Owner як Type
new PropertyMetadata("Default")
);
// CLR wrapper
public string Title
{
get => (string)GetValue(TitleProperty); // Casting ⚠️
set => SetValue(TitleProperty, value);
}
}
public class MyButton : Button
{
// Статичне поле з generic API
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<MyButton, string>(
nameof(Title), // nameof — безпечно ✅
defaultValue: "Default"
);
// CLR wrapper
public string Title
{
get => GetValue(TitleProperty); // Без casting ✅
set => SetValue(TitleProperty, value);
}
}
| Аспект | WPF DependencyProperty | Avalonia StyledProperty |
|---|---|---|
| Реєстрація | DependencyProperty.Register() | AvaloniaProperty.Register<>() |
| Типобезпека | ❌ Runtime casting | ✅ Compile-time generics |
| Назва властивості | Рядок (помилки в runtime) | nameof() (помилки в compile-time) |
| Повернення значення | object → потрібен cast | T → без casting |
| Метадані | PropertyMetadata | Inline параметри або StyledPropertyMetadata<T> |
"Titel" замість "Title") виявиться лише при запуску програми. В Avalonia компілятор відразу покаже помилку завдяки nameof(Title).DirectProperty — унікальна особливість Avalonia, якої немає у WPF. Це "легка" властивість без централізованого сховища значень.
Розглянемо проблему:
// У WPF/Avalonia StyledProperty:
public static readonly StyledProperty<Point> MousePositionProperty = ...
public Point MousePosition
{
get => GetValue(MousePositionProperty); // Читання зі сховища
set => SetValue(MousePositionProperty, value); // Запис у сховище
}
// Проблема: MousePosition змінюється 60+ разів на секунду (при русі миші)
// Кожна зміна = запис у сховище + PropertyChanged event + можлива інвалідація Layout/Render
Overhead StyledProperty для часто змінюваних значень:
AffectsMeasure)DirectProperty — це "пряме" звернення до backing field, без сховища:
public class MyControl : Control
{
// Backing field — звичайне поле класу
private Point _mousePosition;
// DirectProperty — без сховища
public static readonly DirectProperty<MyControl, Point> MousePositionProperty =
AvaloniaProperty.RegisterDirect<MyControl, Point>(
nameof(MousePosition),
o => o._mousePosition, // Getter delegate
(o, v) => o._mousePosition = v // Setter delegate
);
public Point MousePosition
{
get => _mousePosition;
set => SetAndRaise(MousePositionProperty, ref _mousePosition, value);
}
}
StyledProperty
Використання:
Переваги:
Недоліки:
DirectProperty
Використання:
Переваги:
Недоліки:
StyledProperty. DirectProperty — це оптимізація для специфічних випадків, коли ви точно знаєте, що стилізація та анімації не потрібні.Розглянемо, як Avalonia використовує обидва типи властивостей у своїх контролах.
// Avalonia.Controls.Button
public class Button : ContentControl
{
public static readonly StyledProperty<object?> ContentProperty =
ContentControl.ContentProperty.AddOwner<Button>();
public object? Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
}
Чому StyledProperty?
Content може бути стилізований через <Style Selector="Button">Content="{Binding Title}"// Avalonia.Controls.Control
public class Control : InputElement
{
private Rect _bounds;
public static readonly DirectProperty<Control, Rect> BoundsProperty =
AvaloniaProperty.RegisterDirect<Control, Rect>(
nameof(Bounds),
o => o._bounds
);
public Rect Bounds
{
get => _bounds;
private set => SetAndRaise(BoundsProperty, ref _bounds, value);
}
}
Чому DirectProperty?
Bounds змінюється при кожному Layout pass (часто)| Аспект | WPF DependencyProperty | Avalonia StyledProperty | Avalonia DirectProperty |
|---|---|---|---|
| Стилізація | ✅ Так | ✅ Так | ❌ Ні |
| Binding | ✅ Так | ✅ Так | ✅ Так (обмежено) |
| Анімації | ✅ Так | ✅ Так | ❌ Ні |
| Property Inheritance | ✅ Так | ✅ Так | ❌ Ні |
| Типобезпека | ❌ Runtime | ✅ Compile-time | ✅ Compile-time |
| Продуктивність | Середня | Середня | Висока |
| Overhead пам'яті | Середній | Середній | Мінімальний |
| Використання | Універсальне | Універсальне | Специфічне |
Це перша доза матеріалу. Наступна доза буде про:
Продовжити?
Avalonia успадкувала концепцію системи пріоритетів від WPF, але спростила її для кращої передбачуваності та продуктивності.
У WPF система пріоритетів має 11 рівнів. Avalonia скоротила це до 6 основних рівнів:
| Пріоритет | Avalonia | WPF Еквівалент | Відмінності |
|---|---|---|---|
| 1 | Animation | Animation | ✅ Ідентично |
| 2 | Local Value | Local Value | ✅ Ідентично |
| 3 | Style (Triggers + Setters) | Style Triggers → Setters | 🔄 Об'єднано в один рівень |
| 4 | Template | Template Triggers → Setters | 🔄 Об'єднано в один рівень |
| 5 | Inherited | Inherited | ✅ Ідентично |
| 6 | Default | Default | ✅ Ідентично |
<!-- Avalonia XAML -->
<Window xmlns="https://github.com/avaloniaui"
FontSize="16"> <!-- Inherited value -->
<Window.Styles>
<Style Selector="Button">
<Setter Property="Background" Value="Blue"/> <!-- Style value -->
</Style>
</Window.Styles>
<StackPanel Spacing="10" Margin="20">
<!-- Кнопка 1: Style value (Blue) -->
<Button Content="Стиль"/>
<!-- Кнопка 2: Local value (Red) перемагає Style -->
<Button Content="Локальне" Background="Red"/>
<!-- Кнопка 3: FontSize успадковується від Window (16) -->
<Button Content="Успадкований шрифт"/>
</StackPanel>
</Window>
Attached Properties працюють в Avalonia майже ідентично до WPF, але з типобезпечним API.
public class Grid : Panel
{
public static readonly DependencyProperty RowProperty =
DependencyProperty.RegisterAttached(
"Row",
typeof(int),
typeof(Grid),
new PropertyMetadata(0)
);
public static void SetRow(DependencyObject obj, int value)
{
obj.SetValue(RowProperty, value);
}
public static int GetRow(DependencyObject obj)
{
return (int)obj.GetValue(RowProperty);
}
}
public class Grid : Panel
{
public static readonly AttachedProperty<int> RowProperty =
AvaloniaProperty.RegisterAttached<Grid, Control, int>(
"Row",
defaultValue: 0
);
public static void SetRow(Control element, int value)
{
element.SetValue(RowProperty, value);
}
public static int GetRow(Control element)
{
return element.GetValue(RowProperty);
}
}
| Аспект | WPF | Avalonia |
|---|---|---|
| Тип повернення | DependencyProperty | AttachedProperty<T> |
| Реєстрація | RegisterAttached() | RegisterAttached<TOwner, THost, TValue>() |
| Типобезпека | ❌ Casting у Get/Set | ✅ Generic параметри |
| Параметр методів | DependencyObject (базовий) | Control (конкретний тип) |
Grid.Row має сенс тільки для Control, а не для будь-якого AvaloniaObject.Синтаксис ідентичний:
<!-- WPF та Avalonia — однаковий XAML -->
<Grid>
<Button Grid.Row="0" Grid.Column="1" Content="Кнопка"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Текст"/>
</Grid>
AddOwner дозволяє одному класу "перевикористати" властивість іншого класу, змінивши метадані або default value.
// ContentControl визначає ContentProperty
public class ContentControl : TemplatedControl
{
public static readonly StyledProperty<object?> ContentProperty =
AvaloniaProperty.Register<ContentControl, object?>(
nameof(Content)
);
public object? Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
}
// Button "додає себе як власника" ContentProperty
public class Button : ContentControl
{
// AddOwner створює нову StyledProperty<object?> для Button
public static readonly StyledProperty<object?> ContentProperty =
ContentControl.ContentProperty.AddOwner<Button>();
// Тепер Button.ContentProperty та ContentControl.ContentProperty
// вказують на ту саму властивість у системі
}
🔄 Перевикористання логіки
Button автоматично отримує всю логіку ContentProperty від ContentControl.🎨 Зміна метаданих
📦 Організація коду
public class MyButton : Button
{
// Перевизначаємо default value для Content
public static readonly StyledProperty<object?> ContentProperty =
Button.ContentProperty.AddOwner<MyButton>(
new StyledPropertyMetadata<object?>(
defaultValue: "Click Me!" // Новий default
)
);
}
Закріпимо знання через код.
Створимо простий контрол з кастомною властивістю:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
public class RatingControl : Control
{
// StyledProperty для рейтингу (0-5)
public static readonly StyledProperty<int> RatingProperty =
AvaloniaProperty.Register<RatingControl, int>(
nameof(Rating),
defaultValue: 0,
validate: ValidateRating,
coerce: CoerceRating
);
public int Rating
{
get => GetValue(RatingProperty);
set => SetValue(RatingProperty, value);
}
// Validation: рейтинг має бути від 0 до 5
private static bool ValidateRating(int value)
{
return value >= 0 && value <= 5;
}
// Coercion: обмежуємо значення діапазоном
private static int CoerceRating(AvaloniaObject sender, int value)
{
return Math.Clamp(value, 0, 5);
}
// PropertyChanged callback
static RatingControl()
{
RatingProperty.Changed.AddClassHandler<RatingControl>(
(control, args) =>
{
// Реакція на зміну рейтингу
control.InvalidateVisual(); // Перемалювати контрол
}
);
}
}
PropertyMetadata. В Avalonia вони реєструються окремо через Property.Changed.AddClassHandler<>() — це дає більше гнучкості.Створимо властивість для відстеження стану наведення миші:
public class InteractiveControl : Control
{
// Backing field
private bool _isHovered;
// DirectProperty — без сховища
public static readonly DirectProperty<InteractiveControl, bool> IsHoveredProperty =
AvaloniaProperty.RegisterDirect<InteractiveControl, bool>(
nameof(IsHovered),
o => o._isHovered
);
public bool IsHovered
{
get => _isHovered;
private set => SetAndRaise(IsHoveredProperty, ref _isHovered, value);
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
IsHovered = true; // Часта зміна — DirectProperty ефективніший
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
IsHovered = false;
}
}
Створимо attached property для додавання підказок до елементів:
public class Annotations
{
// AttachedProperty для тексту підказки
public static readonly AttachedProperty<string?> HintProperty =
AvaloniaProperty.RegisterAttached<Annotations, Control, string?>(
"Hint",
defaultValue: null
);
public static void SetHint(Control element, string? value)
{
element.SetValue(HintProperty, value);
}
public static string? GetHint(Control element)
{
return element.GetValue(HintProperty);
}
}
Використання у XAML:
<Window xmlns:local="using:MyApp">
<StackPanel Spacing="10" Margin="20">
<TextBox local:Annotations.Hint="Введіть ваше ім'я"/>
<Button local:Annotations.Hint="Натисніть для відправки" Content="Відправити"/>
</StackPanel>
</Window>
Завдання: Портуйте наступний WPF DependencyProperty на Avalonia StyledProperty.
public class ProgressButton : Button
{
public static readonly DependencyProperty ProgressProperty =
DependencyProperty.Register(
"Progress",
typeof(double),
typeof(ProgressButton),
new PropertyMetadata(0.0, OnProgressChanged)
);
public double Progress
{
get => (double)GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
private static void OnProgressChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var button = (ProgressButton)d;
// Логіка оновлення
}
}
// TODO: Портуйте код на Avalonia
Підказки:
AvaloniaProperty.Register<TOwner, TValue>()PropertyMetadata на inline параметриProperty.Changed.AddClassHandler<>()Завдання: Визначте, який тип властивості використати для кожного сценарію.
| Властивість | StyledProperty | DirectProperty | Обґрунтування |
|---|---|---|---|
Button.Background | ? | ? | ? |
Control.ActualWidth | ? | ? | ? |
TextBox.Text | ? | ? | ? |
Slider.Value | ? | ? | ? |
Control.IsPointerOver | ? | ? | ? |
Window.Title | ? | ? | ? |
| Властивість | Тип | Обґрунтування |
|---|---|---|
Button.Background | StyledProperty | Потребує стилізації та binding |
Control.ActualWidth | DirectProperty | Read-only, часто змінюється (Layout) |
TextBox.Text | StyledProperty | Потребує TwoWay binding |
Slider.Value | StyledProperty | Потребує binding та анімації |
Control.IsPointerOver | DirectProperty | Часто змінюється (pointer events), read-only |
Window.Title | StyledProperty | Потребує binding, рідко змінюється |
Завдання: Створіть повноцінний RatingControl з наступними вимогами:
Створіть три StyledProperty:
Rating (int, 0-5) — поточний рейтингMaxRating (int, default 5) — максимальний рейтингStarColor (IBrush, default Gold) — колір зірочокRating не може бути більше за MaxRatingMaxRating не може бути менше 1MaxRating — перевірити RatingПеревизначте Render() для малювання зірочок:
public override void Render(DrawingContext context)
{
base.Render(context);
for (int i = 0; i < MaxRating; i++)
{
var brush = i < Rating ? StarColor : Brushes.Gray;
// Малювання зірочки
}
}
Додайте можливість змінювати рейтинг кліком:
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
var position = e.GetPosition(this);
var starWidth = Bounds.Width / MaxRating;
var clickedStar = (int)(position.X / starWidth) + 1;
Rating = Math.Clamp(clickedStar, 0, MaxRating);
}
У цій статті ми розібрали систему властивостей Avalonia:
Grid.Row, Canvas.Left та створення власних attached properties.Dependency Properties — Концепція та Value Resolution
Розуміння системи властивостей WPF, механізму DependencyProperty та пріоритетів значень
Attached Properties — Властивості без меж
Розуміння механізму Attached Properties у WPF — як Grid.Row працює на Button, і як створювати власні attached properties