У попередній статті ми навчилися прив'язувати колекції через ObservableCollection<T>. Але що, якщо потрібно:
Спроба 1: Змінити оригінальну колекцію
public void SortByName()
{
var sorted = People.OrderBy(p => p.LastName).ToList();
People.Clear();
foreach (var person in sorted)
{
People.Add(person);
}
// ✅ Працює, але...
}
Проблеми:
SelectedItem)Рішення: ICollectionView — "вигляд" на колекцію, що дозволяє сортувати, фільтрувати та групувати без зміни оригінальних даних.
ICollectionView — це інтерфейс, що представляє "вигляд" на колекцію з можливістю сортування, фільтрації та групування.
Аналогія: Уявіть бібліотеку з книгами.
Книги на полицях залишаються на своїх місцях, але каталог показує їх по-різному.
Ключова ідея: Одна колекція → кілька виглядів → кілька UI-контролів з різним відображенням.
Спосіб 1: CollectionViewSource.GetDefaultView()
using System.Windows.Data;
public class ContactsViewModel
{
public ObservableCollection<Person> People { get; set; }
public ICollectionView PeopleView { get; set; }
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко", Age = 25 },
new Person { FirstName = "Марія", LastName = "Коваленко", Age = 30 },
new Person { FirstName = "Олександр", LastName = "Шевченко", Age = 22 }
};
// Отримуємо вигляд на колекцію
PeopleView = CollectionViewSource.GetDefaultView(People);
}
}
XAML:
<!-- Прив'язка до ICollectionView замість ObservableCollection -->
<ListBox ItemsSource="{Binding PeopleView}"/>
Спосіб 2: CollectionViewSource у XAML (декларативний)
<Window.Resources>
<CollectionViewSource x:Key="peopleViewSource"
Source="{Binding People}">
<!-- Сортування, фільтрація, групування тут -->
</CollectionViewSource>
</Window.Resources>
<ListBox ItemsSource="{Binding Source={StaticResource peopleViewSource}}"/>
| Властивість | Опис | Тип |
|---|---|---|
SortDescriptions | Колекція правил сортування | SortDescriptionCollection |
Filter | Predicate для фільтрації | Predicate<object> |
GroupDescriptions | Колекція правил групування | ObservableCollection<GroupDescription> |
CurrentItem | Поточний (вибраний) елемент | object |
CurrentPosition | Індекс поточного елемента | int |
IsEmpty | Чи порожній вигляд (після фільтрації) | bool |
SortDescription визначає правило сортування за властивістю.
ViewModel:
public class ContactsViewModel
{
public ObservableCollection<Person> People { get; set; }
public ICollectionView PeopleView { get; set; }
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко", Age = 25 },
new Person { FirstName = "Марія", LastName = "Коваленко", Age = 30 },
new Person { FirstName = "Олександр", LastName = "Шевченко", Age = 22 }
};
PeopleView = CollectionViewSource.GetDefaultView(People);
// Сортування за прізвищем (за зростанням)
PeopleView.SortDescriptions.Add(
new SortDescription("LastName", ListSortDirection.Ascending)
);
}
}
Результат: Список відображається у порядку: Коваленко, Петренко, Шевченко.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<ListBox Margin="20">
<ListBoxItem Content="Марія Коваленко"/>
<ListBoxItem Content="Іван Петренко"/>
<ListBoxItem Content="Олександр Шевченко"/>
<TextBlock Text="(Відсортовано за прізвищем)"
FontSize="10"
Foreground="Gray"
Margin="10,5,0,0"/>
</ListBox>
Можна додати кілька правил сортування — спочатку за одним полем, потім за іншим:
// Спочатку за містом, потім за прізвищем
PeopleView.SortDescriptions.Add(
new SortDescription("City", ListSortDirection.Ascending)
);
PeopleView.SortDescriptions.Add(
new SortDescription("LastName", ListSortDirection.Ascending)
);
Результат: Спочатку групуються за містом (Київ, Львів, Одеса), всередині кожної групи — за прізвищем.
public void SortByName()
{
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(
new SortDescription("LastName", ListSortDirection.Ascending)
);
}
public void SortByAge()
{
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(
new SortDescription("Age", ListSortDirection.Descending)
);
}
XAML з кнопками:
<StackPanel Margin="20">
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<Button Content="За прізвищем" Click="SortByName_Click" Margin="0,0,5,0"/>
<Button Content="За віком" Click="SortByAge_Click"/>
</StackPanel>
<ListBox ItemsSource="{Binding PeopleView}" Height="200"/>
</StackPanel>
<Window.Resources>
<CollectionViewSource x:Key="peopleViewSource" Source="{Binding People}">
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="LastName" Direction="Ascending"/>
<componentModel:SortDescription PropertyName="FirstName" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListBox ItemsSource="{Binding Source={StaticResource peopleViewSource}}"/>
Namespace для SortDescription у XAML:
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
Filter — це Predicate<object>, що визначає, які елементи показувати.
ViewModel:
public class ContactsViewModel
{
public ObservableCollection<Person> People { get; set; }
public ICollectionView PeopleView { get; set; }
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко", Age = 17 },
new Person { FirstName = "Марія", LastName = "Коваленко", Age = 30 },
new Person { FirstName = "Олександр", LastName = "Шевченко", Age = 22 }
};
PeopleView = CollectionViewSource.GetDefaultView(People);
// Фільтр: показати тільки повнолітніх
PeopleView.Filter = item =>
{
if (item is Person person)
{
return person.Age >= 18;
}
return false;
};
}
}
Результат: Список показує тільки Марію (30) та Олександра (22). Іван (17) прихований.
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="10">
<TextBlock Text="Тільки повнолітні (вік >= 18):" FontWeight="Bold"/>
<ListBox>
<ListBoxItem Content="Марія Коваленко (30)"/>
<ListBoxItem Content="Олександр Шевченко (22)"/>
</ListBox>
<TextBlock Text="(Іван Петренко (17) прихований фільтром)"
FontSize="10"
Foreground="Gray"/>
</StackPanel>
ViewModel з пошуком:
public class ContactsViewModel : INotifyPropertyChanged
{
public ObservableCollection<Person> People { get; set; }
public ICollectionView PeopleView { get; set; }
private string _searchQuery;
public string SearchQuery
{
get => _searchQuery;
set
{
_searchQuery = value;
OnPropertyChanged();
ApplyFilter();
}
}
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко" },
new Person { FirstName = "Марія", LastName = "Коваленко" },
new Person { FirstName = "Олександр", LastName = "Шевченко" }
};
PeopleView = CollectionViewSource.GetDefaultView(People);
}
private void ApplyFilter()
{
if (string.IsNullOrWhiteSpace(SearchQuery))
{
// Немає пошукового запиту — показати всіх
PeopleView.Filter = null;
}
else
{
// Фільтр: ім'я або прізвище містить запит
PeopleView.Filter = item =>
{
if (item is Person person)
{
return person.FirstName.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase) ||
person.LastName.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase);
}
return false;
};
}
}
// ... INotifyPropertyChanged implementation
}
XAML:
<StackPanel Margin="20">
<TextBlock Text="Пошук:"/>
<TextBox Text="{Binding SearchQuery, UpdateSourceTrigger=PropertyChanged}"
Margin="0,5,0,10"/>
<ListBox ItemsSource="{Binding PeopleView}" Height="200"/>
</StackPanel>
Результат: При введенні "Мар" — показується тільки Марія. При введенні "ко" — Марія та Олександр (обидва мають "ко" у прізвищі).
Якщо фільтр залежить від зовнішніх даних, потрібно викликати Refresh():
private int _minAge = 18;
public int MinAge
{
get => _minAge;
set
{
_minAge = value;
OnPropertyChanged();
PeopleView.Refresh(); // Оновлюємо фільтр
}
}
public ContactsViewModel()
{
// ...
PeopleView.Filter = item =>
{
if (item is Person person)
{
return person.Age >= MinAge; // Залежить від MinAge
}
return false;
};
}
XAML:
<StackPanel Margin="20">
<TextBlock Text="Мінімальний вік:"/>
<Slider Value="{Binding MinAge}" Minimum="0" Maximum="100" Margin="0,5,0,10"/>
<TextBlock Text="{Binding MinAge, StringFormat='Вік >= {0}'}"/>
<ListBox ItemsSource="{Binding PeopleView}" Height="200" Margin="0,10,0,0"/>
</StackPanel>
Результат: При зміні слайдера — список автоматично фільтрується.
GroupDescriptions дозволяє групувати елементи за властивістю.
ViewModel:
public class ContactsViewModel
{
public ObservableCollection<Person> People { get; set; }
public ICollectionView PeopleView { get; set; }
public ContactsViewModel()
{
People = new ObservableCollection<Person>
{
new Person { FirstName = "Іван", LastName = "Петренко", City = "Київ" },
new Person { FirstName = "Марія", LastName = "Коваленко", City = "Львів" },
new Person { FirstName = "Олександр", LastName = "Шевченко", City = "Київ" },
new Person { FirstName = "Анна", LastName = "Мельник", City = "Львів" }
};
PeopleView = CollectionViewSource.GetDefaultView(People);
// Групування за містом
PeopleView.GroupDescriptions.Add(new PropertyGroupDescription("City"));
}
}
XAML з GroupStyle:
<ListBox ItemsSource="{Binding PeopleView}">
<!-- Стиль для заголовків груп -->
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Border Background="#2196F3" Padding="5">
<TextBlock Text="{Binding Name}"
Foreground="White"
FontWeight="Bold"
FontSize="14"/>
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
<!-- Шаблон для елементів -->
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Результат:
┌─ Київ ────────────┐
│ Іван │
│ Олександр │
├─ Львів ───────────┤
│ Марія │
│ Анна │
└───────────────────┘
Loading Avalonia WebAssembly...
Downloading .NET runtime (10MB)...
<StackPanel Margin="20" Spacing="5">
<Border Background="#2196F3" Padding="5">
<TextBlock Text="Київ" Foreground="White" FontWeight="Bold"/>
</Border>
<TextBlock Text=" Іван Петренко" Margin="10,0,0,0"/>
<TextBlock Text=" Олександр Шевченко" Margin="10,0,0,0"/>
<Border Background="#2196F3" Padding="5" Margin="0,10,0,0">
<TextBlock Text="Львів" Foreground="White" FontWeight="Bold"/>
</Border>
<TextBlock Text=" Марія Коваленко" Margin="10,0,0,0"/>
<TextBlock Text=" Анна Мельник" Margin="10,0,0,0"/>
</StackPanel>
// Спочатку за містом, потім за віковою категорією
PeopleView.GroupDescriptions.Add(new PropertyGroupDescription("City"));
PeopleView.GroupDescriptions.Add(new PropertyGroupDescription("AgeCategory"));
Модель з обчислюваною властивістю:
public class Person : INotifyPropertyChanged
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public int Age { get; set; }
// Обчислювана властивість для групування
public string AgeCategory
{
get
{
if (Age < 18) return "Неповнолітні";
if (Age < 30) return "18-29";
if (Age < 50) return "30-49";
return "50+";
}
}
// ... INotifyPropertyChanged implementation
}
Результат:
┌─ Київ ────────────┐
│ ├─ 18-29 ─────────┤
│ │ Іван (25) │
│ ├─ 30-49 ─────────┤
│ │ Олександр (35) │
├─ Львів ───────────┤
│ ├─ 18-29 ─────────┤
│ │ Марія (28) │
└───────────────────┘
<Window.Resources>
<CollectionViewSource x:Key="peopleViewSource" Source="{Binding People}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="City"/>
</CollectionViewSource.GroupDescriptions>
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="City" Direction="Ascending"/>
<componentModel:SortDescription PropertyName="LastName" Direction="Ascending"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListBox ItemsSource="{Binding Source={StaticResource peopleViewSource}}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"
FontWeight="Bold"
FontSize="16"
Background="LightGray"
Padding="5"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
Для складної логіки групування:
public class AgeGroupConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int age)
{
if (age < 18) return "Неповнолітні";
if (age < 30) return "Молодь (18-29)";
if (age < 50) return "Дорослі (30-49)";
return "Старші (50+)";
}
return "Невідомо";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
PeopleView.GroupDescriptions.Add(
new PropertyGroupDescription("Age", new AgeGroupConverter())
);
Проблема: Що, якщо у вас 100,000 елементів у списку? WPF створить 100,000 UI-елементів → зависання.
Рішення: Virtualization — WPF створює UI-елементи тільки для видимих рядків.
Без віртуалізації:
З віртуалізацією:
ListBox за замовчуванням використовує VirtualizingStackPanel як ItemsPanel.
Перевірка:
<ListBox ItemsSource="{Binding LargeCollection}">
<!-- За замовчуванням VirtualizingStackPanel -->
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Тест продуктивності:
public class PerformanceViewModel
{
public ObservableCollection<int> LargeCollection { get; set; }
public PerformanceViewModel()
{
LargeCollection = new ObservableCollection<int>();
// Додаємо 100,000 елементів
for (int i = 0; i < 100_000; i++)
{
LargeCollection.Add(i);
}
}
}
Результат: Список з 100,000 елементів відкривається миттєво та прокручується плавно.
Standard (за замовчуванням):
Recycling (оптимізований):
<ListBox ItemsSource="{Binding LargeCollection}"
VirtualizingPanel.VirtualizationMode="Recycling">
<!-- Recycling mode — ще швидше -->
</ListBox>
Порівняння:
| Режим | Створення UI | Видалення UI | Пам'ять | Швидкість |
|---|---|---|---|---|
| Standard | При прокрутці | При прокрутці | Середня | Швидка |
| Recycling | Один раз | Ніколи | Менша | Швидша |
VirtualizationMode="Recycling" для великих списків (>1000 елементів).Проблема 1: ScrollViewer всередині ItemsControl
<!-- ❌ Віртуалізація не працює -->
<ScrollViewer>
<ListBox ItemsSource="{Binding LargeCollection}"/>
</ScrollViewer>
Чому? ListBox має власний ScrollViewer. Зовнішній ScrollViewer змушує ListBox розгорнутися на повну висоту → всі елементи створюються.
Рішення: Видалити зовнішній ScrollViewer.
Проблема 2: ItemsControl без фіксованої висоти
<!-- ❌ Віртуалізація не працює -->
<ListBox ItemsSource="{Binding LargeCollection}" Height="Auto"/>
Чому? Height="Auto" означає "розгорнися на всі елементи" → всі елементи створюються.
Рішення: Встановити фіксовану висоту або Height="*" у Grid.
Проблема 3: StackPanel як ItemsPanel
<!-- ❌ Віртуалізація не працює -->
<ListBox ItemsSource="{Binding LargeCollection}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/> <!-- Замість VirtualizingStackPanel -->
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
Рішення: Використовувати VirtualizingStackPanel (за замовчуванням).
<!-- Вимкнути віртуалізацію (для дебагу) -->
<ListBox ItemsSource="{Binding LargeCollection}"
VirtualizingPanel.IsVirtualizing="False"/>
<!-- Прокрутка по пікселях (плавна) -->
<ListBox VirtualizingPanel.ScrollUnit="Pixel"/>
<!-- Прокрутка по елементах (за замовчуванням) -->
<ListBox VirtualizingPanel.ScrollUnit="Item"/>
Pixel: Плавна прокрутка, але менш ефективна віртуалізація. Item: Прокрутка по елементах (стрибками), але ефективніша віртуалізація.
Мета: Навчитися використовувати SortDescription для сортування колекції.
Завдання:
Створіть список контактів з можливістю сортування:
Вимоги:
ObservableCollection<Person> з мінімум 10 контактамиICollectionView для відображенняКритерії успіху:
SelectedItem) зберігається після сортуванняПідказка:
public void SortByLastNameAsc()
{
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(
new SortDescription("LastName", ListSortDirection.Ascending)
);
}
public void SortByLastNameDesc()
{
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(
new SortDescription("LastName", ListSortDirection.Descending)
);
}
public void SortByAgeDesc()
{
PeopleView.SortDescriptions.Clear();
PeopleView.SortDescriptions.Add(
new SortDescription("Age", ListSortDirection.Descending)
);
}
Мета: Створити список з пошуком та сортуванням через UI.
Завдання:
Створіть додаток "Каталог товарів" з:
Модель Product:
public class Product : INotifyPropertyChanged
{
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
// ... INotifyPropertyChanged implementation
}
Вимоги до UI:
Критерії успіху:
Підказка для комбінованого фільтру:
private void ApplyFilter()
{
PeopleView.Filter = item =>
{
if (item is Product product)
{
// Пошук по назві
bool matchesSearch = string.IsNullOrWhiteSpace(SearchQuery) ||
product.Name.Contains(SearchQuery, StringComparison.OrdinalIgnoreCase);
// Фільтр категорії
bool matchesCategory = SelectedCategory == "Всі" ||
product.Category == SelectedCategory;
// Фільтр наявності
bool matchesStock = !ShowOnlyInStock || product.Stock > 0;
return matchesSearch && matchesCategory && matchesStock;
}
return false;
};
}
Мета: Створити повноцінний Todo-додаток з усіма можливостями ICollectionView.
Завдання:
Створіть Todo-додаток з:
Модель TodoItem:
public class TodoItem : INotifyPropertyChanged
{
private bool _isCompleted;
public string Title { get; set; }
public string Description { get; set; }
public DateTime DueDate { get; set; }
public string Priority { get; set; } // "Високий", "Середній", "Низький"
public string Category { get; set; } // "Робота", "Особисте", "Навчання"
public bool IsCompleted
{
get => _isCompleted;
set
{
_isCompleted = value;
OnPropertyChanged();
}
}
public bool IsOverdue => !IsCompleted && DueDate < DateTime.Now;
// ... INotifyPropertyChanged implementation
}
Вимоги до UI:
Верхня панель:
Фільтри:
Сортування:
Групування:
Список завдань:
Статистика:
Критерії успіху:
Додатково (складно):
Підказка для групування:
private void UpdateGrouping()
{
TodosView.GroupDescriptions.Clear();
if (GroupByCategory)
{
TodosView.GroupDescriptions.Add(new PropertyGroupDescription("Category"));
}
if (GroupByPriority)
{
TodosView.GroupDescriptions.Add(new PropertyGroupDescription("Priority"));
}
}
ICollectionView — це потужний інструмент для маніпуляції відображенням колекцій без зміни оригінальних даних.
Ключові висновки:
👁️ ICollectionView
🔄 SortDescription
🔍 Filter
Refresh(). Комбінування кількох умов.📁 GroupDescriptions
GroupStyle для кастомізації заголовків груп. Множинне групування.⚡ Virtualization
VirtualizingStackPanel + VirtualizationMode="Recycling" для оптимізації.🎯 Один джерело → багато виглядів
ObservableCollection → кілька ICollectionView → різне відображення у різних UI-контролах.Коли використовувати ICollectionView:
Коли використовувати Virtualization:
Коли НЕ використовувати Virtualization:
ICollectionView для сортування та фільтрації замість зміни оригінальної колекції. Це дозволяє зберегти оригінальні дані та мати кілька різних виглядів.Що далі?
ICollectionView з колекції. Метод GetDefaultView() повертає default view для колекції.SortDescription — структура, що визначає правило сортування за властивістю. Параметри: PropertyName, Direction (Ascending/Descending).Filter — властивість типу Predicate<object>, що визначає, які елементи показувати у вигляді.Refresh() — метод для оновлення фільтру після зміни зовнішніх даних, від яких залежить фільтр.GroupDescriptions — колекція правил групування. PropertyGroupDescription групує за властивістю.GroupStyle — стиль для відображення груп у ItemsControl. HeaderTemplate визначає шаблон заголовка групи.Virtualization — техніка оптимізації, коли UI-елементи створюються тільки для видимих рядків списку.VirtualizingStackPanel — панель, що підтримує віртуалізацію. За замовчуванням використовується у ListBox.VirtualizationMode — режим віртуалізації: Standard (створення/видалення UI) або Recycling (перевикористання UI).ScrollUnit — одиниця прокрутки: Pixel (плавна прокрутка) або Item (прокрутка по елементах).📖 Microsoft Docs: ICollectionView
ICollectionView з прикладами використання.📖 CollectionViewSource Class
CollectionViewSource для створення виглядів на колекції.Collections Binding Part 1 — ObservableCollection та ItemsControl
Прив'язка колекцій C#-об'єктів до UI-списків з автоматичним оновленням через ObservableCollection та INotifyCollectionChanged
MVVM Pattern — Від Spaghetti Code до архітектури
Розуміння MVVM як архітектурного патерну — мотивація, структура, три компоненти (Model, View, ViewModel) та золоті правила розділення відповідальності