Desktop UI

Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization

Сортування, фільтрація та групування колекцій без зміни оригінальних даних через ICollectionView та оптимізація продуктивності через віртуалізацію

Collections Binding Part 2: ICollectionView, Filtering, Sorting та Virtualization

Вступ

У попередній статті ми навчилися прив'язувати колекції через ObservableCollection<T>. Але що, якщо потрібно:

  • Відсортувати список контактів за прізвищем?
  • Відфільтрувати завдання — показати тільки активні?
  • Згрупувати товари за категоріями?
  • Відобразити 100,000 елементів без зависання UI?

Спроба 1: Змінити оригінальну колекцію

public void SortByName()
{
    var sorted = People.OrderBy(p => p.LastName).ToList();
    People.Clear();
    foreach (var person in sorted)
    {
        People.Add(person);
    }
    
    // ✅ Працює, але...
}

Проблеми:

  • ❌ Змінюємо оригінальні дані (порушуємо принцип незмінності)
  • ❌ Втрачається вибраний елемент (SelectedItem)
  • ❌ Повне перемалювання UI (неефективно)
  • ❌ Не можна мати кілька різних "виглядів" на одну колекцію

Рішення: ICollectionView — "вигляд" на колекцію, що дозволяє сортувати, фільтрувати та групувати без зміни оригінальних даних.

Для кого ця стаття? Якщо ви вже знайомі з ObservableCollection, ця стаття покаже, як маніпулювати відображенням колекцій без зміни самих даних.

ICollectionView: Вигляд на колекцію

ICollectionView — це інтерфейс, що представляє "вигляд" на колекцію з можливістю сортування, фільтрації та групування.

Концепція: Модель vs Вигляд

Аналогія: Уявіть бібліотеку з книгами.

  • Модель (ObservableCollection) — це фізичні книги на полицях. Вони не змінюються.
  • Вигляд (ICollectionView) — це каталог, де ви можете:
    • Відсортувати книги за автором, роком, назвою
    • Відфільтрувати тільки фантастику
    • Згрупувати за жанрами

Книги на полицях залишаються на своїх місцях, але каталог показує їх по-різному.

Loading diagram...
graph TD
    A[ObservableCollection<Person>] --> B[ICollectionView 1<br/>Сортування: за прізвищем]
    A --> C[ICollectionView 2<br/>Фільтр: вік > 18]
    A --> D[ICollectionView 3<br/>Групування: за містом]
    
    B --> E[ListBox 1]
    C --> F[ListBox 2]
    D --> G[ListBox 3]
    
    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#10b981,stroke:#059669,color:#ffffff
    style C fill:#10b981,stroke:#059669,color:#ffffff
    style D fill:#10b981,stroke:#059669,color:#ffffff

Ключова ідея: Одна колекція → кілька виглядів → кілька UI-контролів з різним відображенням.

Отримання ICollectionView

Спосіб 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}}"/>

Властивості ICollectionView

ВластивістьОписТип
SortDescriptionsКолекція правил сортуванняSortDescriptionCollection
FilterPredicate для фільтраціїPredicate<object>
GroupDescriptionsКолекція правил групуванняObservableCollection<GroupDescription>
CurrentItemПоточний (вибраний) елементobject
CurrentPositionІндекс поточного елементаint
IsEmptyЧи порожній вигляд (після фільтрації)bool

SortDescription: Сортування колекції

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)...

Множинне сортування

Можна додати кілька правил сортування — спочатку за одним полем, потім за іншим:

// Спочатку за містом, потім за прізвищем
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>

Сортування у XAML (декларативне)

<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: Фільтрація колекції

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)...

Динамічна фільтрація (пошук)

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() — Оновлення фільтру

Якщо фільтр залежить від зовнішніх даних, потрібно викликати 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: Групування колекції

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)...

Множинне групування

// Спочатку за містом, потім за віковою категорією
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)      │
└───────────────────┘

Групування у XAML

<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>

Кастомне групування через IValueConverter

Для складної логіки групування:

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())
);

Virtualization: Оптимізація продуктивності

Проблема: Що, якщо у вас 100,000 елементів у списку? WPF створить 100,000 UI-елементів → зависання.

Рішення: Virtualization — WPF створює UI-елементи тільки для видимих рядків.

Як працює віртуалізація?

