Каркас приложения. Модель данных. Привязка данных. Табличный вывод. | Содержание | Поиск, сортировка
В приложениях часто требуется отфильтровать данные либо по словарному полю, либо по каким-либо условиям.
Суть фильтрации сводится к тому, что отображается не полный список объектов ("кошек"), а отфильтрованный по словарному полю (тип, категория...). Для получения фильтрованного списка реализуем геттер и сеттер для списка кошек:
Запись типа
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, а при чтении будет возвращаться отфильтрованный список
При работе с БД у нас обычно есть отдельные модели (таблицы) справочников - реализуем в нашем поставщике данных метод, возвращающий справочник пород:
Сначала создадим класс для элемента справочника (по идее нам было бы достаточно просто массива строк, но мы сразу будем делать "по-взрослому", чтобы сразу пройтись по всем "граблям", которые встретятся при работе с базой данных)
public class CatBreed { public string title { get; set; } }Создаем в классе главного окна свойство для хранения справочника
public List<CatBreed> catBreedList { get; set; }Здесь мы выбрали тип List, т.к. нам нужен изменяемый список, в который мы добавим элемент "Все породы"
В интерфейс поставщика данных (IDataProvider) добавляем декларацию метода для получения списка пород
IEnumerable<CatBreed> getCatBreeds();Реализуем этот метод в 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"} ]Получаем список пород и добавляем в начало "Все породы", чтобы можно было отменить фильтрацию и отображать полный список
// добавляем в класс переменую для хранения текущё выбранной породы private string selectedBreed = "Все породы";// в конструкторе получаем список пород catBreedList = Globals.dataProvider.getCatBreeds().ToList(); // и добавляем в начало "все породы" catBreedList.Insert(0, new CatBreed { title = selectedBreed });Теперь, имея список пород, добавляем в разметку (файл
.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; }Добавим в 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));} ```
Задание
Реализовать пример из лекции в проекте "кошки"
Каркас приложения. Модель данных. Привязка данных. Табличный вывод. | Содержание | Поиск, сортировка