[Каркас приложения. Модель данных. Привязка данных. Табличный вывод.](./wpf_template.md) | [Содержание](../readme.md) | [Поиск, сортировка](./wpf_search_sort.md)

# Фильтрация данных

В приложениях часто требуется отфильтровать данные либо по словарному полю, либо по каким-либо условиям. 

## Фильтрация по словарю

Суть фильтрации сводится к тому, что отображается не полный список объектов ("кошек"), а отфильтрованный по словарному полю (тип, категория...). Для получения фильтрованного списка реализуем геттер и сеттер для списка кошек:

Запись типа `public IEnumerable<Cat> catList { get; set; }` на самом деле является так называемым "синтаксическим сахаром", т.е. сокращённой записью для упрощения написания и повышения читабельности кода.

При компиляции этот код разворачивается примерно в такой (на самом деле get и set реализуются методами `getcatList` и `setcatList(value)`)

```cs
private IEnumerable<Cat> _catList = null;
public IEnumerable<Cat> catList {
   get
   {
       return _catList;
   }
   set {
       _catList = value;
   } 
}
```

То есть создаётся приватная переменная для хранения реального значения свойства и методы **get** и **set** для, соответственно, получения и сохранения значения свойства. "value" это новое значение свойства, устанавливаемое при присваивании.

```cs
private IEnumerable<Cat> _catList = null;
public IEnumerable<Cat> catList {
    get
    {
        // возвращаем не весь список, а фильтрованный по выбранной породе
        return _catList
            .Where(c => selectedBreed == "Все породы" || c.breed.title==selectedBreed);
    }
    set {
        _catList = value;
    } 
}
```

Таким обазом, при присваивании полный список "кошек" будет сохраняться в переменной *_catList*, а при чтении будет возвращаться отфильтрованный список

При работе с БД у нас обычно есть отдельные модели (таблицы) справочников - реализуем в нашем *поставщике данных* метод, возвращающий справочник пород:

1. Сначала создадим класс для элемента справочника (по идее нам было бы достаточно просто массива строк, но мы сразу будем делать "по-взрослому", чтобы сразу пройтись по всем "граблям", которые встретятся при работе с базой данных)

    ```cs
    public class CatBreed { 
        public string title { get; set; }
    }
    ```

1. Создаем в классе главного окна свойство для хранения справочника

    ```cs
    public List<CatBreed> catBreedList { get; set; }
    ```

    Здесь мы выбрали тип **List**, т.к. нам нужен изменяемый список, в который мы добавим элемент "Все породы"

1. В **интерфейс** поставщика данных (**IDataProvider**) добавляем декларацию метода для получения списка пород

    ```cs
    IEnumerable<CatBreed> getCatBreeds();
    ```

1. Реализуем этот метод в **LocalDataProvider**

    Этот метод можно реализовать двумя вариантами:

    * можно создать список руками

        ```cs
        public IEnumerable<CatBreed> getCatBreeds()
        {
            return new CatBreed[] {
                new CatBreed{ title="Дворняжка" },
                new CatBreed{ title="Шотландская вислоухая" },
                new CatBreed{ title="Сиамский" }
            };
        }
        ```

        Из плюсов то, что в списке могут быть породы, которых нет в исходных данных (практически это аналог запроса к базе данных). Минус в том, что вы можете пропустить породу, которая есть в исходных данных.

    * можно выбрать существующие породы из исходных данных (этот вариант предпочтительнее)

        Во-первых, немного поменяем класс **LocalDataProvider**, добавив кеширование списка кошек:

        ```cs
        public class LocalDataProvider : IDataProvider
        {
            // добавляем приватное поле для хранения списка кошек
            private IEnumerable<Cat> _cats = null;

            // в методе выбора кошек добавим проверку списка
            public IEnumerable<Cat> getCats()
            {
                if (_cats == null)
                {
                    _cats = new Cat[]{
                        // тут старый код, формирующий список кошек
                    }
                }
                return _cats;
            }
        }
        ```

        И реализуем метод, формирующий список пород:

        ```cs
        public IEnumerable<CatBreed> getCatBreeds()
        {
            getCats();
            return _cats
                .Select(c => c.breed)
                .DistinctBy(b => b.title);
        }
        ```

        Что тут происходит?

        * метод _getCats_ заполняет локальный список кошек

            Допустим исходный массив выглядит так (массив объектов): 

            ```json
            [
                {"breed": {"title": "Порода №1"}, "name": "Имя1"}, 
                {"breed": {"title": "Порода №2"}, "name": "Имя2"}, 
                {"breed": {"title": "Порода №1"}, "name": "Имя3"} 
            ]
            ```

        * метод _Select_ преобразует элемент массива в новый объект, и т.к. мы из всего объекта вернули только одно поле, то на выходе у нас будет список пород `IEnumerable<CatBreed>`, содержащий все породы из спика кошек:

            ```json
            [
                {"title": "Порода №1"},
                {"title": "Порода №2"},
                {"title": "Порода №1"}
            ]
            ```

        * метод *DistinctBy* выбирает записи с уникальным значением породы

            ```json
            [
                {"title": "Порода №1"},
                {"title": "Порода №2"}
            ]
            ```

1. Получаем список пород и добавляем в начало "Все породы", чтобы можно было отменить фильтрацию и отображать полный список

    ```cs
    // добавляем в класс переменую для хранения текущё выбранной породы
    private string selectedBreed = "Все породы";
    ```

    ```cs
    // в конструкторе получаем список пород
    catBreedList = Globals.dataProvider.getCatBreeds().ToList();
    // и добавляем в начало "все породы"
    catBreedList.Insert(0, new CatBreed { title = selectedBreed });
    ```