Loading diagram...
graph TD
    A[ObservableCollection<br/>100,000 елементів] --> B[VirtualizingStackPanel]
    B --> C{Які елементи видимі?}
    C --> D[Елементи 0-20<br/>видимі на екрані]
    C --> E[Елементи 21-99,999<br/>не видимі]
    
    D --> F[Створити UI<br/>для 21 елемента]
    E --> G[Не створювати UI<br/>економія пам'яті]
    
    H[Користувач прокручує] --> I[Видалити UI<br/>для елементів 0-5]
    H --> J[Створити UI<br/>для елементів 21-26]
    
    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#10b981,stroke:#059669,color:#ffffff
    style F fill:#10b981,stroke:#059669,color:#ffffff
    style G fill:#64748b,stroke:#334155,color:#ffffff

Без віртуалізації:

  • 100,000 елементів → 100,000 UI-контролів → ~500 MB пам'яті → зависання

З віртуалізацією:

  • 100,000 елементів → 20 UI-контролів (тільки видимі) → ~5 MB пам'яті → плавна робота

VirtualizingStackPanel

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 елементів відкривається миттєво та прокручується плавно.

VirtualizationMode: Standard vs Recycling

Standard (за замовчуванням):

  • Створює нові UI-елементи при прокрутці
  • Видаляє старі UI-елементи

Recycling (оптимізований):

  • Перевикористовує існуючі UI-елементи
  • Тільки оновлює DataContext
<ListBox ItemsSource="{Binding LargeCollection}"
         VirtualizingPanel.VirtualizationMode="Recycling">
    <!-- Recycling mode — ще швидше -->
</ListBox>

Порівняння:

РежимСтворення UIВидалення UIПам'ятьШвидкість
StandardПри прокрутціПри прокрутціСередняШвидка
RecyclingОдин разНіколиМеншаШвидша
Best Practice: Завжди використовуйте 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 (за замовчуванням).

IsVirtualizing — Увімкнення/Вимкнення

<!-- Вимкнути віртуалізацію (для дебагу) -->
<ListBox ItemsSource="{Binding LargeCollection}"
         VirtualizingPanel.IsVirtualizing="False"/>
Увага: Вимикайте віртуалізацію тільки для дебагу або дуже малих списків (<100 елементів). Для великих списків це призведе до зависання.

ScrollUnit: Pixel vs Item

<!-- Прокрутка по пікселях (плавна) -->
<ListBox VirtualizingPanel.ScrollUnit="Pixel"/>

<!-- Прокрутка по елементах (за замовчуванням) -->
<ListBox VirtualizingPanel.ScrollUnit="Item"/>

Pixel: Плавна прокрутка, але менш ефективна віртуалізація. Item: Прокрутка по елементах (стрибками), але ефективніша віртуалізація.


Практичні завдання

Рівень 1: Сортування списку по імені через SortDescription

Мета: Навчитися використовувати SortDescription для сортування колекції.

Завдання:

Створіть список контактів з можливістю сортування:

Вимоги:

  1. ObservableCollection<Person> з мінімум 10 контактами
  2. ICollectionView для відображення
  3. Три кнопки сортування:
    • "За прізвищем (А-Я)"
    • "За прізвищем (Я-А)"
    • "За віком (спадання)"
  4. При кліку на кнопку — список пересортовується

Критерії успіху:

  • Сортування працює коректно
  • Вибраний елемент (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)
    );
}

Рівень 2: Фільтрація + сортування з UI

Мета: Створити список з пошуком та сортуванням через 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:

  1. Пошук: TextBox для пошуку по назві товару
  2. Фільтр категорії: ComboBox з категоріями ("Всі", "Електроніка", "Одяг", "Їжа")
  3. Фільтр наявності: CheckBox "Тільки в наявності" (Stock > 0)
  4. Сортування: ComboBox з варіантами:
    • "За назвою (А-Я)"
    • "За назвою (Я-А)"
    • "За ціною (зростання)"
    • "За ціною (спадання)"
  5. Список товарів: ListBox з DataTemplate (назва, категорія, ціна, кількість)

Критерії успіху:

  • Всі фільтри працюють одночасно (пошук + категорія + наявність)
  • Сортування працює разом з фільтрами
  • При зміні будь-якого фільтра — список оновлюється миттєво
  • Показується кількість знайдених товарів

Підказка для комбінованого фільтру:

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;
    };
}

Рівень 3: Повний Todo-додаток з фільтрацією та групуванням

