cs_pagination.md 19 KB

Продолжаем реализовывать макет

Пагинация

В случае если в базе более 20 продуктов, то вывод должен осуществляться постранично (по 20 продуктов на страницу). Для удобства навигации по страницам необходимо вывести список их номеров (как на макете) с возможностью перехода к выбранной странице, а также предусмотреть переходы к предыдущей и следующей страницам.

Вывод данных согласно макету (ListView, Image) Содержание Подсветка элементов по условию. Дополнительные выборки.
Критерий Баллы
Данные выводятся постранично 1
Выводится по 20 записей на странице 0.2
Выводится список номеров страниц 0.5
Реализован переход на выбранную в списке страницу 0.3
Присутсвует возможность перемещаться на предыдущую и следующую страницу 0.5
Итого 2.5

Постраничный вывод данных

Тут всё просто. Нам в любом случае придется делать геттер для фильтрованного списка продукции. Сразу в этом геттере и сделаем выборку данных порциями. Для этого используются LINQ-запросы Skip(N) (пропустить) и Take(N) (получить), где N - количество пропускаемых и выбираемых элементов соответсвенно.

// тут у нас будет храниться полный список продукции
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 (горизонтальный с выравниванием по правому краю)

    <StackPanel 
        x:Name="Paginator"
        Margin="5"
        Grid.Row="2" 
        HorizontalAlignment="Right" 
        Orientation="Horizontal"/>
    
  2. Теперь в сеттере списка продукции динамически создадим текстовые блоки

    Таким образом, при любом изменении списка продукции будут перерисовываться и номера страниц

    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. И реализуем обработчик нажатия мыши на номерах страниц

    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. Создаем массив со списком типов сортировок

    public string[] SortList { get; set; } = {
        "Без сортировки",
        "название по убыванию",
        "название по возрастанию",
        "номер цеха по убыванию",
        "номер цеха по возрастанию",
        "цена по убыванию",
        "цена по возрастанию" };
    
  2. В разметке добавляем элемент выпадающий список (ComboBox)

    <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. Реализуем обработчик выбора из списка

    Запоминаем ИНДЕКС выбранного элемента

    
    private int SortType = 0;
    
    private void SortTypeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SortType = SortTypeComboBox.SelectedIndex;
        Invalidate();
    }
    
  4. И дорабатываем геттер списка продукции

    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. Создаем список продукции, заполняем его данными из базы и добавляем в начало пункт "Всети типы"

    public List<ProductType> ProductTypeList { get; set; }
    
    ...
    
    ProductTypeList = Globals.DataProvider.GetProductTypes().ToList();
    ProductTypeList.Insert(0, new ProductType { Title = "Все типы" });
    
  2. В разметке в верхнюю панель добавляем выпадающий список

    <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 перегрузить это метод:

    public override string ToString() {
        return Title;
    }
    
  3. Реализуем обработчик выбора элемента фильтра

    private int ProductTypeFilterId = 0;
    
    private void ProductTypeFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        // запоминаем ID выбранного типа
        ProductTypeFilterId = (ProductTypeFilter.SelectedItem as ProductType).ID;
        Invalidate();
    }
    
  4. И опять дорабатываем геттер списка продукции

    ...
    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. В верхнюю панель добавляем текстовое поле для ввода строки поиска

    <Label 
        Content="Поиск" 
        VerticalAlignment="Center"/>
    <TextBox
        Width="200"
        VerticalAlignment="Center"
        x:Name="SearchFilterTextBox" 
        KeyUp="SearchFilterTextBox_KeyUp"/>
    
  2. В коде окна запоминаем вводимую строку

    private string SearchFilter="";
    private void SearchFilterTextBox_KeyUp(object sender, KeyEventArgs e)
    {
        SearchFilter = SearchFilterTextBox.Text;
        Invalidate();
    }
    
  3. И снова правим геттер списка продукции

    // ищем вхождение строки фильтра в названии и описании объекта без учета регистра
    if (SearchFilter != "")
        Result = Result.Where(
            p => p.Title.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0 ||
                p.Description.IndexOf(SearchFilter, StringComparison.OrdinalIgnoreCase) >= 0
        );
    

В принципе всё работает, но список страниц формируется по полному списку продукции и не учитывает фильтр и поиск - перенесём этот код из сеттера в геттер.

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

Итоговый список продукции должен выглядеть примерно так:

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();
    }
}
Вывод данных согласно макету (ListView, Image) Содержание Подсветка элементов по условию. Дополнительные выборки.