1. Теперь, имея список пород, добавляем в разметку (файл `.xaml`) выпадающий список для выбор породы (во **WrapPanel**):

    ```xml
    <Label 
        Content="Порода:"
        VerticalAlignment="Center"/>

    <ComboBox
        Name="BreedFilterComboBox"
        VerticalAlignment="Center"
        MinWidth="150"
        SelectedIndex="0"
        ItemsSource="{Binding catBreedList}"
    />
    ```

    Если запустить программу в таком виде, то увидим примерно такое:

    ![](../img/wpf_03.png)

    Вместо названий пород у нас имя класса, почему так происходит?

    Элемент **ComboBox** предназначен для отображения списка **строк**, т.е. где-то под капотом он для элемента списка вызывает метод _ToString_, а этот матод как раз и возвращает название класса.
    
    Для того, чтобы отобразить элементы другого типа данных можно использовать два варината:
    
    * Использовать шаблон **ComboBox.ItemTemplate**, в котором можно реализовать произвольный вид элемента списка (вставить картинки, раскрасить и т.п.) и, в нашем случае, в качестве содержимого выбрать строковое свойство объекта для отображения. 

        ```cs
        <ComboBox
            Name="BreedFilterComboBox"
            VerticalAlignment="Center"
            MinWidth="150"
            SelectedIndex="0"
            ItemsSource="{Binding catBreedList}"
        >
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <Label 
                        Content="{Binding title}"/>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        ```

    * Первый вариант, на мой взгляд, слишком сложен, если нам нужно вывести только одно строковое поле. В таком случае проще переопределить метод _ToString_ в классе **CatBreed**:

        ```cs
        public class CatBreed
        {
            public string title { get; set; }
            public override string ToString() => title;
        }
        ```

1. Добавим в **ComboBox** обработчик события выбора: **SelectionChanged**

    ```xml
    <ComboBox
        Name="BreedFilterComboBox"
        SelectionChanged="BreedFilterComboBox_SelectionChanged"
        ...
    ```

1. В классе главного окна в обработчике события выбора породы (*BreedFilterComboBox_SelectionChanged*) запоминаем выбранную породу

    ```cs
    selectedBreed = (BreedFilterComboBox.SelectedItem as CatBreed).title;
    ```

    Свойство *BreedFilterComboBox.SelectedItem* содержит выбранный элемент списка, в нашем случае это объект типа **CatBreed**.

Если сейчас запустить приложение, то выпадающий список будет отображаться, но реации на выбор не будет - дело в том, что визуальная часть не знает, что данные изменились. В одной из прошлых лекции мы упоминали про интерфейс **INotifyPropertyChanged** - реализуем его для нашего окна:

1. Добавляем интерфейс окну

    ```cs
    public partial class MainWindow : Window, INotifyPropertyChanged
                                            ^^^^^^^^^^^^^^^^^^^^^^^^
    ```

1. Реализуем интерфейс

    ```cs
    public event PropertyChangedEventHandler PropertyChanged;
    ```

1. Пишем метод, который будет сообщать визуальной части что изменился список кошек

    ```cs
    private void Invalidate()
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs("catList"));
    }
    ```

1. В обработчик события выбора породы добавим вызов этого метода

    ```cs
    private void BreedFilterComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        selectedBreed = (BreedFilterComboBox.SelectedItem as CatBreed).title;
        Invalidate();
    }
    ```

## Фильтрация по условию

Иногда встречается требование сделать фильтр по условию. Сам принцип фильтрации остается прежним, только список элементов фильтра формируется программно.

Например, сделаем фильтр по возрасту кошек: котята (до года), молодые (1-10) и старые (>10)

1. Для начала сделаем класс для элементов фильтра:

    ```cs
    public class CatAge { 
        public string title { get; set; }
        public int ageFrom { get; set; }
        public int ageTo { get; set; }
    }
    ```

1. Затем создадим список и переменную для хранения выбранного элемента списка. Обратите внимание, тут мы храним не строку, а весь объект.

    ```cs
    private CatAge selectedAge = null;
    public IEnumerable<CatAge> catAgeList { get; set; } = new CatAge[]{
        new CatAge{title="Все возраста", ageFrom=0, ageTo=99},
        new CatAge{title="Котята", ageFrom=0, ageTo=1},
        new CatAge{title="Молодые", ageFrom=1, ageTo=10},
        new CatAge{title="Старые", ageFrom=10, ageTo=99}
    };
    ```

1. В разметке меняем привязку (аттрибут _ItemsSource_)

    ```xml
    <ComboBox
        Name="BreedFilterComboBox"
        SelectionChanged="BreedFilterComboBox_SelectionChanged"
        VerticalAlignment="Center"
        MinWidth="100"
        SelectedIndex="0"
        ItemsSource="{Binding catAgeList}">

        <ComboBox.ItemTemplate>
            <DataTemplate>
                <Label 
                    Content="{Binding title}"/>
            </DataTemplate>
        </ComboBox.ItemTemplate>
    </ComboBox>
    ```

1. В обработчике события выбора элемента списка просто запоминаем выбранный элемент

    ```cs
    private void BreedFilterComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        selectedAge = BreedFilterComboBox.SelectedItem as CatAge;
        Invalidate();
    }
    ```

1. И меняем геттер списка кошек

    ```cs
    get
    {
        return _catList
            .Where(c=>(c.age >= selectedAge.ageFrom && c.age<selectedAge.ageTo));
    }
    ```

---

## Задание

Реализовать пример из лекции в проекте "кошки"

[Каркас приложения. Модель данных. Привязка данных. Табличный вывод.](./wpf_template.md) | [Содержание](../readme.md) | [Поиск, сортировка](./wpf_search_sort.md)
