Просмотр исходного кода

пагинация, поиск, фильтр, сортровка

Evgeny Kolesnikov 4 лет назад
Родитель
Сommit
0f2c111107
4 измененных файлов с 424 добавлено и 5 удалено
  1. 15 1
      articles/cs_layout.md
  2. 7 4
      articles/cs_mysql_connection2.md
  3. 401 0
      articles/cs_pagination.md
  4. 1 0
      readme.md

+ 15 - 1
articles/cs_layout.md

@@ -3,13 +3,19 @@
 </a></td><td style="width: 20%;">
 <a href="../readme.md">Содержание
 </a></td><td style="width: 40%;">
-<a href="../articles/cs_layout.md">Вывод данных согласно макету (ListView, Image).
+<a href="../articles/cs_pagination.md">Пагинация, сортировка, фильтрация, поиск
 </a></td><tr></table>
 
 # Вывод данных согласно макету (ListView, Image).
 
 >Напоминаю как выглядит макет списка продукции
 >![](../img/product_list_layout.jpg)
+>
+>Критерий | Баллы
+>---------|:-----:
+>Список продукции отображается в соответствии с макетом | 0.5
+>У каждой продукции в списке отображается изображение | 0.3
+>При отсутствии изображения отображается картинка-заглушка из ресурсов | 0.3
 
 Для создания такого макета используется элемент **ListView**
 
