У попередніх статтях ми навчилися прив'язувати окремі властивості до UI:
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding Age}"/>
Але що, якщо у вас є колекція об'єктів — список контактів, завдань, повідомлень? Як відобразити їх у UI? Як зробити так, щоб при додаванні нового елемента у колекцію — UI автоматично оновлювався?
Спроба 1: List
public class MainViewModel
{
public List<Person> People { get; set; }
public MainViewModel()
{
People = new List<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко" },
new Person { FirstName = "Марія", LastName = "Коваленко" }
};
}
public void AddPerson()
{
People.Add(new Person { FirstName = "Олександр", LastName = "Шевченко" });
// ❌ UI не оновлюється!
}
}
<ListBox ItemsSource="{Binding People}"/>
Проблема: При запуску ListBox показує 2 елементи. Але коли ви викликаєте AddPerson() — третій елемент не з'являється. Чому?
Рішення: ObservableCollection
Розберемо детально, чому List<T> не працює для Data Binding.
ViewModel:
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class ContactsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private List<Person> _people;
public List<Person> People
{
get => _people;
set
{
_people = value;
OnPropertyChanged();
}
}
public ContactsViewModel()
{
People = new List<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко" },
new Person { FirstName = "Марія", LastName = "Коваленко" }
};
}
}
XAML:
<Window x:Class="MyApp.MainWindow"
DataContext="{Binding Source={StaticResource viewModel}}">
<StackPanel Margin="20">
<ListBox ItemsSource="{Binding People}" Height="200"/>
<Button Content="Додати особу" Click="AddPerson_Click" Margin="0,10,0,0"/>
</StackPanel>
</Window>
Code-Behind:
private void AddPerson_Click(object sender, RoutedEventArgs e)
{
var viewModel = (ContactsViewModel)DataContext;
viewModel.People.Add(new Person
{
FirstName = "Олександр",
LastName = "Шевченко"
});
// ❌ UI не оновлюється!
}
Результат: ListBox все ще показує тільки 2 елементи (Іван та Марія). Олександр не з'являється.
Проблема: List<T> — це звичайна колекція. Вона не має механізму повідомлення про зміни. Коли ви викликаєте Add(), Remove(), Clear() — ніхто не дізнається про це.
Чому OnPropertyChanged() не допомагає?
Спроба 2:
public void AddPerson()
{
People.Add(new Person { FirstName = "Олександр", LastName = "Шевченко" });
OnPropertyChanged(nameof(People)); // Спроба повідомити
}
Результат: Все одно не працює. Чому?
OnPropertyChanged(nameof(People)) повідомляє, що властивість People змінилася (тобто, що тепер People вказує на інший об'єкт). Але ми не змінювали сам об'єкт List<Person> — ми змінювали його вміст (додали елемент).
Аналогія: Уявіть, що People — це коробка з іграшками. OnPropertyChanged(nameof(People)) каже: "Я замінив коробку на іншу". Але ми не замінювали коробку — ми додали іграшку всередину тієї самої коробки. WPF не знає про це.
Можна замінити всю колекцію:
public void AddPerson()
{
var newList = new List<Person>(People);
newList.Add(new Person { FirstName = "Олександр", LastName = "Шевченко" });
People = newList; // Заміна всієї колекції
// ✅ Тепер UI оновлюється
}
Але це антипатерн:
❌ Неефективно
❌ Втрата стану
SelectedItem), позиція прокрутки, стан розгортання.❌ Повне перемалювання
❌ Не масштабується
ObservableCollection<T> — це спеціальна колекція з простору імен System.Collections.ObjectModel, що реалізує інтерфейс INotifyCollectionChanged.
Інтерфейс:
public interface INotifyCollectionChanged
{
event NotifyCollectionChangedEventHandler CollectionChanged;
}
public delegate void NotifyCollectionChangedEventHandler(
object sender,
NotifyCollectionChangedEventArgs e
);
Аналогія з INotifyPropertyChanged:
| Інтерфейс | Повідомляє про... | Подія |
|---|---|---|
INotifyPropertyChanged | Зміну властивості об'єкта | PropertyChanged |
INotifyCollectionChanged | Зміну вмісту колекції | CollectionChanged |
Для студентів зі слабким розумінням ООП — коротке нагадування.
Подія (Event) — це механізм сповіщення у C#. Об'єкт може "викликати" подію, а інші об'єкти можуть "підписатися" на неї.
// Клас з подією
public class ObservableCollection<T>
{
// Подія — список підписників
public event NotifyCollectionChangedEventHandler CollectionChanged;
public void Add(T item)
{
// Додаємо елемент
_items.Add(item);
// Викликаємо подію — повідомляємо всіх підписників
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(...));
}
}
// Підписник (WPF Binding Engine)
var collection = new ObservableCollection<Person>();
collection.CollectionChanged += (sender, e) =>
{
// Обробка зміни — оновлення UI
Console.WriteLine("Колекція змінилася!");
};
Аналогія: Подія — це як дзвінок. ObservableCollection дзвонить (викликає подію), а WPF Binding Engine відповідає (обробляє подію та оновлює UI).
Процес:
ItemsSource → Binding Engine підписується на CollectionChangedAdd() → ObservableCollection додає елемент та викликає подіюViewModel з ObservableCollection:
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
public class ContactsViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
// ObservableCollection замість List
public ObservableCollection<Person> People { get; set; }
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко" },
new Person { FirstName = "Марія", LastName = "Коваленко" }
};
}
}
XAML:
<Window x:Class="MyApp.MainWindow"
DataContext="{Binding Source={StaticResource viewModel}}">
<StackPanel Margin="20">
<ListBox ItemsSource="{Binding People}" Height="200"/>
<Button Content="Додати особу" Click="AddPerson_Click" Margin="0,10,0,0"/>
</StackPanel>
</Window>
Code-Behind:
private void AddPerson_Click(object sender, RoutedEventArgs e)
{
var viewModel = (ContactsViewModel)DataContext;
viewModel.People.Add(new Person
{
FirstName = "Олександр",
LastName = "Шевченко"
});
// ✅ UI автоматично оновлюється!
}
Результат: При кліку на кнопку — третій елемент (Олександр) з'являється у ListBox автоматично!
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<ListBox Height="150">
<ListBoxItem Content="Іван Петренко"/>
<ListBoxItem Content="Марія Коваленко"/>
<ListBoxItem Content="Олександр Шевченко"/>
</ListBox>
<Button Content="Додати особу"/>
<TextBlock Text="(У реальному WPF новий елемент з'являється при кліку на кнопку)"
FontSize="10"
Foreground="Gray"/>
</StackPanel>
ItemsControl — це базовий клас для всіх контролів, що відображають колекції.
Основні контроли:
HierarchicalDataTemplate)1. ItemsSource — джерело даних (колекція)
<ListBox ItemsSource="{Binding People}"/>
2. ItemTemplate — шаблон для кожного елемента
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
3. ItemsPanel — панель для розташування елементів
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<!-- Горизонтальний список замість вертикального -->
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
4. DisplayMemberPath — властивість для відображення (без DataTemplate)
<!-- Швидкий спосіб без DataTemplate -->
<ListBox ItemsSource="{Binding People}" DisplayMemberPath="FirstName"/>
Підхід 1: ToString() (за замовчуванням)
<ListBox ItemsSource="{Binding People}"/>
Результат: Кожен елемент показує результат ToString() — "MyApp.Models.Person".
Підхід 2: DisplayMemberPath
<ListBox ItemsSource="{Binding People}" DisplayMemberPath="FirstName"/>
Результат: Кожен елемент показує тільки FirstName — "Іван", "Марія".
Підхід 3: ItemTemplate (найгнучкіший)
<ListBox ItemsSource="{Binding People}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding FirstName}" FontWeight="Bold"/>
<TextBlock Text=" "/>
<TextBlock Text="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Результат: Кожен елемент показує "Іван Петренко" з форматуванням.
Підхід 4: Implicit DataTemplate (найкращий)
<Window.Resources>
<DataTemplate DataType="{x:Type local:Person}">
<Border Background="LightBlue" Padding="5" Margin="2">
<StackPanel>
<TextBlock Text="{Binding FirstName}" FontWeight="Bold"/>
<TextBlock Text="{Binding LastName}" FontSize="12"/>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<ListBox ItemsSource="{Binding People}"/>
Результат: Кожен елемент автоматично використовує DataTemplate — красива картка.
ObservableCollection<T> підтримує всі стандартні операції колекцій.
public void AddPerson()
{
People.Add(new Person
{
FirstName = "Новий",
LastName = "Користувач"
});
// UI автоматично оновлюється
}
Подія: CollectionChanged з Action = Add, NewItems = [new Person].
public void RemovePerson(Person person)
{
People.Remove(person);
// UI автоматично оновлюється
}
Подія: CollectionChanged з Action = Remove, OldItems = [person].
public void ClearAll()
{
People.Clear();
// UI автоматично оновлюється (всі елементи зникають)
}
Подія: CollectionChanged з Action = Reset.
public void InsertAtBeginning()
{
People.Insert(0, new Person
{
FirstName = "Перший",
LastName = "Елемент"
});
// UI автоматично оновлюється
}
Подія: CollectionChanged з Action = Add, NewStartingIndex = 0.
public void ReplacePerson(int index, Person newPerson)
{
People[index] = newPerson;
// UI автоматично оновлюється
}
Подія: CollectionChanged з Action = Replace, OldItems, NewItems.
public void MoveToTop(int index)
{
People.Move(index, 0);
// UI автоматично оновлюється
}
Подія: CollectionChanged з Action = Move, OldStartingIndex, NewStartingIndex.
| Операція | Метод | Подія CollectionChanged | UI Оновлення |
|---|---|---|---|
| Додавання | Add(item) | Action = Add | ✅ Додає елемент |
| Видалення | Remove(item) | Action = Remove | ✅ Видаляє елемент |
| Очищення | Clear() | Action = Reset | ✅ Видаляє всі |
| Вставка | Insert(index, item) | Action = Add | ✅ Вставляє на позицію |
| Заміна | this[index] = item | Action = Replace | ✅ Замінює елемент |
| Переміщення | Move(oldIndex, newIndex) | Action = Move | ✅ Переміщує елемент |
Selector (базовий клас для ListBox, ComboBox) має властивості для роботи з вибраним елементом.
ViewModel:
public class ContactsViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> People { get; set; }
private Person _selectedPerson;
public Person SelectedPerson
{
get => _selectedPerson;
set
{
_selectedPerson = value;
OnPropertyChanged();
}
}
// ... INotifyPropertyChanged implementation
}
XAML:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- Список -->
<ListBox Grid.Column="0"
ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"/>
<!-- Деталі вибраного елемента -->
<Border Grid.Column="1"
Background="LightGray"
Padding="20"
Margin="10,0,0,0">
<StackPanel>
<TextBlock Text="Деталі:" FontWeight="Bold" FontSize="16" Margin="0,0,0,10"/>
<TextBlock Text="{Binding SelectedPerson.FirstName, StringFormat='Ім''я: {0}'}"/>
<TextBlock Text="{Binding SelectedPerson.LastName, StringFormat='Прізвище: {0}'}"/>
<TextBlock Text="{Binding SelectedPerson.Email, StringFormat='Email: {0}'}"/>
</StackPanel>
</Border>
</Grid>
Результат: При кліку на елемент у ListBox — деталі відображаються справа (Master-Detail pattern).
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0">
<ListBoxItem Content="Іван Петренко" IsSelected="True"/>
<ListBoxItem Content="Марія Коваленко"/>
<ListBoxItem Content="Олександр Шевченко"/>
</ListBox>
<Border Grid.Column="1" Background="LightGray" Padding="20" Margin="10,0,0,0">
<StackPanel Spacing="5">
<TextBlock Text="Деталі:" FontWeight="Bold" FontSize="16"/>
<TextBlock Text="Ім'я: Іван"/>
<TextBlock Text="Прізвище: Петренко"/>
<TextBlock Text="Email: ivan@example.com"/>
</StackPanel>
</Border>
</Grid>
<StackPanel>
<ListBox ItemsSource="{Binding People}"
SelectedIndex="{Binding SelectedIndex}"/>
<TextBlock Text="{Binding SelectedIndex, StringFormat='Вибрано елемент #{0}'}"/>
</StackPanel>
Для вибору конкретної властивості об'єкта:
<ComboBox ItemsSource="{Binding People}"
DisplayMemberPath="FirstName"
SelectedValuePath="Id"
SelectedValue="{Binding SelectedPersonId}"/>
Що відбувається:
DisplayMemberPath="FirstName" — показує FirstName у спискуSelectedValuePath="Id" — при виборі повертає Id вибраного PersonSelectedValue="{Binding SelectedPersonId}" — прив'язка до int SelectedPersonId у ViewModelUse Case: Коли потрібен тільки ID вибраного елемента, а не весь об'єкт.
ListBox.SelectionMode:
<!-- Один елемент (за замовчуванням) -->
<ListBox SelectionMode="Single" ItemsSource="{Binding People}"/>
<!-- Множинний вибір (Ctrl+Click) -->
<ListBox SelectionMode="Multiple" ItemsSource="{Binding People}"/>
<!-- Розширений вибір (Shift+Click для діапазону) -->
<ListBox SelectionMode="Extended" ItemsSource="{Binding People}"/>
Для множинного вибору:
public class ContactsViewModel
{
public ObservableCollection<Person> People { get; set; }
// Для Multiple/Extended режиму
public ObservableCollection<Person> SelectedPeople { get; set; }
}
<ListBox ItemsSource="{Binding People}"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedPeople}"/>
SelectedItems — це не DependencyProperty, тому прив'язка працює тільки в один бік (UI → ViewModel). Для повноцінного TwoWay binding потрібні додаткові техніки (Attached Behaviors).Мета: Навчитися використовувати ObservableCollection для простих типів.
Завдання:
Створіть додаток "Список завдань" (спрощена версія):
Вимоги:
ObservableCollection<string> для зберігання завданьListBox для відображення завданьTextBox для введення нового завданняTextBox у колекціюКритерії успіху:
TextBox очищається після додаванняПідказка:
public class TodoViewModel : INotifyPropertyChanged
{
public ObservableCollection<string> Tasks { get; set; }
private string _newTask;
public string NewTask
{
get => _newTask;
set
{
_newTask = value;
OnPropertyChanged();
}
}
private string _selectedTask;
public string SelectedTask
{
get => _selectedTask;
set
{
_selectedTask = value;
OnPropertyChanged();
}
}
public TodoViewModel()
{
Tasks = new ObservableCollection<string>
{
"Купити молоко",
"Зробити домашнє завдання"
};
}
}
<StackPanel Margin="20">
<TextBox Text="{Binding NewTask, UpdateSourceTrigger=PropertyChanged}"/>
<Button Content="Додати" Click="Add_Click" Margin="0,5,0,0"/>
<ListBox ItemsSource="{Binding Tasks}"
SelectedItem="{Binding SelectedTask}"
Height="200"
Margin="0,10,0,0"/>
<Button Content="Видалити"
Click="Remove_Click"
IsEnabled="{Binding SelectedTask, Converter={local:NullToBoolConverter}}"
Margin="0,5,0,0"/>
</StackPanel>
Мета: Створити повноцінний список контактів з красивим UI.
Завдання:
Створіть додаток "Контакти" з:
Модель Contact:
public class Contact : INotifyPropertyChanged
{
private string _firstName;
private string _lastName;
private string _phone;
private string _email;
public string FirstName
{
get => _firstName;
set
{
_firstName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName));
}
}
public string LastName
{
get => _lastName;
set
{
_lastName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName));
}
}
public string Phone
{
get => _phone;
set
{
_phone = value;
OnPropertyChanged();
}
}
public string Email
{
get => _email;
set
{
_email = value;
OnPropertyChanged();
}
}
public string FullName => $"{FirstName} {LastName}";
// ... INotifyPropertyChanged implementation
}
Вимоги до UI:
ListBox з DataTemplate для кожного контакту:
Критерії успіху:
Підказка для DataTemplate:
<DataTemplate DataType="{x:Type local:Contact}">
<Border Background="White"
BorderBrush="#E0E0E0"
BorderThickness="0,0,0,1"
Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Аватар з ініціалами -->
<Border Width="50"
Height="50"
Background="#4CAF50"
CornerRadius="25"
Grid.Column="0"
Margin="0,0,10,0">
<TextBlock Text="{Binding Initials}"
Foreground="White"
FontSize="18"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<!-- Інформація -->
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Text="{Binding FullName}"
FontWeight="Bold"
FontSize="14"/>
<TextBlock Text="{Binding Phone}"
FontSize="12"
Foreground="Gray"/>
<TextBlock Text="{Binding Email}"
FontSize="12"
Foreground="Gray"/>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
Мета: Реалізувати Master-Detail pattern для перегляду та редагування деталей.
Завдання:
Створіть додаток "Бібліотека книг" з Master-Detail інтерфейсом:
Модель Book:
public class Book : INotifyPropertyChanged
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public string Genre { get; set; }
public string Description { get; set; }
public int Pages { get; set; }
public string ISBN { get; set; }
// ... INotifyPropertyChanged implementation
}
Вимоги до UI:
Ліва частина (Master):
ListBox з книгами (Title + Author)Права частина (Detail):
Критерії успіху:
Додатково (складно):
Підказка для Master-Detail layout:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Master -->
<Border Grid.Column="0" BorderBrush="Gray" BorderThickness="0,0,1,0">
<StackPanel>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}"
Margin="10"/>
<ListBox ItemsSource="{Binding FilteredBooks}"
SelectedItem="{Binding SelectedBook}"/>
<Button Content="Додати книгу" Click="AddBook_Click" Margin="10"/>
</StackPanel>
</Border>
<!-- Detail -->
<Border Grid.Column="1" Padding="20">
<StackPanel>
<TextBlock Text="{Binding SelectedBook.Title}"
FontSize="24"
FontWeight="Bold"/>
<TextBlock Text="{Binding SelectedBook.Author}"
FontSize="18"
Foreground="Gray"/>
<!-- Інші поля -->
</StackPanel>
</Border>
</Grid>
ObservableCollection<T> — це фундаментальна колекція для Data Binding у WPF. Вона автоматично повідомляє UI про зміни через INotifyCollectionChanged.
Ключові висновки:
📦 ObservableCollection<T>
INotifyCollectionChanged для сповіщення Binding Engine.🔔 INotifyCollectionChanged
CollectionChanged. Викликається при Add, Remove, Replace, Move, Reset.📋 ItemsControl
ItemsSource, ItemTemplate, ItemsPanel.🎯 SelectedItem
❌ List<T> не працює
List<T> не повідомляє про зміни. Використовуйте ObservableCollection<T> для Data Binding.✅ Автоматичне оновлення
OnPropertyChanged.Коли використовувати ObservableCollection:
Коли НЕ використовувати ObservableCollection:
List<T> або масивList<T> + ручне оновленняObservableCollection у конструкторі ViewModel. Не робіть її null — це спричинить помилки Binding.Що далі?
ICollectionViewSystem.Collections.ObjectModel, що реалізує INotifyCollectionChanged для автоматичного повідомлення про зміни.INotifyCollectionChanged — інтерфейс з подією CollectionChanged, що викликається при зміні вмісту колекції (Add, Remove, Replace, Move, Reset).CollectionChanged event — подія, що повідомляє про зміни у колекції. Параметри: Action (тип зміни), NewItems, OldItems, NewStartingIndex, OldStartingIndex.ItemsControl — базовий клас для контролів, що відображають колекції. Властивості: ItemsSource, ItemTemplate, ItemsPanel, DisplayMemberPath.ItemsSource — властивість для прив'язки колекції до UI-контролу. Приймає IEnumerable.ItemTemplate — DataTemplate для відображення кожного елемента колекції.ItemsPanel — панель для розташування елементів (StackPanel, WrapPanel, VirtualizingStackPanel).Selector — базовий клас для контролів з вибором (ListBox, ComboBox). Властивості: SelectedItem, SelectedIndex, SelectedValue.SelectedItem — вибраний об'єкт у колекції. TwoWay binding за замовчуванням.SelectedIndex — індекс вибраного елемента (0-based). -1 якщо нічого не вибрано.Master-Detail pattern — UI-патерн, де список (Master) зліва, деталі вибраного елемента (Detail) справа.📖 Microsoft Docs: ObservableCollection
ObservableCollection<T> з прикладами використання.📖 INotifyCollectionChanged Interface
INotifyCollectionChanged та події CollectionChanged.Data Templates — Візуалізація об'єктів у WPF
Перетворення C#-об'єктів у красивий UI автоматично через DataTemplate — від простих шаблонів до DataTemplateSelector та HierarchicalDataTemplate
Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization
Сортування, фільтрація та групування колекцій без зміни оригінальних даних через ICollectionView та оптимізація продуктивності через віртуалізацію