cs_coloring.md 19 KB

Подсветка элементов по условию. Дополнительные выборки.

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

Пагинация, сортировка, фильтрация, поиск Содержание Создание, изменение продукции
Критерий Баллы
Реализовано выделение (любым образом) продуктов, которые не продавались агентами в последний месяц 1.5
Выделение реализовано в виде светло-красной подсветки элемента продукции 0.5
Итого 2

В самом списке продукции данных о продажах нет. Судя по названиям таблиц данные эти должны быть в таблице ProductSale. Судя по связям этой таблицы, мы должны заполнить ещё таблицы Agent и AgentType.

На демо-экзамене до этого вы вряд-ли дойдёте (хотя критерий достаточно жирный), но в рамках курсовой/дипломной работы реализовать дополнительный функционал надо.

Добавление данных вручную

  1. Добавляем типы агентов

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

    И так как исходных данных нет, а редактировать напрямую из MySQL Workbench нельзя, то пишем SQL-запрос вставки данных в основном его синтаксисе (с VALUE) то добавляем данные прямо в таблицы.

  2. Добавляем агента

    Тут заполняем только обязательные поля. AgentTypeID смотрим в таблице AgentType.

  3. Создание продаж продукции

    Добавляем несколько записей. Поля AgentID и ProductID смотрим в соответствующих таблицах. Дата продажи заполняется в формате YYYY-MM-DD

Дополнительная выборка

Сначала пишем и отлаживаем запрос получения количества дней с последней продажи (дата продажи сама по себе нам не нужна, достаточно знать сколько дней прошло с момента продажи)

SELECT 
    ProductID, 
    DATEDIFF(NOW(), max(SaleDate)) AS LastSaleDate
FROM 
    ProductSale
GROUP BY 
    ProductID
  • Функция DATEDIFF вычисляет количество дней между датами.
  • Функция NOW возвращает текущую дату.

Теперь этот запрос можно вставить в нашу основную выборку ещё одним LEFT JOIN-ом

SELECT 
    p.*,
    pt.Title AS ProductTypeTitle,
    pp.MaterialList, pp.Total,
    Sales.DaysFromLastSale
--  ^^^^^^^^^^^^^^^^^^^^^^
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
LEFT JOIN 
    (
        select 
            ProductID, 
            DATEDIFF(NOW(), max(SaleDate)) as DaysFromLastSale
        from 
            ProductSale
        group by ProductID
    ) Sales on Sales.ProductID = p.ID ;

Атрибут DaysFromLastSale в модель пропишите сами.

В методе получения данных с сервера надо вставить проверку на тип NULL

NewProduct.DaysFromLastSale = 
    (Reader["DaysFromLastSale"] as int?) ?? 999;

Или просто завернуть чтение этого поля в try...except и, при возникновении исключения, подставлять данные по-умолчанию

try
{
    NewProduct.DaysFromLastSale = Convert.ToInt32(
        Reader["DaysFromLastSale"].ToString());
}
catch (Exception)
{
    // нам не интересно всё, что больше 30
    NewProduct.DaysFromLastSale = 999;
}

Раскраска по условию

Тут элементарно. У элемента рамка (Border) задаем цвет фона:

<Border 
    BorderThickness="1" 
    BorderBrush="Black" 
    Background="{Binding BackgroundColor}"
                ^^^^^^^^^^^^^^^^^^^^^^^^^
    CornerRadius="5">

И добавляем в модель геттер BackgroundColor

public string BackgroundColor {
    get {
        if(DaysFromLastSale>30) return "#fee";
        return "#fff";
    }
}

Про цвета: их можно отдавать в формате #RGB. Причём, чем ближе к F, тем светлее (#FFF - белый)

Массовая смена цены продукции

