Parcourir la source

dapper sqlbuilder

Евгений Колесников il y a 1 an
Parent
commit
33c1420268

+ 0 - 232
articles/cs_layout.md

@@ -1,232 +0,0 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/2.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>
-
-# Вывод данных согласно макету (ListView, Image).
-
->Напоминаю как выглядит макет списка продукции
->![](../img/product_list_layout.jpg)
->
->Критерий | Баллы
->---------|:-----:
->Список продукции отображается в соответствии с макетом | 0.5
->У каждой продукции в списке отображается изображение | 0.3
->При отсутствии изображения отображается картинка-заглушка из ресурсов | 0.3
-
-Для создания такого макета используется элемент **ListView**
-
-В разметке вместо **DataGrid** вставляем **ListView**
-
-```xml
-<ListView
-    Grid.Row="1"
-    Grid.Column="1"
-    ItemsSource="{Binding ProductList}"
->
-
-</ListView>
-```
-
-Внутри него вставляем макет для элемента списка: пока у нас только прямоугольная рамка со скругленными углами (в этом макете вроде скрулять не надо, возможно осталось от другого шаблона)
-
-```xml
-<ListView.ItemTemplate>
-    <DataTemplate>
-        <Border 
-            BorderThickness="1" 
-            BorderBrush="Black" 
-            CornerRadius="5">
-
-            <!-- сюда потом вставить содержимое -->
-
-        </Border>
-    </DataTemplate>
-</ListView.ItemTemplate>                
-```
-
-Внутри макета вставляем **Grid** из трёх колонок: для картинки, основного содержимого и стоимости.
-
-```xml
-<Grid 
-    Margin="10" 
-    HorizontalAlignment="Stretch">
-
-    <Grid.ColumnDefinitions>
-        <ColumnDefinition Width="64"/>
-        <ColumnDefinition Width="*"/>
-        <ColumnDefinition Width="auto"/>
-    </Grid.ColumnDefinitions>
-
-    <!-- сюда потом вставить содержимое -->
-
-</Grid>
-```
-
-В первой колонке выводим изображение:
-
-```xml
-<Image
-    Width="64" 
-    Height="64"
-    Source="{Binding ImagePreview,TargetNullValue={StaticResource defaultImage}}" />
-```
-
-Обратите внимание, вместо поля *Image* я вывожу вычисляемое поле *ImagePreview* - в геттере проверяю есть ли такая картинка, т.к. наличие данных в базе не означает наличие файла на диске
-
-```cs
-public Uri ImagePreview {
-    get {
-        var imageName = Environment.CurrentDirectory + (Image ?? "");
-        return System.IO.File.Exists(imageName) ? new Uri(imageName) : null;
-    }
-}
-```
-
-1. Файлы подгружаемые с диска должны быть в формате **Uri**, иначе программа ищет их в ресурсах исполняемого файла
-2. К имени файла добавляю путь к текущему каталогу 
-3. Если такого файла нет, то возвращаю **null**, в этом случае срабатывает параметр привязки *TargetNullValue* - отображать изображение по-умолчанию.
-4. Изображение по-умолчанию задается в ресурсах окна (первый элемент окна)
-
-    ```xml
-    <Window.Resources>
-        <BitmapImage 
-            x:Key='defaultImage' 
-            UriSource='./Images/picture.png' />
-    </Window.Resources>
-    ```
-
-    тут, как раз, указывается путь к изображению в ресурсах (в моём случае в приложении сознад каталог `Image` и в него ЗАГРУЖЕН файл)
-
-Во второй колонке вывожу основную информацию о продукте: тип + название, ариткул и список материалов.
-
-Так как данные выводятся в несколько строк, то заворачиваю их в **StackPanel** (тут можно использовать и **Grid**, но их и так уже много в разметке)
-
-```xml
-<StackPanel
-    Grid.Column="1"
-    Margin="5"
-    Orientation="Vertical">
-
-    <TextBlock 
-        Text="{Binding TypeAndName}"/>
-
-    <TextBlock 
-        Text="{Binding ArticleNumber}"/>
-
-    <TextBlock 
-        Text="{Binding MaterialString}"/>
-</StackPanel>
-```
-
-Вообще выводимый текст можно форматировать, но чтобы не запоминать лишних сущностей можно нарисовать ещё один геттер *TypeAndName*
-
-```cs
-public string TypeAndName {
-    get {
-        return ProductTypeTitle+" | "+Title;
-    }
-}
-```
-
-Артикул и список материалов выводятся как есть
-
-На данный момент приложение выглядит примерно так
-
-![](../img/01066.png)
-
-Видно, что размер элемента зависит от содержимого.
-
-Чтобы это исправить нужно добавить в **ListView** стиль для элемента контейнера, в котором задать горизонтальное выравнивание по ширине:
-
-```xml
-<ListView
-    Grid.Row="1"
-    Grid.Column="1"
-    ItemsSource="{Binding ProductList}"
->
-    <ListView.ItemContainerStyle>
-        <Style 
-            TargetType="ListViewItem">
-            <Setter 
-                Property="HorizontalContentAlignment"
-                Value="Stretch" />
-        </Style>
-    </ListView.ItemContainerStyle>
-    ...
-```
-
-Теперь окно должно выглядеть как положено:
-
-![](../img/01067.png)
-
-# Вывод данных "плиткой"
-
-Такое задание было на одном из прошлых соревнований WorldSkills, вполне вероятно что появится и на демо-экзамене.
-
-Компоненты **ListBox** и **ListView** по умолчанию инкапсулируют все элементы списка в специальную панель **VirtualizingStackPanel**, которая располагает все элементы по вертикали. Но с помощью свойства **ItemsPanel** можно переопределить панель элементов внутри списка. 
-
-Мы будем использовать уже знакомую вам **WrapPanel**:
-
-```xml
-<ListView.ItemsPanel>
-    <ItemsPanelTemplate>
-        <WrapPanel 
-            HorizontalAlignment="Center" />
-    </ItemsPanelTemplate>
-</ListView.ItemsPanel>
-```
-
->Атрибут *HorizontalAlignment* используем, чтобы "плитки" центрировались.
-
-![](../img/01072.png)
-
-Как видим, элементы отображаются горизонтальным списком, но нет переноса. Для включения переноса элементов нужно в **ListView** отключить горизонтальный скролл, добавив атрибут `ScrollViewer.HorizontalScrollBarVisibility="Disabled"`:
-
-![](../img/01073.png)
-
-Свойство *ItemContainerStyle* уже не нужно и его можно убрать.
-
-Размеры наших элементов по-прежнему зависят от содержимого - тут надо править шаблон **ItemTemplate**.
-
-Итоговая разметка для вывода "плиткой" должна выглядеть примерно так:
-
-```xml
-<ListView
-    ItemsSource="{Binding ProductList}"
-    x:Name="ListView"
-    ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
->
-    <ListView.ItemsPanel>
-        <ItemsPanelTemplate>
-            <WrapPanel 
-                HorizontalAlignment="Center" />
-        </ItemsPanelTemplate>
-    </ListView.ItemsPanel>
-
-    <!--ListView.ItemContainerStyle>
-        <Style 
-            TargetType="ListViewItem">
-            <Setter 
-                Property="HorizontalContentAlignment"
-                Value="Stretch" />
-        </Style>
-    </-->
-    
-    
-    <ListView.ItemTemplate>
-        ...
-    </ListView.ItemTemplate>
-</ListView>    
-```
-
-<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>

