wpf_filtering.md 16 KB

Каркас приложения. Модель данных. Привязка данных. Табличный вывод. | Содержание | Поиск, сортировка

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

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

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

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

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

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

>private IEnumerable<Cat> _catList = null;
>public IEnumerable<Cat> catList {
>   get
>   {
>       return _catList;
>   }
>   set {
>       _catList = value;
>   } 
>}
>```
>То есть создаётся приватная переменная для хранения реального значения свойства и методы **get** и **set** для, соответственно, получения и сохранения значения свойства. "value" это новое значение свойства, устанавливаемое при присваивании.

cs private IEnumerable _catList = null; public IEnumerable catList {

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

} ```

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

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

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

    public class CatBreed { 
        public string title { get; set; }
    }
    
  2. Создаем в классе главного окна свойство для хранения справочника

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

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

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

    IEnumerable<CatBreed> getCatBreeds();
    
  4. Реализуем этот метод в LocalDataProvider

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

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

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

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

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

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

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

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

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

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

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

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

        [
            {"breed": {"title": "Порода №1"}, "name": "Имя1"}, 
            {"breed": {"title": "Порода №2"}, "name": "Имя2"}, 
            {"breed": {"title": "Порода №1"}, "name": "Имя3"} 
        ]
        
      • метод Select преобразует элемент массива в новый объект, и т.к. мы из всего объекта вернули только одно поле, то на выходе у нас будет список пород IEnumerable<CatBreed>, содержащий все породы из спика кошек:

        [
            {"title": "Порода №1"},
            {"title": "Порода №2"},
            {"title": "Порода №1"}
        ]
        
      • метод DistinctBy выбирает записи с уникальным значением породы

        [
            {"title": "Порода №1"},
            {"title": "Порода №2"}
        ]
        
  5. Получаем список пород и добавляем в начало "Все породы", чтобы можно было отменить фильтрацию и отображать полный список

    // добавляем в класс переменую для хранения текущё выбранной породы
    private string selectedBreed = "Все породы";
    
    // в конструкторе получаем список пород
    catBreedList = Globals.dataProvider.getCatBreeds().ToList();
    // и добавляем в начало "все породы"
    catBreedList.Insert(0, new CatBreed { title = selectedBreed });
    
  6. Теперь, имея список пород, добавляем в разметку (файл .xaml) выпадающий список для выбор породы (во WrapPanel):

    <Label 
        Content="Порода:"
        VerticalAlignment="Center"/>
    
    <ComboBox
        Name="BreedFilterComboBox"
        VerticalAlignment="Center"
        MinWidth="150"
        SelectedIndex="0"
        ItemsSource="{Binding catBreedList}"
    />
    

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

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

    Элемент ComboBox предназначен для отображения списка строк, т.е. где-то под капотом он для элемента списка вызывает метод ToString, а этот матод как раз и возвращает название класса.

    Для того, чтобы отобразить элементы другого типа данных можно использовать два варината:

    • Использовать шаблон ComboBox.ItemTemplate, в котором можно реализовать произвольный вид элемента списка (вставить картинки, раскрасить и т.п.) и, в нашем случае, в качестве содержимого выбрать строковое свойство объекта для отображения.

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

      public class CatBreed
      {
          public string title { get; set; }
          public override string ToString() => title;
      }
      
  7. Добавим в ComboBox обработчик события выбора: SelectionChanged

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

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

    } ```


    Задание

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

    Каркас приложения. Модель данных. Привязка данных. Табличный вывод. | Содержание | Поиск, сортировка