Мета: Створити повноцінний 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:

Верхня панель:

  1. TextBox для додавання нового завдання
  2. ComboBox для вибору пріоритету
  3. DatePicker для вибору дати
  4. Кнопка "Додати"

Фільтри:

  1. Три кнопки: "Всі", "Активні", "Завершені"
  2. ComboBox для фільтрації за категорією
  3. CheckBox "Показати прострочені"

Сортування:

  1. ComboBox з варіантами:
    • "За датою (найближчі спочатку)"
    • "За пріоритетом"
    • "За назвою"

Групування:

  1. CheckBox "Групувати за категорією"
  2. CheckBox "Групувати за пріоритетом"

Список завдань:

  1. ListBox з DataTemplate:
    • CheckBox для позначення завершення
    • Назва завдання (закреслена якщо завершено)
    • Дата (червона якщо прострочено)
    • Пріоритет (кольоровий індикатор)
  2. Кнопка "Видалити" для кожного завдання

Статистика:

  1. Кількість всіх завдань
  2. Кількість активних
  3. Кількість завершених
  4. Кількість прострочених

Критерії успіху:

  • Всі фільтри працюють одночасно
  • Сортування працює разом з фільтрами
  • Групування працює разом з фільтрами та сортуванням
  • Статистика оновлюється автоматично
  • Прострочені завдання виділяються червоним
  • Завершені завдання мають закреслений текст

Додатково (складно):

  • Збереження завдань у JSON файл
  • Редагування завдань (подвійний клік)
  • Drag & Drop для зміни пріоритету
  • Нагадування про прострочені завдання

Підказка для групування:

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

Predicate для фільтрації елементів. Динамічна фільтрація через Refresh(). Комбінування кількох умов.

📁 GroupDescriptions

Групування елементів за властивістю. GroupStyle для кастомізації заголовків груп. Множинне групування.

⚡ Virtualization

Створення UI тільки для видимих елементів. VirtualizingStackPanel + VirtualizationMode="Recycling" для оптимізації.

🎯 Один джерело → багато виглядів

Одна ObservableCollection → кілька ICollectionView → різне відображення у різних UI-контролах.

Коли використовувати ICollectionView:

  • ✅ Сортування списків без зміни оригінальних даних
  • ✅ Фільтрація (пошук, категорії, статуси)
  • ✅ Групування (за категоріями, датами, статусами)
  • ✅ Кілька різних виглядів на одну колекцію

Коли використовувати Virtualization:

  • ✅ Списки з >1000 елементів
  • ✅ Таблиці з великою кількістю рядків
  • ✅ Нескінченна прокрутка (infinite scroll)

Коли НЕ використовувати Virtualization:

  • ❌ Малі списки (<100 елементів)
  • ❌ Коли потрібен доступ до всіх UI-елементів одночасно
  • ❌ Складні layout-и з динамічною висотою елементів
Best Practice: Завжди використовуйте ICollectionView для сортування та фільтрації замість зміни оригінальної колекції. Це дозволяє зберегти оригінальні дані та мати кілька різних виглядів.

Що далі?

  • MVVM Pattern (наступна стаття) — архітектурний патерн для повного розділення UI та логіки
  • Commands (Блок 7) — замість event handlers у code-behind
  • Dependency Injection (Блок 7) — для тестованості та розширюваності

Словник термінів

ICollectionView — інтерфейс, що представляє "вигляд" на колекцію з можливістю сортування, фільтрації та групування без зміни оригінальних даних.CollectionViewSource — клас для створення 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

API документація інтерфейсу ICollectionView з прикладами використання.

📖 CollectionViewSource Class

Документація класу CollectionViewSource для створення виглядів на колекції.

🎓 Filtering and Sorting

Офіційний гайд з фільтрації та сортування колекцій через ICollectionView.

🎓 Grouping Data

Гайд з групування даних у WPF з прикладами GroupStyle.

⚡ UI Virtualization

Повний гайд з оптимізації продуктивності через віртуалізацію.

🔧 VirtualizingStackPanel

API документація VirtualizingStackPanel з усіма властивостями та режимами.

📚 Попередня стаття: Collections Binding Part 1

Повернутися до основ Collections Binding — ObservableCollection та ItemsControl.

📚 Наступна стаття: MVVM Pattern

Дізнатися про MVVM — архітектурний патерн для розділення UI та логіки.