+ 1 - 0
articles/cs_layout2.md

@@ -2,6 +2,7 @@
 :----------------:|:----------:|:----------------:
 [Создание подключения к БД MySQL. Получение данных с сервера.](./cs_mysql_connection3.md) | [Содержание](../readme.md#c-и-mysql) | [Пагинация, сортировка, фильтрация, поиск](./cs_pagination2.md)
 
+
 # Вывод данных согласно макету (ListBox, Image).
 
 >Напоминаю как выглядит макет списка продукции

+ 0 - 492
articles/cs_mysql_connection.md

@@ -1,492 +0,0 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/sql_import.md">Создание базы данных. Импорт данных.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/cs_mysql_connection.md">---
-</a></td><tr></table>
-
-# Создание подключения к БД MySQL. Получение данных с сервера.
-
-Дальше мы продолжим разбор задания прошлогоднего демо-экзамена. 
-
-Базу мы развернули и данные в неё импортировали, теперь начнём разбор второй сессии: создание desktop-приложения.
-
->## Разработка desktop-приложений
->
->### Список продукции
->
->Необходимо реализовать вывод продукции, которая хранится в базе данных, согласно предоставленному макету (файл `product_list_layout.jpg` находится в ресурсах). При отсутствии изображения необходимо вывести картинку-заглушку из ресурсов (picture.png).
->
->![](../img/product_list_layout.jpg)
->
->...
->
->Стоимость продукта должна быть рассчитана исходя из используемых материалов.
-
-По макету видно, что на первом экране уже нужны все данные, которые мы импортировали ранее: список продуктов (Product), список материалов (Material) продукта (через таблицу ProductMaterial).
-
-Сразу оговорюсь, что получать данные с сервера можно по-разному: можно через **DataAdapter** загрузить данные в **DataSet** и привязать его к компоненту отображающему данные:
-
-```cs
-private DataSet MyDataSet;
-...
-MySqlDataAdapter mda = new MySqlDataAdapter(
-        "SELECT * FROM Product", 
-        Connection);
-
-productAdapter.Fill(MyDataSet, "Product");
-
-ProductListView.DataContext = MyDataSet.Tables["Product"].DefaultView;
-```
-
-А можно используя **DataReader** заполнять список моделей ~~, что мы и будем дальше делать~~ Мы попробуем реализовать оба варианта.
-
->В рамках демо-экзамена требуется работать с моделями ("Основные сущности представлены отдельными классами", но стоит это всего 0,2 балла).
-
-## Реализация с помощью моделей и **DataReader**-а.
-
-Шаблон приложения берём из лекций прошлого года.
-
-Первым делом рисуем модели для данных. Если в прошлом году вы их разрабатывали сами, то сейчас придумывать ничего не надо - просто смотрим на структуру таблиц:
-
-### Модель "Продукт"
-
-![](../img/01061.png)
-
-```cs
-public class Product
-{
-    public int ID { get; set; }
-    public string Title { get; set; }
-    public int ProductTypeID { get; set; }
-    public string ArticleNumber { get; set; }
-    public string Description { get; set; }
-    public string Image { get; set; }
-    public int ProductionPersonCount { get; set; }
-    public int ProductionWorkshopNumber { get; set; }
-    public decimal MinCostForAgent { get; set; }
-}
-```
-
-### Модель "Материал"
-
-![](../img/01062.png)
-
-```cs
-public class Material
-{
-    public int ID { get; set; }
-    public string Title { get; set; }
-    public int CountInPack { get; set; }
-    public string Unit { get; set; }
-    public double CountInStock { get; set; }
-    public double MinCount { get; set; }
-    public string Description { get; set; }
-    public decimal Cost { get; set; }
-    public string Image { get; set; }
-    public int MaterialTypeID { get; set; }
-}
-```
-
-### Получение данных из базы
-
-1. Создаем интерфейс поставщика данных (пока только для продукции)
-
-    ```cs
-    interface IDataProvider
-    {
-        IEnumerable<Product> GetProducts();
-    }
-    ```
-
-2. Создаем класс **MySqlDataProvider**, реализующий этот интерфейс
-
-    ```cs
-    class MySQLDataProvider: IDataProvider
-    {
-        // соединение с базой данных
-        private MySqlConnection Connection;
-    ```
-
-    ![](../img/01060.png)
-
-
-    ```cs
-        // в конструкторе создаём подключение и сразу его открываем
-        public MySQLDataProvider()
-        {
-            try
-            {
-                Connection = new MySqlConnection("Server=kolei.ru;Database=ТУТ ВАША БАЗА;port=3306;UserId=ТУТ ВАШ ЛОГИН;password=ТУТ ПАРОЛЬ;");
-                Connection.Open();
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        // в деструкторе закрываем соединение
-        ~MySQLDataProvider()
-        {
-            Connection.Close();
-        }
-
-
-        // реализуем метод получения списка продукции
-        public IEnumerable<Product> GetProducts()
-        {
-            List<Product> ProductList = new List<Product>();
-
-            // выбираем ВСЕ записи
-            // в реальных приложенияъ этого делать, конечно нельзя
-            // но у нас базы маленькие, поэтому условиями не заморачиваемся
-            MySqlCommand Command = new MySqlCommand(
-                "SELECT * FROM Product", 
-                Connection);
-
-            // создаем DataReader, который и будет читать данные из базы
-            MySqlDataReader Reader = Command.ExecuteReader();
-
-            try
-            {
-                while(Reader.Read())
-                {
-                    // для каждой строки таблицы Product создаем экземпляр 
-                    // соответствующей модели, заполняем её
-                    Product NewProduct = new Product();
-                    NewProduct.ID = Reader.GetInt32("ID");
-                    NewProduct.Title = Reader.GetString("Title");
-                    NewProduct.ProductTypeID = Reader.GetInt32("ID");
-                    NewProduct.ArticleNumber = Reader.GetInt32("ID");
-                    NewProduct.ProductionPersonCount = Reader.GetInt32("ID");
-                    NewProduct.ProductionWorkshopNumber = Reader.GetInt32("ID");
-                    NewProduct.MinCostForAgent = Reader.GetInt32("ID");
-
-                    // Методы Get<T> не поддерживают работу с NULL
-                    // для полей, в которых может встретиться NULL (а лучше для всех)
-                    // используйте следующий синтаксис
-                    NewProduct.Description = Reader["Description"].ToString();
-                    NewProduct.Image = Reader["Image"].ToString();
-
-                    // и сохраняем в списке
-                    ProductList.Add(NewProduct);
-                }
-            }
-            catch (Exception)
-            {
-            }
-
-            return ProductList;
-    }
-    ```
-
-3. В конструкторе главного окна создаем поставщика данных и получаем с помощью него список продукции
-
-    ```cs
-    public MainWindow()
-    {
-        InitializeComponent();
-        DataContext = this;
-
-        Globals.DataProvider = new MySQLDataProvider();
-        var ProductList = Globals.DataProvider.GetProducts();
-        ...
-    ```
-
-4. В вёрстке главного окна пока выведем обычный **DataGrid**, чтобы проверить, всё-ли нормально
-
-    ```xml
-    <DataGrid
-        Grid.Row="1"
-        Grid.Column="1"
-        CanUserAddRows="False"
-        AutoGenerateColumns="False"
-        ItemsSource="{Binding ProductList}">
-        
-        <DataGrid.Columns>
-            <DataGridTextColumn
-                Header="Название"
-                Binding="{Binding Title}"/>
-            <DataGridTextColumn
-                Header="Артикул"
-                Binding="{Binding ArticleNumber}"/>
-            <DataGridTextColumn
-                Header="Описание"
-                Binding="{Binding Description}"/>
-        </DataGrid.Columns>
-    </DataGrid>
-    ```
-
-    ![](../img/01063.png)
-
-## Реализация с помощью **DataAdapter**
-
-Модели в этом варианте рисовать не надо, сразу делаем получение данных
-
-1. Интерфейс поставщика данных
-
-    ```cs
-    interface IDataProvider2
-    {
-        DataView GetProducts();
-    }
-    ```
-
-2. Класс **MySqlDataProvider2**, реализующий этот интерфейс.
-
-    Конструктор и деструктор не отличаются, а вот получение данных намного проще:
-
-    ```cs
-    class MySQLDataProvider2 : IDataProvider2
-    {
-        private MySqlConnection Connection;
-        private DataSet MyDataSet;
-
-        public MySQLDataProvider2()
-        {
-            try
-            {
-                Connection = new MySqlConnection("Server=kolei.ru;Database=ТУТ ВАША БАЗА;port=3306;UserId=ТУТ ВАШ ЛОГИН;password=ТУТ ПАРОЛЬ;");
-                MyDataSet = new DataSet();
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        public DataView GetProducts()
-        {
-            try
-            {
-                Connection.Open();
-                MySqlDataAdapter productAdapter = new MySqlDataAdapter(
-                    "SELECT FROM Product", Connection);
-                productAdapter.Fill(MyDataSet, "Product");
-                return MyDataSet.Tables["Product"].DefaultView;
-            }
-            finally
-            {
-                Connection.Close();
-            }
-        }
-    }
-    ```
-
-3. Конструктор главного экрана
-
-    ```cs
-    public MainWindow()
-    {
-        InitializeComponent();
-        DataContext = this;
-
-        Globals.DataProvider2 = new MySQLDataProvider2();
-
-        // данные привязываем к контексту визуального компонента
-        ProductListGrid.DataContext = Globals.DataProvider2.GetProducts();
-    }
-    ```
-
-4. Вёрстка практически не отличается, только в *ItemsSource* используем свойство *Table* класса **DataView**
-
-    ```xml
-    <DataGrid
-        Grid.Row="1"
-        Grid.Column="1"
-        CanUserAddRows="False"
-        AutoGenerateColumns="False"
-        Name="ProductListGrid"
-        ItemsSource="{Binding Table}">
-
-        <DataGrid.Columns>
-            <DataGridTextColumn
-                Header="Название"
-                Binding="{Binding Title}"/>
-            <DataGridTextColumn
-                Header="Артикул"
-                Binding="{Binding ArticleNumber}"/>
-            <DataGridTextColumn
-                Header="Описание"
-                Binding="{Binding Description}"/>
-        </DataGrid.Columns>
-    </DataGrid>
-    ```
-
-### Получение связанных данных (словари и связи многие-ко-многим)
-
-Нам нужно:
-
-* вывести тип продукта
-* подсчитать сумму материалов
-* вывести список материалов
-
-Для получения связанных данных из **DataSet**-а я нашел два варианта:
-
-* создание связей в таблицах набора данных и использование вычисляемых полей
-* использование конвертера данных
-
-### Cоздание связей в таблицах набора данных и использование вычисляемых полей
-
->На примере таблицы **ProductType**
-
-1. Загружаем таблицу типов продуктов в набор данных
-
-    ```cs
-    MySqlDataAdapter productTypeAdapter = new MySqlDataAdapter(
-        "SELECT * FROM ProductType", 
-        Connection);
-    productTypeAdapter.Fill(MyDataSet, "ProductType");
-    ```
-
-2. У нас теперь в наборе данных есть таблицы **Product** и **ProductType**, добавляем связь между ними:
-
-    В глобальном классе создаем свойство для хранения созданной связи
-
-    ```cs
-    // Globals.cs
-    public static DataRelation ProductTypeRelation;
-    ```
-
-    В конструкторе поставщика данных после создания набора данных добавляем связь
-
-    ```cs
-    Globals.ProductTypeRelation = MyDataSet.Relations.Add(
-        // название связи
-        "ProductTypeRelation", 
-        // родительское поле
-        MyDataSet.Tables["ProductType"].Columns["ID"], 
-        // потомок (child)
-        MyDataSet.Tables["Product"].Columns["ProductTypeID"]);
-    ```
-
-    И в таблицу продукции добавляем вычисляемый столбец
-
-    ```cs
-    DataColumn ProductTypeTitle = new DataColumn();
-    ProductTypeTitle.DataType = Type.GetType("System.String");
-    ProductTypeTitle.ColumnName = "ProductTypeTitle";
-    ProductTypeTitle.Expression = "Parent.Title";
-                                   ^^^^^^^^^^^^ - связь с "родителем"
-    MyDataSet.Tables["Product"].Columns.Add(ProductTypeTitle);
-    ```
-
-3. В разметке таблицы добавляем отображение вычисляемого поля
-
-    ```xml
-    <DataGridTextColumn
-        Header="Тип продукта"
-        Binding="{Binding ProductTypeTitle}"/>
-    ```
-
-Для словарей код ещё не очень сложный, но для того, чтобы добраться до суммы материалов нужно добавлять две связи (напомню, что там у нас связь многие-ко-многим) через промежуточную таблицу. И если для суммы материалов ещё более менее понятно как достать данные (хотя и муторно), то как получить список материалов я пока не представляю.
-
-### Использование конвертера данных
-
-1. В разметке таблицы добавляем поле с конвертером
-
-    Сначала описываем ресурс:
-
-    ```xml
-    <Window.Resources>
-        <local:ProductTypeConverter x:Key="myConverter"/>
-    </Window.Resources>
-    ```
-
-    Здесь **ProductTypeConverter** класс, который будет использоваться для конвертирования данных, **myConverter** - алиас этого конвертера для использования в разметке
-
-    И для вывода типа продукта используем конвертер
-
-    ```xml
-    <DataGridTextColumn
-        Header="Описание"
-        Binding="{Binding ProductTypeID,Converter={StaticResource myConverter}}"/>
-    ```
-
-2. Реализуем конвертер данных
-
-    ```cs
-    public class ProductTypeConverter : IValueConverter
-    {
-        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            // тут старый вариант, использующий связи
-            // return (value as DataRowView).Row.GetParentRow(Globals.ProductTypeRelation)["Title"];
-
-            // LINQ-запрос для получения названия типа продукта по его ID
-            var Title = Globals.MyDataSet.Tables["ProductType"].AsEnumerable()
-                        .Where(t => t.Field<int>("ID") == (int)value)
-                        .Select(t => t.Field<string>("Title"))
-                        .ToArray();
-            if (Title.Count() > 0) return Title[0];
-            return "";
-        }
-
-        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
-        {
-            throw new NotImplementedException();
-        }
-    }
-    ```
-
-И так для каждого вычисляемого поля...
-
-Например, так выглядит получение списка материалов (естественно, в набор данных надо загрузить таблицы **ProductMaterial** и **Material**)
-
-```cs
-public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
-{
-    // выбираем идентификаторы материалов
-    var MT = Globals.MyDataSet.Tables["ProductMaterial"].AsEnumerable()
-                .Where(t => t.Field<int>("ProductID") == (int)value)
-                .Select(t => t.Field<int>("MaterialID")) 
-                .ToList();
-
-    // выбираем названия материалов
-    var Materials = Globals.MyDataSet.Tables["Material"].AsEnumerable()
-                .Where(t => MT.Contains(t.Field<int>("ID")))
-                .Select(t => t.Field<string>("Title"));
-
-    var res = "";
-
-    foreach (string m in Materials)
-        res += "," + m;
-
-    return res;
-}
-```
-
-![](../img/01064.png)
-
-Для получения суммы материалов вместо *Select* надо использовать метод *Sum*
-
-При реализации через классы, достаточно объявить атрибут класса, в геттере которого и реализовать LINQ-запрос для получения связанных данных.
-
-```sql
-select 
-	p.*,
-	pt.Title as ProductTypeTitle,
-	pp.materials, pp.total
-from 
-	Product p
-left join 
-	ProductType pt on p.ProductTypeID = pt.ID 
-left JOIN 
-	(select 
-        pm.ProductID, 
-        GROUP_CONCAT(m.Title separator ', ') as materials, 
-        sum(pm.Count * m.Cost/m.CountInPack) as total
-	from 
-	    Material m,
-		ProductMaterial pm
-	where m.ID=pm.MaterialID
-	group by ProductID) pp on pp.ProductID = p.ID
-```
-
-<!-- 
-
-https://docs.microsoft.com/ru-ru/dotnet/api/system.data.dataset?view=net-5.0
-
-https://metanit.com/sharp/adonet/3.7.php
-
--->

+ 0 - 409
articles/cs_mysql_connection2.md

@@ -1,409 +0,0 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/sql_import.md">Создание базы данных. Импорт данных.
-</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></td><tr></table>
-
-Дальше мы продолжим разбор задания прошлогоднего демо-экзамена. 
-
-Базу мы развернули и данные в неё импортировали, теперь начнём разбор второй сессии: создание desktop-приложения.
-
->## Разработка desktop-приложений
->
->### Список продукции
->
->Необходимо реализовать вывод продукции, которая хранится в базе данных, согласно предоставленному макету (файл `product_list_layout.jpg` находится в ресурсах). При отсутствии изображения необходимо вывести картинку-заглушку из ресурсов (picture.png).
->
->![](../img/product_list_layout.jpg)
->
->...
->
->Стоимость продукта должна быть рассчитана исходя из используемых материалов.
-
-# Создание подключения к БД MySQL. Получение данных с сервера.
-
-По макету видно, что на первом экране уже нужны почти все данные, которые мы импортировали ранее: наименование продукта и артикул (таблица **Product**), тип продукта (**ProductType**), список материалов и стоимость материалов (**Material** через **ProductMaterial**).
-
-Есть несколько вариантов работы с данными:
-
-* Фреймворки (библиотеки) - предпочтительный вариант при реальной разработке. Есть платные и есть бесплатная **Entity**, входящая в состав **Visual Studio**. Но у **Entity** есть два минуса. Во-первых, она криво работает с **MySQL** (по крайней мере в прошлом году я так и не смог их подружить). Во-вторых, её не понятно как тестировать (в **Visual Studio** есть технологии для эмуляции тестового окружения, но только в **Enterprise** версии)
-
-* Загрузака с помощью **DataAdapter** в наборы данных (**DataSet**). Для наборов данных можно даже установить связи между таблицами. Эта технология широко применялась в эпоху **Windows Forms**
-
-* **WPF** "заточена" на работу с объектами. И так как от **Entity** мы отказались, то будем вручную создавать модели и грузить в них данные с помощью **DataReader**-a
-
-## Создание моделей
-
-Шаблон приложения [берём](https://github.com/kolei/OAP/blob/master/articles/wpf_template.md) из лекций прошлого года.
-
-Первым делом рисуем модели для данных. Если в прошлом году вы их разрабатывали сами, то сейчас придумывать ничего не надо - просто смотрим на структуру таблиц.
-
-**Но**, для главного экрана мы можем использовать **сводную** таблицу, в которой будут все нужные нам данные!!!
-
-```sql
-SELECT 
-    -- выбираем все поля из таблицы Product
-	p.*,
-    -- выбираем название продукта
-	pt.Title AS ProductTypeTitle,
-    -- выбираем список материалов и их сумму
-	pp.MaterialList, pp.Total
-FROM
-	Product p
-LEFT JOIN
-	ProductType pt ON p.ProductTypeID = pt.ID 
-LEFT JOIN 
-    -- для выбора информации о метериалах используем подзапрос, 
-    -- т.к. она связана через промежуточную таблицу
-	(
-        SELECT
-            pm.ProductID, 
-            -- GROUP_CONCAT объединяет указанные поля разделяя из сеператором
-            GROUP_CONCAT(m.Title SEPARATOR ', ') as MaterialList, 
-            SUM(pm.Count * m.Cost/m.CountInPack) as Total
-	    FROM
-	        Material m,
-		    ProductMaterial pm
-	    WHERE m.ID=pm.MaterialID
-	    GROUP BY ProductID
-    ) pp ON pp.ProductID = p.ID
-```
-
-Приведенный запрос выбирает все продукты, добавляет поле *тип продукта* из словаря и поля *список материалов* и *стоимость материалов* из подзапроса, который использует таблицу связей и таблицу материалов
-
->Про **LEFT JOIN** я раньше не рассказывал, в принципе это аналог добавления второй таблицы в секцию **FROM**, только условие добавления описывается не в секции **WHERE** а в параметре **ON**
-
-### Модель "Продукт" (с данными о материалах)
-
-![](../img/01061.png)
-
-```cs
-public class Product
-{
-    public int ID { get; set; }
-    public string Title { get; set; }
-    public int ProductTypeID { get; set; }
-    public string ArticleNumber { get; set; }
-    public string Description { get; set; }
-    public string Image { get; set; }
-    public int ProductionPersonCount { get; set; }
-    public int ProductionWorkshopNumber { get; set; }
-    public decimal MinCostForAgent { get; set; }
-
-    // данные из сводной таблицы
-    public string ProductTypeTitle { get; set; }
-    public string MaterialList { get; set; }
-    public string Total { get; set; }
-}
-```
-
->Если вы не уверены, что сможете написать сводный SQL-запрос, то описывайте остальные модели. Мы потом разберем, как извлечь сводные данные с помощью LINQ-запросов. Но на демо-экзамене можете сразу остальные модели не делать, а только если останется время, т.к. основная работа будет над таблицей **Product**
-
-## Получение данных из базы
-
-1. Создаем интерфейс поставщика данных (он в шаблоне приложения уже есть)
-
-    ```cs
-    interface IDataProvider
-    {
-        IEnumerable<Product> GetProducts();
-    }
-    ```
-
-2. Создаем класс **MySqlDataProvider**, реализующий этот интерфейс
-
-    ```cs
-    class MySQLDataProvider: IDataProvider
-    {
-        // соединение с базой данных
-        private MySqlConnection Connection;
-    ```
-
-    ![](../img/01060.png)
-
-
-    ```cs
-        // в конструкторе создаём подключение
-        public MySQLDataProvider()
-        {
-            try
-            {
-                Connection = new MySqlConnection(
-                    "Server=kolei.ru;Database=ТУТ ВАША БАЗА;port=3306;UserId=ТУТ ВАШ ЛОГИН;password=ТУТ ПАРОЛЬ;");
-            }
-            catch (Exception)
-            {
-            }
-        }
-
-        public IEnumerable<Product> GetProducts()
-        {
-            List<Product> ProductList = new List<Product>();
-            string Query = @"SELECT 
-                p.*,
-                pt.Title AS ProductTypeTitle,
-                pp.MaterialList, pp.Total
-            FROM
-                Product p
-            LEFT JOIN
-                ProductType pt ON p.ProductTypeID = pt.ID
-            LEFT JOIN
-                (
-                SELECT
-                    pm.ProductID,
-                    GROUP_CONCAT(m.Title SEPARATOR ', ') as MaterialList, 
-                    SUM(pm.Count * m.Cost / m.CountInPack) as Total
-                FROM
-                    Material m,
-		            ProductMaterial pm
-                WHERE m.ID = pm.MaterialID
-                GROUP BY ProductID
-                ) pp ON pp.ProductID = p.ID";
-
-            try
-            {
-                // открываем соединение с сервером
-                Connection.Open();
-                try
-                {
-                    // создаем команду
-                    MySqlCommand Command = new MySqlCommand(Query, Connection);
-                    // получаем результат команды (массив строк)
-                    MySqlDataReader Reader = Command.ExecuteReader();
-
-                    // перебираем стоки
-                    while (Reader.Read())
-                    {
-                        // создаем экземпляр класса 
-                        Product NewProduct = new Product();
-                        // и заполняем его поля
-                        NewProduct.ID = Reader.GetInt32("ID");
-                        NewProduct.Title = Reader.GetString("Title");
-                        NewProduct.ProductTypeID = Reader.GetInt32("ProductTypeID");
-                        NewProduct.ArticleNumber = Reader.GetString("ArticleNumber");
-                        NewProduct.ProductionPersonCount = Reader.GetInt32("ProductionPersonCount");
-                        NewProduct.ProductionWorkshopNumber = Reader.GetInt32("ProductionWorkshopNumber");
-                        NewProduct.MinCostForAgent = Reader.GetInt32("MinCostForAgent");
-
-                        // Методы Get<T> не поддерживают работу с NULL
-                        // для полей, в которых может встретиться NULL (а лучше для всех)
-                        // используйте следующий синтаксис
-                        NewProduct.Description = Reader["Description"].ToString();
-                        NewProduct.Image = Reader["Image"].ToString();
-
-                        NewProduct.ProductTypeTitle = Reader["ProductTypeTitle"].ToString();
-                        NewProduct.MaterialString = Reader["MaterialList"].ToString();
-                        NewProduct.Total = Reader["Total"].ToString();
-
-                        // добавляем экземпляр класса в список продуктов
-                        ProductList.Add(NewProduct);
-                    }
-                }
-                finally
-                {
-                    // обязательно закрываем соединение
-                    // ресурсы сервера конечны
-                    Connection.Close();
-                }
-            }
-            catch (Exception)
-            {
-            }
-
-            return ProductList;
-        }
-    }
-    ```
-
-3. В конструкторе главного окна создаем поставщика данных и получаем с помощью него список продукции
-
-    ```cs
-    public IEnumerable<Product> ProductList { get; set; }
-
-    public MainWindow()
-    {
-        InitializeComponent();
-        DataContext = this;
-
-        Globals.DataProvider = new MySQLDataProvider();
-        ProductList = Globals.DataProvider.GetProducts();
-        ...
-    ```
-
-4. В вёрстке главного окна пока выведем обычный **DataGrid**, чтобы проверить, всё-ли нормально
-
-    ```xml
-    <DataGrid
-        Grid.Row="1"
-        Grid.Column="1"
-        CanUserAddRows="False"
-        AutoGenerateColumns="False"
-        ItemsSource="{Binding ProductList}">
-
-        <DataGrid.Columns>
-            <DataGridTextColumn
-                Header="Название"
-                Binding="{Binding Title}"/>
-            <DataGridTextColumn
-                Header="Артикул"
-                Binding="{Binding ArticleNumber}"/>
-            <DataGridTextColumn
-                Header="Тип продукта"
-                Binding="{Binding ProductTypeTitle}"/>
-            <DataGridTextColumn
-                Header="Стоимость материалов"
-                Binding="{Binding Total}"/>
-            <DataGridTextColumn
-                Header="Используемые материалы"
-                Binding="{Binding MaterialString}"/>
-
-        </DataGrid.Columns>
-    </DataGrid>
-    ```
-
-    ![](../img/01063.png)
-
->Просто отобразив список материалов и стоимость вы уже заработаете 2 балла:
->
->Критерий | Баллы
->---------|:-----:
->Список продукции загружается из БД | 0.5
->Выводится информация по продукции (тип,наименование, артикул) | 0.3
->Выводится список материалов для каждой продукции | 0.5
->Стоимость продукции рассчитана исходя из используемых материалов | 0.7
-
-## Получение связанных данных (словари и связи многие-ко-многим)
-
->Это нужно только если вы не осилите сводный SQL-запрос. Если и с LINQ-запросами проблемы, то лучше сосредоточтесь на создании диаграммы прецедентов и спецификации к ней, а в программе выведите только список продукции без всяких связей.
-
-Нам нужно:
-
-* [вывести тип продукта](#получение-типа-продукта)
-* [вывести список материалов](#получение-списка-материалов)
-* подсчитать сумму материалов
-
-### Получение типа продукта
-
-1. Рисуем модель **ProductType**
-
-    ![](../img/01065.png)
-
-    ```cs
-    public class ProductType
-    {
-        public int ID { get; set; }
-        public string Title { get; set; }
-    }
-    ```
-
-2. В интерфейс добавляем метод **GetProductTypes** и реализуем его
-
-    ```cs
-    interface IDataProvider
-    {
-        IEnumerable<Product> GetProducts();
-        IEnumerable<ProductType> GetProductTypes();
-    }
-    ```
-
-    Переменную для хранения списка типов продукции делаем глобальной
-
-    ```cs
-    class Globals
-    {
-        public static IDataProvider DataProvider;
-        public static IEnumerable<ProductType> ProductTypeList { get; set; }
-    }
-    ```
-
-    В MysqlDataProvider реализуем добавленный метод    
-
-    ```cs
-    public IEnumerable<ProductType> GetProductTypes()
-    {
-        List<ProductType> productTypeList = new List<ProductType>();
-        string Query = "SELECT * FROM ProductType";
-
-        try
-        {
-            Connection.Open();
-            try
-            {
-                MySqlCommand Command = new MySqlCommand(Query, Connection);
-                MySqlDataReader Reader = Command.ExecuteReader();
-
-                while (Reader.Read())
-                {
-                    ProductType NewProductType = new ProductType();
-                    NewProductType.ID = Reader.GetInt32("ID");
-                    NewProductType.Title = Reader.GetString("Title");
-
-                    productTypeList.Add(NewProductType);
-                }
-            }
-            finally
-            {
-                Connection.Close();
-            }
-        }
-        catch (Exception)
-        {
-        }
-
-        return productTypeList;
-
-    }
-    ```
-
-3. В класс **Product** добавляем геттер, для получения названия типа продукции
-
-    ```cs
-    public string LinqTitle {
-        get {
-            return Globals.ProductTypeList
-                .Where(t=>t.ID==ProductTypeID)
-                .Select(t=>t.Title)
-                .FirstOrDefault();
-        }
-    }
-    ```
-
-4. В разметке теперь можем использовать поле *LinqTitle* для отображения типа продукции.
-
-### Получение списка материалов
-
->Тут подробно расписывать не буду, всё как в предыдущей части, просто покажу результирующий геттер
-
-```cs
-public string LinqMaterials {
-    get {
-        // выбираем идентификаторы материалов
-        var PM = Globals.ProductMaterial
-            .Where(t => t.ProductID == ID)
-            .Select(t => t.MaterialID) 
-            .ToList();
-
-        // выбираем названия материалов
-        var Materials = Globals.Material
-            .Where(t => PM.Contains(t.ID))
-            .Select(t => t.Title);
-
-        var Result = "";
-
-        foreach (string Material in Materials)
-            Result += "," + Material;
-
-        return Result;
-    }
-}
-```
-
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/sql_import.md">Создание базы данных. Импорт данных.
-</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></td><tr></table>

+ 0 - 408
articles/cs_pagination.md

@@ -1,408 +0,0 @@
-<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_coloring.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();
-    }
-}
-```
-
-<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_coloring.md">Подсветка элементов по условию. Дополнительные выборки.
-</a></td><tr></table>

+ 278 - 69
articles/cs_pagination2.md

@@ -11,6 +11,9 @@
 * [Фильтрация](#фильтрация)
 * [Поиск](#поиск)
 
+При работе с базой данных нужно учитывать, что объем выборки может быть очень большой. Соответственно фильтр, поиск и пагинацию нужно делать запросами к базе. Но такой вариант усложняет SQL-запросы, поэтому на демо экзамене можно выбрать всю таблицу целиком, а потом использовать LINQ-запросы в геттере (как мы до этого и делали). В рамках лекции мы рассмотрим оба варианта: _правильный_ вариант с запросами к базе вы будете использовать в курсовом проекте, а _простой_ на демо экзамене.
+
+
 ## Пагинация
 
 >В случае если в базе более 20 продуктов, то вывод должен осуществляться постранично (по 20 продуктов на страницу). Для удобства навигации по страницам необходимо вывести список их номеров (как на макете) с возможностью перехода к выбранной странице, а также предусмотреть переходы к предыдущей и следующей страницам.
@@ -28,7 +31,9 @@
 
 ## Постраничный вывод данных
 
-Тут всё просто. Нам в любом случае придется делать *геттер* для фильтрованного списка продукции. Сразу в этом геттере и сделаем выборку данных порциями. Для этого используются методы LINQ-запросов **Skip(N)** (пропустить) и **Take(N)** (получить), где N - количество пропускаемых и выбираемых элементов соответсвенно.
+### Простой вариант
+
+Тут всё просто. Нам в любом случае придется делать *геттер* для фильтрованного списка продукции. Сразу в этом геттере и сделаем выборку данных порциями. Для этого используются методы **LINQ**-запросов **Skip(N)** (пропустить) и **Take(N)** (получить), где `N` - количество пропускаемых и выбираемых элементов соответсвенно.
 
 ```cs
 // КЛАСС ГЛАВНОГО ОКНА
@@ -36,9 +41,11 @@
 // тут у нас будет храниться полный список продукции
 private IEnumerable<Product> _productList;
 
+// размер страницы
 private const int PAGE_LEN = 20;
+
 // тут мы храним номер текущей страницы
-private int _currentPage = 0;
+private int _currentPage = 1;
 
 // при смене текущей страницы мы должны перерисовать список (вспоминайте про INotifyPropertyChanged)
 private int currentPage {
@@ -55,13 +62,13 @@ private int currentPage {
 // и реализуем геттер и сеттер списка продукции
 public IEnumerable<Product> productList { 
     get {
-        var res = _productList;
+        var result = _productList;
 
         // тут будет поиск, сортировка и фильтрация
 
-        res = res.Skip(PAGE_LEN * currentPage).Take(PAGE_LEN);
+        result = result.Skip(PAGE_LEN * currentPage).Take(PAGE_LEN);
 
-        return res;
+        return result;
     } 
     set {
         _productList = value;
@@ -70,16 +77,137 @@ public IEnumerable<Product> productList {
 }
 ```
 
+### Сложный вариант
+
+1. В глобальные переменные добавим размер страницы (можно его хранить и в классе окна, но тогда придётся добавлять дополнительный параметр в метод _getProduct_)
+
+    ```cs
+    class Globals
+    {
+        public static IDataProvider dataProvider;
+        public static int PAGE_LEN = 20;
+    }
+    ```
+
+1. В интерфейсе **IDataProvider** в метод _getProduct_ добавляем параметр "номер страницы"
+
+    ```cs
+    public interface IDataProvider
+    {
+        IEnumerable<Product> getProduct(int pageNum);
+    }
+    ```
+
+1. В классе **DBDataProvider** дорабатываем метод _getProduct_ с учётом номера страницы
+
+    ```cs
+    public IEnumerable<Product> getProduct(int pageNum)
+    {
+        using (MySqlConnection db = new MySqlConnection(connectionString))
+        {
+            return db.Query<Product>(
+                "SELECT * FROM ProductView " +
+                "LIMIT @pageLen OFFSET @offset", 
+                new { 
+                    pageLen = Globals.PAGE_LEN, 
+                    offset = (pageNum - 1) * Globals.PAGE_LEN }
+            ).ToList();
+        }
+    }
+    ```
+
+    Здесь предполагается, что в БД создано представление (view) **ProductView** в котором производится выборка, которую мы написали на одном из прошлых занатий.
+
+    * **LIMIT** - оператор MySQL, который выбирает указанное количество записей
+
+    * **OFFSET** - пропустить указанное количество записей 
+
+1. При получении данных (в классе окна) указываем параметр
+
+    ```cs
+    productList = Globals.dataProvider.getProduct(currentPage);
+    ```
+
 ## Динамический вывод номеров страниц 
 
+### Определение количества записей
+
+Для начала мы должны знать сколько всего записей в базе
+
+В **простом** варианте мы можем просто использовать **LINQ** метод _Count_, который возвращает размер нашей выборки.
+
+В **сложном** варианте мы храним только часть данных, поэтому для получения количества записей в таблице запрашиваем количество из базы:
+
+1. В интерфейс **IDataProvider** добавляем метод _getProductCount_
+
+    ```cs
+    int getProductCount();
+    ```
+
+1. Реализуем этот метод в классе **DBDataProvider**
+
+    ```cs
+    public int getProductCount()
+    {
+        using (MySqlConnection db = new MySqlConnection(connectionString))
+        {
+            return db.QuerySingle<int>(
+            "SELECT count(*) FROM ProductView");
+        }
+    }
+    ```
+
+    >Для получения скалярных данных используется метод _QuerySingle_
+
+### Реализация пагинатора
+
+1. В **классе окна** объявить массив строк **pageList**
+
+    ```cs
+    public List<String> pageList { get; set; } = new List<String>();
+    ```
+
+1. В **геттере списка продукции** заполнять его
+
+    >Я реализую только **сложный** вариант
+
+    ```cs
+    private int productCount;
+
+    public IEnumerable<Product> productList { 
+        get {
+            // перечитываю данные из БД
+            var result = Globals.dataProvider.getProduct(currentPage);
+
+            // получаю количество записей в БД
+            productCount = Globals.dataProvider.getProductCount();
+
+            // очищаю список страниц и заполняю его заново
+            pageList.Clear();
+            pageList.Add("<");
+            for (int i = 1; i < (productCount / Globals.PAGE_LEN) + 1; i++)
+            {
+                pageList.Add(i.ToString());
+            }
+            pageList.Add(">");
+
+            // данные пишу напрямую в визуальный компонент
+            PageListListBox.ItemsSource = pageList;
+
+            return result;
+        } 
+    }
+    ```
+
 >Для пагинатора используем третью строку главной сетки
 
-1. В вёрстке использовать горизонтальный **ListBox** (есть в прошлой версии про вёрстку плиткой)
+1. В вёрстке использовать горизонтальный **ListBox** 
 
     ```xml
     <ListBox
-        x:DataType="system:String"
-        ItemsSource="{Binding PageList}"
+        x:Name="PageListListBox"
+        ItemsSource="{Binding pageList}"
+        Grid.Column="1"
         Grid.Row="2">
 
         <ListBox.ItemsPanel>
@@ -97,42 +225,7 @@ public IEnumerable<Product> productList {
                     PreviewMouseDown="InputElement_OnPointerPressed"/>
             </DataTemplate>
         </ListBox.ItemTemplate>
-    </ListView>
-    ```
-
-1. В **классе окна** объявить массив **pageList** и в **геттере списка продукции** заполнять его
-
-    ```cs
-    public List<String> pageList { get; set; } = new List<String>();
-
-    ...
-
-    // в геттере списка продукции после поиска и фильтрации
-    pageList.Clear();
-    pageList.Add("<");
-    for (int i = 1; i < (res.Count() / PAGE_LEN) + 1; i++){
-        pageList.Add(i.ToString());
-    }
-    pageList.Add(">");
-
-    // не забываем уведомить визуальный интерфейс о том, что список страниц изменился
-    Invalidate("pageList");
-    
-    res = res.Skip(PAGE_LEN*currentPage).Take(PAGE_LEN);
-
-    return res;
-    ```
-
-1. Метод **Invalidate** с указанием изменившегося элемента
-
-    ```cs
-    private void Invalidate(string ComponentName = "productList") 
-    {
-        if (PropertyChanged != null)
-            PropertyChanged(
-                this, 
-                new PropertyChangedEventArgs(ComponentName));
-    }
+    </ListBox>
     ```
 
 1. Реализация обработчика клика по кнопкам пагинатора:
@@ -145,17 +238,17 @@ public IEnumerable<Product> productList {
         {
             case "<":
                 // переход на предыдущую страницу с проверкой счётчика
-                if (currentPage > 0) currentPage--;
+                if (currentPage > 1) currentPage--;
                 return;
             case ">":
                 // переход на следующую страницу с проверкой счётчика
-                if (currentPage < productList.Count() / PAGE_LEN) currentPage++;
+                if (currentPage < productCount / Globals.PAGE_LEN) currentPage++;
                 return;
             default:
                 // в остальных элементах просто номер странцы
-                // учитываем, что номера страниц начинаются с 0
                 currentPage = Convert.ToInt32(
-                    (sender as TextBlock).Text) - 1;
+                    (sender as TextBlock).Text
+                );
                 return;
         }
     }
@@ -203,8 +296,7 @@ public IEnumerable<Product> productList {
             VerticalContentAlignment="Center"
             MinWidth="200"
             SelectionChanged="SortTypeComboBox_SelectionChanged"
-            x:DataType="system:String"
-            ItemsSource="{Binding #root:sortList}"/>
+            ItemsSource="{Binding sortList}"/>
     </WrapPanel>    
     ```
 
@@ -227,25 +319,96 @@ public IEnumerable<Product> productList {
     }
     ```
 
-1. И дорабатываем геттер списка продукции
+1. И дорабатываем геттер списка продукции. Я реализую только **сложный** вариант, **простой** вы можете посмотреть в лекциях за прошлый год
 
     ```cs
     get {
-        var res = _productList;
-
+        // ДО выборки данных устанавливаем или сбрасываем условие сортировки (я реализовал только первые 2, остальные реализуйте сами по аналогии)
         switch (sortType)
         {
-            // сортировка по названию продукции
+            case 0:
+                Globals.dataProvider.setOrder("");
+                break;
             case 1:
-                res = res.OrderBy(p => p.Title);
+                Globals.dataProvider.setOrder("Title");
                 break;
             case 2:
-                res = res.OrderByDescending(p => p.Title);
+                Globals.dataProvider.setOrder("Title DESC");
                 break;
-            // остальные сортировки реализуйте сами
         }
+
+        var result = Globals.dataProvider.getProduct(currentPage);
+
         ...
-    } 
+    ```
+
+    В интерфейс **IDataProvider** добавляем метод _setOrder_
+
+    ```cs
+    void setOrder(string condition);
+    ```
+
+    И реализуем его в **DBDataProvider**
+
+    ```cs
+     private string orderCondition = "";
+    public void setOrder(string condition)
+    {
+        orderCondition = condition;
+    }
+    ```
+
+    В методе _getProduct_ мы должны учесть сортировку:
+
+    При добавлении сортировок и условий выборки код усложняется, например для добавления сортировки придётся нарисовать такой код:
+
+    ```cs
+    var sql = "SELECT * FROM ProductView ";
+
+    if (orderCondition.Length > 0)
+        sql += " ORDER BY "+orderCondition;
+
+    sql += " LIMIT @pageLen OFFSET @offset";
+
+    return db.Query<Product>(
+        sql, 
+        new { 
+            pageLen = Globals.PAGE_LEN, 
+            offset = (pageNum - 1) * Globals.PAGE_LEN }
+    ).ToList();
+    ```
+
+    Получается очень сложно, но в счастью есть библиотеки облегчающие нам жизнь. 
+
+    Установите через **NuGet** пакет **Dapper.SqlBuilder**
+
+    С учётом построителя запросов получится такой код:
+
+    ```cs
+    public IEnumerable<Product> getProduct(int pageNum)
+    {
+        using (MySqlConnection db = new MySqlConnection(connectionString))
+        {
+            var builder = new SqlBuilder();
+
+            // добавляем сортировку
+            if (orderCondition.Length>0) 
+                builder.OrderBy(orderCondition);
+
+            // формируем шаблон запроса
+            var template = builder.AddTemplate(
+                "SELECT * FROM ProductView /**where**/ /**orderby**/ LIMIT @pageLen OFFSET @offset",
+                new { 
+                    pageLen = Globals.PAGE_LEN, 
+                    offset = (pageNum - 1) * Globals.PAGE_LEN }
+            );
+
+            // выполняем запрос
+            return db.Query<Product>(
+                template.RawSql,
+                template.Parameters).ToList();        
+        }
+    }
     ```
 
 ## Фильтрация
@@ -264,12 +427,14 @@ public IEnumerable<Product> productList {
 
 1. Создаем список типов продукции, заполняем его данными из базы (в конструкторе главного окна, там же где получали список продукции) и добавляем в начало пункт "Все типы продукции"
 
+    >Я не показываю реализацию метода _getProductTypes_, с этим вы уже должны справиться сами
+
     ```cs
-    public List<ProductType> ProductTypeList { get; set; }
+    public List<ProductType> productTypeList { get; set; }
 
     ...
 
-    ProductTypeList = context.ProductTypes.ToList();
+    productTypeList = context.ProductTypes.ToList();
     ProductTypeList.Insert(0, new ProductType { Title = "Все типы продукции" });
     ```
 
@@ -281,8 +446,7 @@ public IEnumerable<Product> productList {
         x:Name="ProductTypeFilter"
         SelectedIndex="0"
         SelectionChanged="ProductTypeFilter_SelectionChanged"
-        x:DataType="model:ProductType"
-        ItemsSource="{Binding #root.productTypeList}"/>
+        ItemsSource="{Binding productTypeList}"/>
     ```
 
     Элементами списка являются не строки, а объекты. В прошлом году я показывал как делать шаблон элемента списка, но как мне кажеться шаблон здесь излишен (его имеет смысл использовать если кроме названия выводится ещё что-то)
@@ -298,31 +462,76 @@ public IEnumerable<Product> productList {
 1. Реализуем обработчик выбора элемента фильтра    
 
     ```cs
-    private int ProductTypeFilterId = 0;
+    private int productTypeFilterId = 0;
 
     private void ProductTypeFilter_SelectionChanged(object sender, SelectionChangedEventArgs e)
     {
         // запоминаем ID выбранного типа
-        ProductTypeFilterId = (ProductTypeFilter.SelectedItem as ProductType).ID;
+        productTypeFilterId = (ProductTypeFilter.SelectedItem as ProductType).ID;
         Invalidate();
     }
     ```
 
 1. И опять дорабатываем геттер списка продукции
 
+    >Так как в SQL запросе теперь появляются условия (where), то в интерфейс добавляем методы для добавления и очистки списка условий:
+    >
+    >```cs
+    >private Dictionary<string, object> filters = new >Dictionary<string, object>();
+    >public void addFilter(string name, string value)
+    >{
+    >    filters.Add(name, value);
+    >}
+    >
+    >public void clearFilter()
+    >{
+    >    filters.Clear();
+    >}
+    >```
+
+    Перед получением данных добавляем фильтр
+
     ```cs
     ...
-    var res = _productList;
 
-    // действия, которые уменьшают размер выборки помещаем вверх
+    Globals.dataProvider.clearFilter();
     if (productTypeFilterId > 0)
-        res = res.Where(
-            p => p.ProductTypeId == productTypeFilterId);
+        Globals.dataProvider.addFilter(
+            "ProductTypeID = @ProductTypeID", 
+            new {ProductTypeID = productTypeFilterId}
+        );
 
-    switch (sortType)
     ...
     ```
 
+    В поставщике данных при получении данных учитываем фильтры
+
+    ```cs
+    using (MySqlConnection db = new MySqlConnection(connectionString))
+    {
+        var builder = new SqlBuilder();
+
+        if (orderCondition.Length>0) 
+            builder.OrderBy(orderCondition);
+
+        if (filters.Count>0)
+        {
+            foreach (var item in filters)
+                builder.Where(item.Key, item.Value);
+        }
+
+
+        var template = builder.AddTemplate(
+            "SELECT * FROM ProductView /**where**/ /**orderby**/ LIMIT @pageLen OFFSET @offset",
+            new { pageLen = Globals.PAGE_LEN, offset = (pageNum - 1) * Globals.PAGE_LEN }
+        );
+
+        return db.Query<Product>(
+            template.RawSql,
+            template.Parameters).ToList();
+    }
+    ```
+
 ## Поиск
 
 >Пользователь должен иметь возможность искать конкретную продукцию, используя поисковую строку.