@@ -156,3 +162,11 @@ public string TypeAndName {
 Теперь окно должно выглядеть как положено:
 
 ![](../img/01067.png)
+
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/cs_mysql_connection2.md">Создание подключения к БД MySQL. Получение данных с сервера.
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/cs_pagination.md">Пагинация, сортировка, фильтрация, поиск
+</a></td><tr></table>

+ 7 - 4
articles/cs_mysql_connection2.md

@@ -266,10 +266,13 @@ public class Product
     ![](../img/01063.png)
 
 >Просто отобразив список материалов и стоимость вы уже заработаете 2 балла:
->* Список продукции загружается из БД: 0.5
->* Выводится информация по продукции (тип,наименование, артикул): 0.3
->* Выводится список материалов для каждой продукции: 0.5
->* Стоимость продукции рассчитана исходя из используемых материалов: 0.7
+>
+>Критерий | Баллы
+>---------|:-----:
+>Список продукции загружается из БД | 0.5
+>Выводится информация по продукции (тип,наименование, артикул) | 0.3
+>Выводится список материалов для каждой продукции | 0.5
+>Стоимость продукции рассчитана исходя из используемых материалов | 0.7
 
 ## Получение связанных данных (словари и связи многие-ко-многим)
 

+ 401 - 0
articles/cs_pagination.md

@@ -0,0 +1,401 @@
+<table style="width: 100%;"><tr><td style="width: 40%;">
+<a href="../articles/cs_layout.md">Вывод данных согласно макету (ListView, Image)
+</a></td><td style="width: 20%;">
+<a href="../readme.md">Содержание
+</a></td><td style="width: 40%;">
+<a href="../articles/cs_pagination.md">Пагинация
+</a></td><tr></table>
+
+Продолжаем реализовывать макет
+
+* [Пагинация](#пагинация)
+* [Сортировка](#сортировка)
+* [Фильтрация](#фильтрация)
+* [Поиск](#поиск)
+
+# Пагинация
+
+>В случае если в базе более 20 продуктов, то вывод должен осуществляться постранично (по 20 продуктов на страницу). Для удобства навигации по страницам необходимо вывести список их номеров (как на макете) с возможностью перехода к выбранной странице, а также предусмотреть переходы к предыдущей и следующей страницам.
+>
+>![](../img/product_list_layout.jpg)
+
+Критерий | Баллы
+---------|:---:
+Данные выводятся постранично | 1
+Выводится по 20 записей на странице | 0.2
+Выводится список номеров страниц | 0.5
+Реализован переход на выбранную в списке страницу | 0.3
+Присутсвует возможность перемещаться на предыдущую и следующую страницу | 0.5 
+**Итого** | **2.5**
+
+## Постраничный вывод данных
+
+Тут всё просто. Нам в любом случае придется делать геттер для фильтрованного списка продукции. Сразу в этом геттере и сделаем выборку данных порциями. Для этого используются LINQ-запросы **Skip(N)** (пропустить) и **Take(N)** (получить), где N - количество пропускаемых и выбираемых элементов соответсвенно.
+
+```cs
+// тут у нас будет храниться полный список продукции
+private IEnumerable<Product> _ProductList;
+// тут мы храним текущую страницу
+private int _CurrentPage = 0;
+
+// при смене текущей страницы мы должны перерисовать список (вспоминайте пр INotifyPropertyChanged)
+private int CurrentPage {
+    get {
+        return _CurrentPage;
+    }
+    set {
+        _CurrentPage = value;
+        Invalidate();
+    }
+}
+
+// и реализуем геттер и сеттер списка продукции
+public IEnumerable<Product> ProductList { 
+    get {
+        return _ProductList.Skip(20 * CurrentPage).Take(20);
+    } 
+    set {
+        _ProductList = value;
+        Invalidate();
+    }
+}
+```
+
+## Динамический вывод номеров страниц 
+
+В принципе можно руками в разметке нарисовать эти элементы, и может даже эксперты не обратят на это внимания. Но рассмотрим всё-таки правильный вариант.
+
+1. В разметку страницы в правую сетку под **ListView** добавьте **пустой** именованный **StackPanel** (горизонтальный с выравниванием по правому краю)
+
+    ```xml
+    <StackPanel 
+        x:Name="Paginator"
+        Margin="5"
+        Grid.Row="2" 
+        HorizontalAlignment="Right" 
+        Orientation="Horizontal"/>
+    ```
+
+2. Теперь в сеттере списка продукции динамически создадим текстовые блоки
+
+    Таким образом, при любом изменении списка продукции будут перерисовываться и номера страниц
+
+    ```cs
+    set {
+        // это остаётся как есть
+        _ProductList = value;
+
+        // очищаем содержимое пагинатора
+        Paginator.Children.Clear();
+
+        // добавляем переход на предыдущую страницу
+        Paginator.Children.Add(new TextBlock { Text = " < " });
+
+        // в цикле добавляем страницы
+        for (int i = 1; i < _ProductList.Count()/20; i++)
+            Paginator.Children.Add(
+                new TextBlock { Text = " "+i.ToString()+" " });
+
+        // добавляем переход на следующую страницу
+        Paginator.Children.Add(new TextBlock { Text = " > " });
+
+        // проходимся в цикле по всем сохданным элементам и задаем им обработчик PreviewMouseDown
+        foreach (TextBlock tb in Paginator.Children)
+            tb.PreviewMouseDown += PrevPage_PreviewMouseDown;
+    }
+    ```
+
+    >Можно в разметке временно создать один **TextBlock**, создать для него обработчик нажатия мыши и потом его и использовать.
+
+3. И реализуем обработчик нажатия мыши на номерах страниц
+
+    ```cs
+    private void PrevPage_PreviewMouseDown(object sender, MouseButtonEventArgs e)
+    {
+        switch ((sender as TextBlock).Text)
+        {
+            case " < ":
+                // переход на предыдущую страницу с проверкой счётчика
+                if (CurrentPage > 0) CurrentPage--;
+                return;
+            case " > ":
+                // переход на следующую страницу с проверкой счётчика
+                if (CurrentPage < _ProductList.Count() / 20) CurrentPage++;
+                return;
+            default:
+                // в остальных элементах просто номер странцы
+                // учитываем, что надо обрезать пробелы (Trim)
+                // и то, что номера страниц начинаются с 0
+                CurrentPage = Convert.ToInt32(
+                    (sender as TextBlock).Text.Trim() )-1;
+                return;
+        }   
+    }
+    ```
+
+# Сортировка
+
+>Пользователь должен иметь возможность отсортировать продукцию (по возрастанию и убыванию) по следующим параметрам: наименование, номер производственного цеха и минимальная стоимость для агента. Выбор сортировки должен быть реализован с помощью выпадающего списка. 
+
+Критерий | Баллы
+---------|:---:
+Реализована сортировка по названию продукции | 0.2
+Реализована сортировка по номеру цеха | 0.2
+Реализована сортировка по минимальной стоимости для агента | 0.2
+Выбор сортировки реализован с помощью выпадающего списка | 0.5
+Сортировка работает в реальном времени | 0.2 
+**Итого** | **1.3**
+
+В прошлом году мы делали выбор сортировки радио-кнопками, но такой вариант подходит только если сортировка по одному критерию, а у нас их три...
+
+1. Создаем массив со списком типов сортировок
+
+    ```cs
+    public string[] SortList { get; set; } = {
+        "Без сортировки",
+        "название по убыванию",
+        "название по возрастанию",
+        "номер цеха по убыванию",
+        "номер цеха по возрастанию",
+        "цена по убыванию",
+        "цена по возрастанию" };
+    ```
+
+2. В разметке добавляем элемент выпадающий список (**ComboBox**)
+
+    ```xml
+    <WrapPanel 
+        Orientation="Horizontal" 
+        ItemHeight="50">
+        <Label 
+            Content="Сортировка: "
+            Margin="10,0,0,0"
+            VerticalAlignment="Center"/>
+        <ComboBox
+            Name="SortTypeComboBox"
+            SelectedIndex="0"
+            VerticalContentAlignment="Center"
+            MinWidth="200"
+            SelectionChanged="SortTypeComboBox_SelectionChanged"
+            ItemsSource="{Binding SortList}"/>
+    </WrapPanel>    
+    ```
+
+3. Реализуем обработчик выбора из списка
+
+    Запоминаем ИНДЕКС выбранного элемента
+
+    ```cs
+
+    private int SortType = 0;
+
+    private void SortTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        SortType = SortTypeComboBox.SelectedIndex;
+        Invalidate();
+    }
+    ```
+
+4. И дорабатываем геттер списка продукции
+
+    ```cs
+    get {
+        var Result = _ProductList;
+
+        switch (SortType)
+        {
+            // сортировка по названию продукции
+            case 1:
+                Result = Result.OrderBy(p => p.Title);
+                break;
+            case 2:
+                Result = Result.OrderByDescending(p => p.Title);
+                break;
+            // остальные сортировки реализуйте сами
+        }
+        return Result.Skip(20 * CurrentPage).Take(20);
+    } 
+    ```
+
+# Фильтрация
+
+>Кроме этого, пользователь должен иметь возможность отфильтровать данные по типу продукта. Все типы из базы данных должны быть выведены в выпадающий список для фильтрации. Первым элементом в выпадающем списке должен быть “Все типы”, при выборе которого настройки фильтра сбрасываются.
+
+Критерий | Баллы
+---------|:---:
+Для фильтрации используется выпадающий список с типами продукции | 0.3
+Первый элемент в списке "Все типы" | 0.2
+Реализована фильтрация | 0.4
+Фильтрация работает в реальном времени | 0.2
+**Итого** | **1.1**
+
+Список типов продукции мы должны загрузить из базы данных. Если у вас ещё нет модели **ProductType**, то реализуем её и метод для получения этого списка в интерфейсе **IDataProvider** и поставщике данных (здесь я это расписывать не буду, мы это уже рассматривали в одной из предыдущих лекций, когда обсуждали вариант с получением сводных с помощью LINQ-запросов)
+
+1. Создаем список продукции, заполняем его данными из базы и добавляем в начало пункт "Всети типы"
+
+    ```cs
+    public List<ProductType> ProductTypeList { get; set; }
+
+    ...
+
+    ProductTypeList = Globals.DataProvider.GetProductTypes().ToList();
+    ProductTypeList.Insert(0, new ProductType { Title = "Все типы" });
+    ```
+
+2. В разметке в верхнюю панель добавляем выпадающий список
+
+    ```xml
+    <Label Content="Тип продукции" VerticalAlignment="Center"/>
+    <ComboBox
+        Width="150"
+        x:Name="ProductTypeFilter"
+        VerticalAlignment="Center"
+        SelectedIndex="0"
+        SelectionChanged="ProductTypeFilter_SelectionChanged"
+        ItemsSource="{Binding ProductTypeList}">
+    </ComboBox>
+    ```
+
+    Элементами списка являются не строки, а объекты. В прошлом году я показывал как делать шаблон элемента списка, но как мне кажеться шаблон здесь излишен (его имеет смысл использовать если кроме названия выводится ещё что-то)
+
+    Для преобразования объекта в строку есть метод *Object.ToString()*, и так как все объекты являются потомками класса **Object**, то нам достаточно в модели **ProductType** перегрузить это метод:
+
+    ```cs
+    public override string ToString() {
+        return Title;
+    }
+    ```
+
+3. Реализуем обработчик выбора элемента фильтра    
+
+    ```cs
+    private int ProductTypeFilterId = 0;
+
+    private void ProductTypeFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        // запоминаем ID выбранного типа
+        ProductTypeFilterId = (ProductTypeFilter.SelectedItem as ProductType).ID;
+        Invalidate();
+    }
+    ```
+
+4. И опять дорабатываем геттер списка продукции
+
+    ```cs
+    ...
+    var Result = _ProductList;
+
+    if (ProductTypeFilterId > 0)
+        Result = Result.Where(
+            p => p.ProductTypeID == ProductTypeFilterId);
+
+    switch (SortType)
+    ...
+    ```
+
+# Поиск
+
+>Пользователь должен иметь возможность искать конкретную продукцию, используя поисковую строку.
+>
+>Поиск должен осуществляться по наименованию и описанию продукта.
+Поиск, сортировка и фильтрация должны происходить в реальном времени, без необходимости нажатия кнопки “найти”/”отфильтровать” и т.п. Фильтрация и поиск должны применяться совместно. Параметры сортировки, выбранные ранее пользователем, должны сохраняться и во время фильтрации с поиском.
+
+Критерий | Баллы
+---------|:---:
+Реализован поиск | 0.3
+Поиск работает одновременно по нескольким атрибутам | 0.2
+Фильтрация и поиск работают одновременно | 0.3
+Сортировка работает во время поиска и фильтрации | 0.3
+**Итого** | **1.1**
+
+1. В верхнюю панель добавляем текстовое поле для ввода строки поиска
+
+    ```xml
+    <Label 
+        Content="Поиск" 
+        VerticalAlignment="Center"/>
+    <TextBox
+        Width="200"
+        VerticalAlignment="Center"
+        x:Name="SearchFilterTextBox" 
+        KeyUp="SearchFilterTextBox_KeyUp"/>
+    ```
+
+2. В коде окна запоминаем вводимую строку
+
+    ```cs
+    private string SearchFilter="";
+    private void SearchFilterTextBox_KeyUp(object sender, KeyEventArgs e)
+    {
+        SearchFilter = SearchFilterTextBox.Text;
+        Invalidate();
+    }
+    ```
+3. И снова правим геттер списка продукции
+
+    ```cs
+    // ищем вхождение строки фильтра в названии и описании объекта без учета регистра
+    if (SearchFilter != "")
+        Result = Result.Where(
+            p => p.Title.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
+                p.Description.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0
+        );
+    ```
+
+---
+
+В принципе всё работает, но список страниц формируется по полному списку продукции и не учитывает фильтр и поиск - перенесём этот код из сеттера в геттер. 
+
+Ещё одна засада в том, что если мы находимся на последней странице и включаем фильтр, то сдвиг уходит за границы массива и отображается пустой список - т.е. перед выводом списка надо проверять валидность старицы
+
+Итоговый список продукции должен выглядеть примерно так:
+
+```cs
+public IEnumerable<Product> ProductList {
+    get {
+        var Result = _ProductList;
+
+        if (ProductTypeFilterId > 0)
+            Result = Result.Where(i => i.ProductTypeID == ProductTypeFilterId);
+
+        switch (SortType)
+        {
+            // сортировка по названию продукции
+            case 1:
+                Result = Result.OrderBy(p => p.Title);
+                break;
+            case 2:
+                Result = Result.OrderByDescending(p => p.Title);
+                break;
+                // остальные сортировки реализуйте сами
+
+        }
+
+        // ищем вхождение строки фильтра в названии и описании объекта без учета регистра
+        if (SearchFilter != "")
+            Result = Result.Where(
+                p => p.Title.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
+                        p.Description.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0
+            );
+
+        Paginator.Children.Clear();
+
+        Paginator.Children.Add(new TextBlock { Text = " < " });
+        for (int i = 1; i <= (Result.Count() / 20)+1; i++)
+            Paginator.Children.Add(new TextBlock { Text = " " + i.ToString() + " " });
+        Paginator.Children.Add(new TextBlock { Text = " > " });
+        foreach (TextBlock tb in Paginator.Children)
+            tb.PreviewMouseDown += PrevPage_PreviewMouseDown;
+
+        if (CurrentPage > Result.Count() / 20)
+            CurrentPage = Result.Count() / 20;
+
+        return Result.Skip(20 * CurrentPage).Take(20);
+    } 
+    set {
+        _ProductList = value;
+        Invalidate();
+    }
+}
+```
+

+ 1 - 0
readme.md

@@ -122,6 +122,7 @@ http://sergeyteplyakov.blogspot.com/2014/01/microsoft-fakes-state-verification.h
 
 2. [Вывод данных согласно макету (ListView, Image).](./articles/cs_layout.md)
 
+3. [Пагинация, сортировка, фильтрация, поиск](./articles/cs_pagination.md)
 <!--
 
 https://office-menu.ru/uroki-sql Уроки SQL