Необходимо добавить возможность изменения минимальной стоимости продукции для агента сразу для нескольких выбранных продуктов. Для этой цели реализуйте возможность выделения сразу нескольких элементов в списке продукции, после чего должна появиться кнопка “Изменить стоимость на ...”. При нажатии на кнопку необходимо отобразить модальное окно с возможностью ввода числового значения, на которое и будет увеличена стоимость выбранных продуктов. По умолчанию в поле должно быть введено среднее значение цены на продукцию для агента. После нажатия кнопки “Изменить” стоимость выделенных продуктов должна быть изменена в базе данных, а также обновлена в интерфейсе.

Критерий Баллы
Реализована возможность выделения сразу нескольких элементов в списке 0.2
После выделения элементов в списке появляется кнопка "Изменить стоимость на ..." 0.3
При нажатии на кнопку отображается модальное окно для изменения цены 0.1
В модальном окне есть возможность ввода числового значения 0.1
По умолчанию введено значение средней цены выбранных продуктов 0.2
Реализована проверка на ввод только числового значения 0.2
После нажатия кнопки "Изменить" стоимость всех выбранных продуктов изменяется в БД 0.5
После нажатия кнопки "Изменить" стоимость всех выбранных продуктов обновляется в списке 0.2
Итого 1.8

Выделение нескольких элементов

Возможность выделения нескольких элементов в ListView есть, специально её реаизовывать не надо. Надо лишь поймать это событие и сосчитать количество выделенных элементов.

В ListView нужно добавить название (мы потом будем по нему искать количество выделенных элементов) и обработчик события SelectionChanged

x:Name="ProductListView"
SelectionChanged="ListView_SelectionChanged"

Реализуем обработчик ListView_SelectionChanged:

public int ProductsSelectedCount = 0;
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ProductsSelectedCount = ProductListView.SelectedItems.Count;
}

Отображение кнопки "Изменить стоимость на ..."

Сначала просто добавляем эту кнопку в разметку

<Button
    x:Name="CostChangeButton"
    Visibility=""
    Content="Изменить стоимость на..."
    />

И запоминаем какие значения может прнимать атрибут Visibility

  • Collapsed - элемент не отображается и НЕ ЗАНИМАЕТ МЕСТО в контейнере
  • Hidden - элемент не отображается, но под него остается выделенное место
  • Visible - элемент отображается

Теперь, чтобы видимость кнопки зависела от количества выделенных элементов, мы привязываем атрибут Visibility к свойству CostChangeButtonVisible

<Button
    x:Name="CostChangeButton"
    Visibility="{Binding CostChangeButtonVisible}"
    Content="Изменить стоимость на..."
    />

И реализуем это свойство в коде ОКНА

public string CostChangeButtonVisible {
    get {
        if (ProductsSelectedCount > 1) return "Visible";
        return "Collapsed";
    }
}

Осталось в обработчик события ListView_SelectionChanged вставить вызов метода PropertyChanged для свойства CostChangeButtonVisible

Чтобы не плодить кучу одинаковых методов, я добавил в параметры метода Invalidate имя свойства, которое изменилось

private void Invalidate(string ComponentName = "ProductList") {
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(ComponentName));
}

private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ProductsSelectedCount = ProductListView.SelectedItems.Count;
    Invalidate("CostChangeButtonVisible");
}

