Уявіть, що вам потрібно створити форму редагування профілю користувача з 50 полями: ім'я, прізвище, email, телефон, адреса, дата народження, біографія, налаштування приватності, і так далі. Традиційний підхід виглядає так:
// Завантаження даних у форму
private void LoadUser(User user)
{
txtFirstName.Text = user.FirstName;
txtLastName.Text = user.LastName;
txtEmail.Text = user.Email;
txtPhone.Text = user.Phone;
// ... ще 46 рядків
}
// Збереження даних з форми
private void SaveUser()
{
user.FirstName = txtFirstName.Text;
user.LastName = txtLastName.Text;
user.Email = txtEmail.Text;
user.Phone = txtPhone.Phone;
// ... ще 46 рядків
}
// Обробка змін для кожного поля
private void txtFirstName_TextChanged(object sender, TextChangedEventArgs e)
{
user.FirstName = txtFirstName.Text;
UpdateUI();
}
private void txtLastName_TextChanged(object sender, TextChangedEventArgs e)
{
user.LastName = txtLastName.Text;
UpdateUI();
}
// ... ще 48 обробників
Підрахунок: 50 полів × 3 методи (Load, Save, TextChanged) = 150+ рядків коду тільки для синхронізації UI з даними. І це без валідації, форматування, обробки помилок!
А тепер уявіть, що весь цей код можна замінити на:
<TextBox Text="{Binding FirstName}"/>
<TextBox Text="{Binding LastName}"/>
<TextBox Text="{Binding Email}"/>
<!-- ... ще 47 рядків XAML -->
Підрахунок: 50 рядків XAML. Без жодного рядка C#-коду для синхронізації. Це — Data Binding.
Перш ніж зануритися у Data Binding, розберемо детально, чому традиційний підхід через code-behind стає неконтрольованим у реальних проєктах.
Розглянемо просту форму редагування контакту:
<StackPanel Margin="20">
<TextBlock Text="Ім'я:"/>
<TextBox x:Name="txtFirstName"/>
<TextBlock Text="Прізвище:"/>
<TextBox x:Name="txtLastName"/>
<TextBlock Text="Email:"/>
<TextBox x:Name="txtEmail"/>
<TextBlock Text="Повне ім'я:"/>
<TextBlock x:Name="lblFullName" FontWeight="Bold"/>
<Button Content="Зберегти" Click="SaveButton_Click"/>
</StackPanel>
Code-behind для цієї форми:
public partial class ContactForm : Window
{
private Contact _contact;
public ContactForm(Contact contact)
{
InitializeComponent();
_contact = contact;
LoadContact();
}
// 1️⃣ Завантаження даних у UI
private void LoadContact()
{
txtFirstName.Text = _contact.FirstName ?? string.Empty;
txtLastName.Text = _contact.LastName ?? string.Empty;
txtEmail.Text = _contact.Email ?? string.Empty;
UpdateFullName();
}
// 2️⃣ Оновлення обчислюваних полів
private void UpdateFullName()
{
lblFullName.Text = $"{txtFirstName.Text} {txtLastName.Text}";
}
// 3️⃣ Обробка змін для кожного поля
private void txtFirstName_TextChanged(object sender, TextChangedEventArgs e)
{
_contact.FirstName = txtFirstName.Text;
UpdateFullName();
}
private void txtLastName_TextChanged(object sender, TextChangedEventArgs e)
{
_contact.LastName = txtLastName.Text;
UpdateFullName();
}
private void txtEmail_TextChanged(object sender, TextChangedEventArgs e)
{
_contact.Email = txtEmail.Text;
}
// 4️⃣ Збереження
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
// Дані вже в _contact завдяки TextChanged
_contactRepository.Save(_contact);
Close();
}
}
Підрахунок: Для 3 полів — ~40 рядків коду. Для 50 полів — ~600+ рядків.
🍝 Spaghetti Code
LoadContact(), UpdateFullName(), TextChanged обробниками та SaveButton_Click. Важко зрозуміти потік даних.🔗 Тісна зв'язність
txtFirstName.Text) змішаний з бізнес-логікою (_contact.FirstName). Неможливо тестувати логіку без UI.📝 Дублювання
TextChanged → оновити модель → оновити залежні поля.🐛 Помилки синхронізації
UpdateFullName() після зміни FirstName. Або забути оновити UI після зміни моделі з коду.🧪 Нетестовність
UpdateFullName() без створення Window? Як перевірити, що FirstName правильно оновлюється?🔄 Складність підтримки
Data Binding у WPF — це декларативний механізм автоматичної синхронізації між UI-властивостями (Target) та властивостями об'єктів даних (Source).
Замість імперативного коду:
txtFirstName.Text = contact.FirstName; // UI ← Дані
contact.FirstName = txtFirstName.Text; // Дані ← UI
Ви пишете декларативний XAML:
<TextBox Text="{Binding FirstName}"/>
І WPF автоматично синхронізує TextBox.Text з contact.FirstName в обох напрямках.
<TextBox Text="{Binding Path=FirstName, Mode=TwoWay}"/>
Розберемо по частинах:
| Частина | Опис | Обов'язкова? |
|---|---|---|
{Binding ...} | Markup Extension — синтаксис для створення об'єкта Binding | ✅ Так |
Path=FirstName | Шлях до властивості у джерелі даних (Source) | ✅ Так |
Mode=TwoWay | Режим синхронізації (OneWay, TwoWay, OneTime, OneWayToSource) | ❌ Ні (є default) |
{Binding FirstName} еквівалентний {Binding Path=FirstName}. Якщо Path — перший параметр, його можна опустити.DependencyProperty UI-елемента)DataContext — це властивість кожного FrameworkElement, що визначає джерело даних за замовчуванням для всіх Binding-виразів у цьому елементі та його дочірніх елементах.
Ключова особливість DataContext — він успадковується по Logical Tree. Якщо елемент не має власного DataContext, він використовує DataContext батьківського елемента.
Найпростіший спосіб — у code-behind конструктора:
public partial class ContactForm : Window
{
public ContactForm()
{
InitializeComponent();
// Створюємо об'єкт даних
var contact = new Contact
{
FirstName = "Іван",
LastName = "Петренко",
Email = "ivan@example.com"
};
// Встановлюємо DataContext для всього вікна
DataContext = contact;
}
}
Тепер всі Binding-вирази у цьому вікні будуть використовувати contact як Source:
<StackPanel Margin="20">
<!-- Binding шукає FirstName у DataContext (contact) -->
<TextBox Text="{Binding FirstName}"/>
<!-- Binding шукає LastName у DataContext (contact) -->
<TextBox Text="{Binding LastName}"/>
<!-- Binding шукає Email у DataContext (contact) -->
<TextBox Text="{Binding Email}"/>
</StackPanel>
Коли WPF зустрічає {Binding FirstName}, він виконує наступний алгоритм:
Чи має TextBox власний DataContext? Якщо так — використати його.
Якщо ні — піднятися до батьківського елемента (StackPanel) та перевірити його DataContext.
Продовжувати підйом до Window, потім до Application.
Якщо DataContext не знайдено на жодному рівні — Binding не працює (помилка у Output Window).
DataContext — це як "контекст, що тече по трубах". Ви наливаєте воду (дані) у верхню трубу (Window.DataContext), і вона автоматично тече до всіх нижчих труб (дочірніх елементів).Binding підтримує кілька режимів синхронізації між Source та Target.
| Mode | Напрямок | Коли оновлюється Target? | Коли оновлюється Source? | Use Case |
|---|---|---|---|---|
OneWay | Source → Target | При зміні Source | Ніколи | Відображення даних (Label, TextBlock) |
TwoWay | Source ↔ Target | При зміні Source | При зміні Target | Редагування даних (TextBox, CheckBox) |
OneTime | Source → Target | Один раз при ініціалізації | Ніколи | Статичні дані (константи) |
OneWayToSource | Target → Source | Ніколи | При зміні Target | Рідкісний (наприклад, Slider → ViewModel) |
Default | Залежить від Target | Залежить від Target | Залежить від Target | Автоматичний вибір |
Найпростіший режим — дані течуть тільки від Source до Target.
<!-- TextBlock завжди показує актуальне значення FirstName -->
<TextBlock Text="{Binding FirstName, Mode=OneWay}"/>
Коли використовувати:
Дані течуть в обох напрямках — зміни у Source оновлюють Target, зміни у Target оновлюють Source.
<!-- Користувач редагує → оновлюється Source -->
<!-- Source змінюється з коду → оновлюється TextBox -->
<TextBox Text="{Binding FirstName, Mode=TwoWay}"/>
Коли використовувати:
TextBox.Text за замовчуванням TwoWay, TextBlock.Text — OneWay. Тому часто Mode можна опустити.Дані завантажуються один раз при ініціалізації та більше не оновлюються.
<!-- Завантажується один раз при створенні вікна -->
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>
Коли використовувати:
Рідкісний режим — дані течуть тільки від Target до Source.
<!-- Slider оновлює Source, але Source не оновлює Slider -->
<Slider Value="{Binding Volume, Mode=OneWayToSource}"/>
Коли використовувати:
TwoWay кращий вибірСтворимо повноцінний приклад з Data Binding.
// POCO = Plain Old CLR Object (звичайний C#-клас)
public class Contact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
}
INotifyPropertyChanged. Це означає, що Binding працює тільки в один бік — від UI до моделі. Зміни моделі з коду не оновлять UI. Це виправимо у Part 2.<Window x:Class="DataBindingDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Контакт" Width="400" Height="300">
<StackPanel Margin="20">
<TextBlock Text="Ім'я:" Margin="0,0,0,5"/>
<TextBox Text="{Binding FirstName}" Margin="0,0,0,10"/>
<TextBlock Text="Прізвище:" Margin="0,0,0,5"/>
<TextBox Text="{Binding LastName}" Margin="0,0,0,10"/>
<TextBlock Text="Email:" Margin="0,0,0,5"/>
<TextBox Text="{Binding Email}" Margin="0,0,0,10"/>
<TextBlock Text="Телефон:" Margin="0,0,0,5"/>
<TextBox Text="{Binding Phone}" Margin="0,0,0,10"/>
<Button Content="Показати дані" Click="ShowData_Click"/>
</StackPanel>
</Window>
public partial class MainWindow : Window
{
private Contact _contact;
public MainWindow()
{
InitializeComponent();
// Створюємо контакт з початковими даними
_contact = new Contact
{
FirstName = "Іван",
LastName = "Петренко",
Email = "ivan@example.com",
Phone = "+380501234567"
};
// Встановлюємо DataContext
DataContext = _contact;
}
private void ShowData_Click(object sender, RoutedEventArgs e)
{
// Дані автоматично оновлені завдяки TwoWay Binding!
string message = $"Ім'я: {_contact.FirstName}\n" +
$"Прізвище: {_contact.LastName}\n" +
$"Email: {_contact.Email}\n" +
$"Телефон: {_contact.Phone}";
MessageBox.Show(message, "Дані контакту");
}
}
Contact з початковими данимиDataContext = _contact — всі Binding тепер "бачать" цей об'єктFirstName, LastName, Email, Phone з _contactTextBoxTwoWay Binding (default для TextBox.Text)_contact.FirstName, _contact.LastName, тощо_contact.FirstName — він вже оновлений!Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="Ім'я:"/>
<TextBox Text="Іван"/>
<TextBlock Text="Прізвище:"/>
<TextBox Text="Петренко"/>
<TextBlock Text="Email:"/>
<TextBox Text="ivan@example.com"/>
<Button Content="Показати дані" Command="{Binding ShowMessageCommand}" CommandParameter="Дані оновлені через Binding!"/>
</StackPanel>
Порівняємо обсяг коду для тієї ж форми.
// Завантаження: 4 рядки
txtFirstName.Text = _contact.FirstName;
txtLastName.Text = _contact.LastName;
txtEmail.Text = _contact.Email;
txtPhone.Text = _contact.Phone;
// Збереження: 4 рядки
_contact.FirstName = txtFirstName.Text;
_contact.LastName = txtLastName.Text;
_contact.Email = txtEmail.Text;
_contact.Phone = txtPhone.Text;
// TextChanged обробники: 4 × 3 = 12 рядків
private void txtFirstName_TextChanged(object sender, TextChangedEventArgs e)
{
_contact.FirstName = txtFirstName.Text;
}
// ... ще 3 обробники
// Підсумок: ~20 рядків C# для 4 полів
// Встановлення DataContext: 1 рядок
DataContext = _contact;
// Підсумок: 1 рядок C# для 4 полів (і для 50 полів теж 1 рядок!)
<!-- XAML: 4 рядки -->
<TextBox Text="{Binding FirstName}"/>
<TextBox Text="{Binding LastName}"/>
<TextBox Text="{Binding Email}"/>
<TextBox Text="{Binding Phone}"/>
| Метрика | Code-Behind | Data Binding | Економія |
|---|---|---|---|
| Рядків C# | ~20 | 1 | 95% |
| Рядків XAML | 4 | 4 | 0% |
| Обробників подій | 4 | 0 | 100% |
| Ручна синхронізація | Так | Ні | ✅ |
| Ризик помилок | Високий | Низький | ✅ |
| Тестовність | Низька | Висока | ✅ |
Закріпимо знання через реальні сценарії.
Створимо форму реєстрації користувача з валідацією довжини пароля.
Модель:
public class RegistrationData
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public bool AcceptTerms { get; set; }
}
XAML:
<Window x:Class="BindingDemo.RegistrationWindow"
Title="Реєстрація" Width="400" Height="350">
<StackPanel Margin="20">
<TextBlock Text="Реєстрація нового користувача"
FontSize="18"
FontWeight="Bold"
Margin="0,0,0,20"/>
<TextBlock Text="Ім'я користувача:" Margin="0,0,0,5"/>
<TextBox Text="{Binding Username}" Margin="0,0,0,10"/>
<TextBlock Text="Email:" Margin="0,0,0,5"/>
<TextBox Text="{Binding Email}" Margin="0,0,0,10"/>
<TextBlock Text="Пароль:" Margin="0,0,0,5"/>
<PasswordBox x:Name="passwordBox" Margin="0,0,0,10"/>
<CheckBox Content="Я приймаю умови використання"
IsChecked="{Binding AcceptTerms}"
Margin="0,0,0,20"/>
<Button Content="Зареєструватися"
Click="Register_Click"
IsEnabled="{Binding AcceptTerms}"/>
</StackPanel>
</Window>
Code-Behind:
public partial class RegistrationWindow : Window
{
private RegistrationData _data;
public RegistrationWindow()
{
InitializeComponent();
_data = new RegistrationData();
DataContext = _data;
}
private void Register_Click(object sender, RoutedEventArgs e)
{
// Дані вже в _data завдяки Binding
if (string.IsNullOrWhiteSpace(_data.Username))
{
MessageBox.Show("Введіть ім'я користувача!");
return;
}
if (passwordBox.Password.Length < 6)
{
MessageBox.Show("Пароль має бути не менше 6 символів!");
return;
}
MessageBox.Show($"Користувач {_data.Username} зареєстрований!");
Close();
}
}
PasswordBox.Passwordне є DependencyProperty з міркувань безпеки, тому не підтримує Binding. Доступ тільки через code-behind або через Attached Property (просунута техніка).Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="Реєстрація" FontSize="18" FontWeight="Bold"/>
<TextBlock Text="Ім'я користувача:"/>
<TextBox Text="john_doe"/>
<TextBlock Text="Email:"/>
<TextBox Text="john@example.com"/>
<CheckBox Content="Я приймаю умови використання" IsChecked="True"/>
<Button Content="Зареєструватися" Command="{Binding ShowMessageCommand}" CommandParameter="Реєстрація успішна!"/>
</StackPanel>
Створимо простий калькулятор, де результат автоматично оновлюється при зміні операндів.
Модель:
public class CalculatorData
{
public double Number1 { get; set; }
public double Number2 { get; set; }
// Обчислювана властивість (поки без INPC — не оновлюється автоматично)
public double Result => Number1 + Number2;
}
XAML:
<StackPanel Margin="20">
<TextBlock Text="Простий калькулятор" FontSize="18" FontWeight="Bold" Margin="0,0,0,20"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Число 1:" Grid.Row="0" Grid.Column="0" Margin="0,0,10,10" VerticalAlignment="Center"/>
<TextBox Text="{Binding Number1}" Grid.Row="0" Grid.Column="1" Margin="0,0,0,10"/>
<TextBlock Text="Число 2:" Grid.Row="1" Grid.Column="0" Margin="0,0,10,10" VerticalAlignment="Center"/>
<TextBox Text="{Binding Number2}" Grid.Row="1" Grid.Column="1" Margin="0,0,0,10"/>
<TextBlock Text="Результат:" Grid.Row="2" Grid.Column="0" Margin="0,0,10,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Result}" Grid.Row="2" Grid.Column="1" FontWeight="Bold" FontSize="16"/>
</Grid>
<Button Content="Оновити результат" Click="Update_Click" Margin="0,20,0,0"/>
</StackPanel>
Code-Behind:
public partial class CalculatorWindow : Window
{
private CalculatorData _data;
public CalculatorWindow()
{
InitializeComponent();
_data = new CalculatorData { Number1 = 5, Number2 = 3 };
DataContext = _data;
}
private void Update_Click(object sender, RoutedEventArgs e)
{
// Примусово оновлюємо DataContext, щоб Result перерахувався
// (У Part 2 з INPC це буде автоматично)
DataContext = null;
DataContext = _data;
}
}
Result не оновлюється автоматично при зміні Number1 або Number2. Потрібна кнопка "Оновити". У Part 2 з INotifyPropertyChanged це буде працювати автоматично.Створимо простий список завдань з можливістю позначати виконані.
Модель:
public class TodoItem
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
}
XAML:
<Window x:Class="BindingDemo.TodoWindow"
Title="Список завдань" Width="400" Height="400">
<DockPanel Margin="20">
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,10">
<TextBlock Text="Нове завдання:" Margin="0,0,0,5"/>
<DockPanel>
<Button Content="Додати"
DockPanel.Dock="Right"
Click="Add_Click"
Margin="10,0,0,0"
Padding="10,5"/>
<TextBox x:Name="txtNewTask"/>
</DockPanel>
</StackPanel>
<ListBox x:Name="lstTasks" DockPanel.Dock="Top">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Title}"
IsChecked="{Binding IsCompleted}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
Code-Behind:
using System.Collections.ObjectModel;
public partial class TodoWindow : Window
{
private ObservableCollection<TodoItem> _tasks;
public TodoWindow()
{
InitializeComponent();
// ObservableCollection автоматично повідомляє UI про додавання/видалення
_tasks = new ObservableCollection<TodoItem>
{
new TodoItem { Title = "Вивчити Data Binding", IsCompleted = true },
new TodoItem { Title = "Створити проєкт", IsCompleted = false },
new TodoItem { Title = "Написати тести", IsCompleted = false }
};
lstTasks.ItemsSource = _tasks;
}
private void Add_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(txtNewTask.Text))
{
_tasks.Add(new TodoItem { Title = txtNewTask.Text, IsCompleted = false });
txtNewTask.Clear();
}
}
}
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<DockPanel Margin="20">
<StackPanel DockPanel.Dock="Top" Spacing="10" Margin="0,0,0,20">
<TextBlock Text="Список завдань" FontSize="18" FontWeight="Bold"/>
</StackPanel>
<StackPanel Spacing="5">
<CheckBox Content="Вивчити Data Binding" IsChecked="True"/>
<CheckBox Content="Створити проєкт" IsChecked="False"/>
<CheckBox Content="Написати тести" IsChecked="False"/>
</StackPanel>
</DockPanel>
Завдання: Створіть форму профілю користувача з Data Binding.
public class UserProfile
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public string City { get; set; }
}
<StackPanel Margin="20">
<TextBlock Text="Ім'я:"/>
<TextBox Text="{Binding FirstName}"/>
<!-- TODO: Додайте поля для LastName, Age, City -->
<Button Content="Показати профіль" Click="Show_Click"/>
</StackPanel>
public MainWindow()
{
InitializeComponent();
var profile = new UserProfile
{
FirstName = "Олена",
LastName = "Коваленко",
Age = 25,
City = "Київ"
};
DataContext = profile;
}
Очікуваний результат: При запуску форма заповнена даними. При редагуванні та натисканні кнопки — MessageBox показує оновлені дані.
Завдання: Створіть форму, що демонструє різницю між OneWay та TwoWay.
Вимоги:
FirstName)Mode=OneWay (тільки читання з моделі)Mode=TwoWay (редагування)FirstName у моделіПідказка:
<StackPanel Margin="20">
<TextBlock Text="OneWay (тільки читання):"/>
<TextBox Text="{Binding FirstName, Mode=OneWay}" IsReadOnly="True" Background="LightGray"/>
<TextBlock Text="TwoWay (редагування):"/>
<TextBox Text="{Binding FirstName, Mode=TwoWay}"/>
<Button Content="Змінити з коду" Click="ChangeFromCode_Click"/>
<Button Content="Оновити UI" Click="RefreshUI_Click"/>
</StackPanel>
Очікувана поведінка:
Завдання: Створіть дві ідентичні форми — одну через Code-Behind, іншу через Data Binding. Порахуйте рядки коду.
Вимоги:
Форма 1 (Code-Behind):
// Завантаження
private void LoadData()
{
txtFirstName.Text = user.FirstName;
txtLastName.Text = user.LastName;
// ... ще 8 полів
}
// Збереження
private void SaveData()
{
user.FirstName = txtFirstName.Text;
user.LastName = txtLastName.Text;
// ... ще 8 полів
}
// Очищення
private void ClearData()
{
txtFirstName.Clear();
txtLastName.Clear();
// ... ще 8 полів
}
Форма 2 (Data Binding):
// Завантаження
DataContext = user;
// Збереження
// Дані вже в user!
// Очищення
DataContext = new User();
Підрахунок:
| Метрика | Code-Behind | Data Binding |
|---|---|---|
| Рядків C# | ? | ? |
| Рядків XAML | ? | ? |
| Обробників подій | ? | ? |
На цьому етапі ми використовували звичайні C#-класи (POCO) без INotifyPropertyChanged. Це створює кілька обмежень:
❌ Односторонній зв'язок
DataContext.❌ Обчислювані властивості
FullName = FirstName + LastName не оновлюється при зміні FirstName. Потрібна кнопка "Оновити".❌ Складні сценарії
INotifyPropertyChanged — інтерфейс, що дозволяє моделі повідомляти UI про зміни. Це відкриє двері до повноцінного MVVM.У цій статті ми розібрали основи Data Binding:
INotifyPropertyChangedRouted Events — Маршрутизація подій у WPF
Розуміння системи подій WPF — Tunneling, Bubbling та Direct routing для складних UI-ієрархій
INotifyPropertyChanged — Живе оновлення UI
Вирішення проблеми односторонньої синхронізації через INotifyPropertyChanged — інтерфейс, що дозволяє моделі повідомляти UI про зміни