Вывод модального окна

  1. Добавляем кнопке обработчик события клика

    <Button
        x:Name="CostChangeButton"
        Visibility="{Binding CostChangeButtonVisible}"
        Click="CostChangeButton_Click"
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        Content="Изменить стоимость на..."
    />
    
  2. Создаем и показываем модальное окно

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

    Создадим папку Windows и в неё добавим окно (WPF)

    Название окна должно быть осмысленным и с суффиксом Window. У меня получилось EnterMinCostForAgentWindow

    Можно в каталог Windows перетащить и главное окно MainWindow. Только в этом случае надо в разметке приложения (App.xaml) добавить название каталога:

    ><Application x:Class="mysql.App"
    >   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    >   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >   xmlns:local="clr-namespace:mysql"
    >   StartupUri="Windows/MainWindow.xaml">
    >               ^^^^^^^^
    >...
    
    Опять же, все окна должны иметь нормальные заголовки. В разметке окна поменяйте атрибут *Title* элемента **Window** (это надо сделать и для основного окна)
    
    Содержимое у этого окна элементарное (текстовое поле и кнопка):
    
    

    xml

    <StackPanel
        Orientation="Vertical" Margin="0,50,0,0">
    

        <TextBox
            Name="CostTextBox"
            />
        <Button 
            Content="Изменить"/>
    </StackPanel>
    

    
    В коде этого окна меняем конструктор:
    
    

    cs public EnterMinCostForAgentWindow(decimal AvgCost)

                                  ^^^^^^^^^^^^^^^    
    

    {

    InitializeComponent();
    CostTextBox.Text = AvgCost.ToString();
    

    } ```

    Во-первых, в конструктор добавляем параметр (средняя цена). Во-вторых, записываем эту цену в текстовое поле.

  3. По условиям задачи мы должны вычислить среднюю цену для выделенных элементов списка (я на вскидку не нашёл как преобразовать IList в IEnumerable, поэтому тупо перебираем список выбранных элементов, считаем сумму и, заодно, собираем список идентификаторов)

    private void CostChangeButton_Click(object sender, RoutedEventArgs e)
    {
        decimal Sum = 0;
        List<int> idList = new List<int>();
        foreach (Product item in ProductListView.SelectedItems){
            Sum += item.MinCostForAgent;
            idList.Add(item.ID);
        }
    
        // создаём окно, передавая ему среднюю цену    
        var NewWindow = new EnterMinCostForAgentWindow(
            AvgSum / ProductListView.SelectedItems.Count);
    
        // показываем МОДАЛЬНОЕ окно    
        NewWindow.ShowDialog();
    }
    
  4. Проверка на ввод только числового значения.

    // объявляем публичную переменную из которой
    // будем считывать результат
    public decimal Result;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
        try
        {
            // пробуем сконвертировать в число
            Result = Convert.ToDecimal(CostTextBox.Text);
            // при присвоении результата свойству DialogResult модальное окно закрывается
            DialogResult = true;
        }
        catch (Exception)
        {
            MessageBox.Show("Стоимость должна быть числом");
        }
    }
    
  5. Запись новой цены в БД и обновление списка.

    • редактируем пункт 3, добавляя анализ результата

      if((bool)NewWindow.ShowDialog())
      {
          // вся работа с БД у нас в DataProvider-e, 
          // передаем список идентификаторов и новую цену
          Globals.DataProvider.SetAverageCostForAgent(idList, NewWindow.Result);
          // для перерисовки списка продукции просто перечитываем его
          ProductList = Globals.DataProvider.GetProducts();
      }
      
    • Описываем метод SetAverageCostForAgent в интерфейсе IDataProvider:

      void SetAverageCostForAgent(List<int> ProductIds, decimal NewCost);
      
    • И реализуем его в MySQLDataProvider-e:

      public void SetAverageCostForAgent(List<int> ProductIds, decimal NewCost)
      {
          try
          {
              Connection.Open();
              try
              {
                  // вместо значений записываем шаблоны
                  string Query = @"UPDATE 
                      Product 
                  SET MinCostForAgent=@MinCostForAgent 
                  WHERE ID=@ID";
      
                  // перебираем список идентификаторов
                  foreach (int item in ProductIds)
                  {
                      MySqlCommand Command = new MySqlCommand(Query, Connection);
                      // заменяем шаблоны параметрами
                      Command.Parameters.AddWithValue("@MinCostForAgent", NewCost);
                      Command.Parameters.AddWithValue("@ID", item);
                      // и выполняем запрос
                      Command.ExecuteNonQuery();
                  }
              }
              finally
              {
                  Connection.Close();
              }
          }
          catch (Exception)
          {
          }
      }
      
    Пагинация, сортировка, фильтрация, поиск Содержание Создание, изменение продукции