# Добавление/редактирование продукции
* [Создание окна редактирования продукции](#создание-окна-редактирования-продукции)
* [Открытие окна редактирования для существующей и новой продукции](#открытие-окна-редактирования-для-существующей-и-новой-продукции)
* [Проверки перед сохранением продукта](#проверки-перед-сохранением-продукта)
* [Удаление продукции](#удаление-продукции)
>Необходимо добавить возможность редактирования данных существующей продукции, а также добавление новой продукции в новом окне - форме для добавления/редактирования продукции.
>
>Переходы на данное окно должны быть реализованы из главной формы списка: для редактирования - при нажатии на конкретный элемент, для добавления - при нажатии кнопки “Добавить продукцию”.
>
>На форме должны быть предусмотрены следующие поля: артикул, наименование, тип продукта (выпадающий список), изображение, количество человек для производства, номер производственного цеха, минимальная стоимость для агента и подробное описание (с возможностью многострочного ввода).
>
>Также необходимо реализовать вывод списка материалов, используемых при производстве продукции, с указанием количества. В список можно добавлять новые позиции и удалять существующие. При добавлении материалы должны выбираться из выпадающего списка с возможностью поиска по наименованию.
>
>При открытии формы для редактирования все поля выбранного объекта должны быть подгружены в соответствующие поля из базы данных, а таблица заполнена актуальными значениями.
>
>Стоимость продукции может включать сотые части, а также не может быть отрицательной. Система должна проверять существование продукта с введенным артикулом и не давать использовать один артикул для нескольких продуктов.
>
>Пользователь может добавить/заменить изображение у продукции.
>
>Для того чтобы администратор случайно не изменял несколько продуктов, предусмотрите невозможность открытия более одного окна редактирования.
>
>В окне редактирования продукта должна присутствовать кнопка “Удалить”, которая удаляет продукт из базы данных. При этом должны соблюдаться следующие условия. Если у продукта есть информация о материалах, используемых при его производстве, или история изменения цен, то эта информация должна быть удалена вместе с продуктом. Но если у продукта есть информация о его продажах агентами, то удаление продукта из базы данных должно быть запрещено. После удаления продукта система должна сразу вернуть пользователя обратно к списку продукции.
>
>После редактирования/добавления/удаления продукции данные в окне списка продукции должны быть обновлены.
Критерий | Баллы
---------|:---:
Реализован переход на окно добавления | 0.1
Реализован переход на окно редактирования выбранного объекта | 0.2
Присутствуют все поля для заполнения | 0.5
При редактировании продукции в поля для ввода загружены данные из БД | 0.3
Выбор типа продукта реализован в виде выпадающего списка со значениями из БД | 0.3
Для ввода описания продукции предусмотрено многострочное поле для ввода | 0.2
***Реализован список используемых материалов для текущего продукта*** | 0.3
***В списке присутствует название материала и используемое количество*** | 0.2
***При редактировании продукции список материалов заполнен значениями из БД*** | 0.2
***В список можно добавлять новые позиции*** | 0.3
***Из списка можно удалять существующие позиции*** | 0.2
***При добавлении материалы выбираются из выпадающего списка со значениями из БД*** | 0.3
***В списке материалов реализована возможность поиска по наименованию*** | 0.2
***Список используемых материалов сохраняется в БД при добавлении*** | 0.5
***Список используемых материалов сохраняется в БД при редактировании*** | 0.5
Стоимость продукции не может быть отрицательной | 0.1
Стоимость продукции записывается только с точностью до сотых | 0.2
Реализована проверка артикула на уникальность | 0.3
Есть возможность выбрать изображение | 0.2
Изображение продукции подгружается из БД при редактировании | 0.2
Есть возможность заменить изображение | 0.1
Данные при добавлении сохраняются в БД | 0.5
Данные при редактировании изменяются в БД | 0.5
Открывается только одно окно редактирования | 0.1
*Реализовано удаление выбранного продукта, у которого не заполнен список используемых материалов* | 0.2
*Реализовано удаление продукта вместе с информацией об используемых материалах* | 0.5
*Запрещено удаление продукта, по которому были выполнены продажи агентом* | 0.3
*После удаления реализован автоматический переход обратно в список* | 0.1
После закрытия окна данные в таблице обновляются | 0.3
**Итого** | **7.9**
## Создание окна редактирования продукции
Для добавления и редактирования мы будем использовать одно и то же окно. Заголовок окна будем вычислять по наличию ID у продукции (у новой записи это поле равно `0`)
1. Создайте новое окно: **EditProductWindow** (в папке **Windows** - не забываем про логическую структуру проекта)
1. В классе окна **EditProductWindow** добавьте **свойство** *currentProduct*, в котором будет храниться добавляемый/редактируемый экземпляр продукции:
```cs
public Product currentProduct { get; set; };
```
1. В разметке окна вставьте аттрибут *Name*
```xml
```
1. В конструктор окна добавьте параметр типа **Product**, присвойте его ранее объявленному свойству. Задайте заголовок окна:
```cs
public EditWindow(Product EditProduct)
{
currentProduct = editProduct;
InitializeComponent();
root.Title = currentProduct.Id == 0 ? "Новый продукт" : "Редактирование продукта";
}
```
1. В разметке окна редактирования продукции создайте сетку из трёх колонок: в первой у нас будет изображение, во второй редактируемые поля продукта, а в третей список материалов
```xml
```
1. Во вторую колонку добавьте **StackPanel** с границами (чтобы визуальные компоненты не прилипали к границам окна) и в этом списке разместите редактируемые элементы
>На форме должны быть предусмотрены следующие поля: _артикул_, _наименование_, _тип продукта_ (выпадающий список), _изображение_, _количество человек для производства_, _номер производственного цеха_, _минимальная стоимость для агента_ и _подробное описание_
```xml
...
```
Обычные поля наклепайте по шаблону сами, а я подробнее остановлюсь на полях: *тип продукта*, *изображение* и *описание*:
* Выбор типа продукта из списка
В классе окна объявляем свойство *productTypeList* - список типов продукции
```cs
public IEnumerable productTypeList { get; set; }
```
И в конструкторе получаем его из БД:
```cs
using (var context = new ksmirnovContext())
{
productTypeList = context.ProductTypes.ToList();
}
```
В вёрстке окна редактирования продукции мы можем использовать выпадающий список, атрибут *SelectedItem* которого позволяет отобразить **текущее** значение редактируемого элемента
```xml
```
>Из документации: *Класс ComboBox выполняет поиск указанного объекта с помощью **IndexOf** метода. Этот метод, в свою очередь, использует метод **Equals** для определения равенства (объектов)*.
А метод **Equals** сравнивает объекты по уникальному идентификатору (встроенное свойство у базового класса в C#, не путать с полем `id` таблицы БД), т.е. свойство **ProductType** у экземпляра продукции **НЕ РАВНО** экземпляру элемента списка *productTypeList*
Чтобы исправить эту неприятность нужно переопределить метод **Equals** у класса **ProductType**:
```cs
public override bool Equals(object obj)
{
return (obj != null) &&
(obj is ProductType) &&
(this.Id == (obj as ProductType).Id);
}
```
Здесь мы проверяем определён ли вообще объект (у нового продукта его ещё нет), нужного ли он типа и совпадет ли его **Id** с **Id** текущего типа продукции
* смена изображения продукции
Вывод изображения производится как и в главном окне
```xml
```
А для смены изображения используем стандартный диалог открытия файлов, повесив его на кнопку *Сменить картинку* (кнопку добавьте сами в центральную колонку)
Обработчик кнопки:
```cs
private void ChangeImage_Click(object sender, RoutedEventArgs e)
{
var getImageDialog = new OpenFileDialog();
// задаем фильтр для выбираемых файлов
getImageDialog.Filters.Add(
new FileDialogFilter() {
Name = "Файлы изображений",
Extensions = {"png", "jpg", "jpeg"}});
// чтобы не искать по всему диску задаем начальный каталог
// В ТЕКУЩЕЙ ВЕРСИИ АВАЛОНИИ ТАКОГО СВОЙСТВА НЕТ
// getImageDialog.InitialDirectory = Environment.CurrentDirectory;
// открываем СИНХРОННО диалог
// в ответ получим массив выбранных файлов
var files = await getImageDialog.ShowAsync(this);
if (files != null)
{
// перед присвоением пути к картинке обрезаем начало строки, т.к. диалог возвращает полный путь
currentProduct.Image = files[0].Substring(Environment.CurrentDirectory.Length);
// тут нужно либо реализовать интерфейс InotifyPropertyChanged
// и обновить информацию о картинке
// либо тупо поменять свойство нужного контрола
CurrentProductImage.Source = currentProduct.ImageBitmap;
}
}
```
* Многострочное описание
Тут просто - разрешаем переносы и задаем высоту элемента
```xml
```
1. Сохранение введенных данных
В разметку добавьте кнопку **Сохранить** и напишите обработчик
```cs
private void Button_Click(object sender, RoutedEventArgs e)
{
using (var context = new esmirnovContext())
{
// вся работа с БД должна быть завернута в исключения
try
{
Product product = null;
if (currentProduct.Id != 0)
product = context.Products.Find(currentProduct.Id);
else
product = new Product();
if (product != null)
{
// сюда добавлять проверки
product.Title = currentProduct.Title;
product.ArticleNumber = currentProduct.ArticleNumber;
...
product.ProductTypeId = currentProduct.ProductType.Id;
if (product.Id==0)
context.Products.Add(product);
else
context.Products.Update(product);
if (context.SaveChanges() > 0)
{
Close(true);
}
}
}
catch (Exception ex)
{
// if(ex.InnerException != null)
// MessageBox.Show(ex.InnerException.Message);
// else
// MessageBox.Show(ex.Message);
}
}
}
```
## Открытие окна редактирования для существующей и новой продукции
* для редактирования существующей продукции в списке продукции реализуем обработчик двойного клика
```cs
private void ProductListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
var currentProduct = (sender as ListBox).SelectedItem as Product;
// в создаваемое окно передаем выбранный продукт
var newEditWindow = new EditProductWindow(product);
var res = await newEditWindow.ShowDialog(this);
if (res)
{
// эту функцию реализуйте сами - тут перечитывание списка продукции из базы
getProductList();
}
}
```
* для создания нового продукта в разметке главного окна создайте кнопку "Добавить продукцию" (в левой или верхней панели) и в её обработчике создайте новый экземпляр продукта, а дальше то же, что и в предыдущем пункте
```cs
var newEditWindow = new EditProductWindow(new Product());
...
```
## Проверки перед сохранением продукта
Все проверки вставляем в обработчик кнопки "Сохранить" окна редактирования, до вызова метода сохранения продукта
* Стоимость продукции не может быть отрицательной
```cs
if (currentProduct.MinCostForAgent < 0)
throw new Exception("Цена продукта не может быть отрицательной");
```
* Стоимость продукции записывается только с точностью до сотых
* Реализована проверка артикула на уникальность
Тут надо делать запрос к базе c методом `Where`
```cs
var dublicateArticle = context.Products
.Where(p => p.ArticleNumber == currentProduct.ArticleNumber)
.FirstOrDefault();
if (dublicateArticle != null)
throw new Exception("Такой артикул уже есть в базе");
```
## Удаление продукции
>В окне редактирования продукта должна присутствовать кнопка “Удалить”, которая удаляет продукт из базы данных. При этом должны соблюдаться следующие условия. Если у продукта есть информация о материалах, используемых при его производстве, или история изменения цен, то эта информация должна быть удалена вместе с продуктом. Но если у продукта есть информация о его продажах агентами, то удаление продукта из базы данных должно быть запрещено. После удаления продукта система должна сразу вернуть пользователя обратно к списку продукции.
Добавьте кнопку *Удалить* в среднюю колоку (рядом с кнопкой *сохранить*). Атрибут **IsVisible** привяжите к **Id** продукта (т.е. у нового продукта кнопки удалить быть не должно). Можно реализовать через _Binding_, но можно задать в конструкторе
```cs
DeleteButton.IsVisible = currentProduct.Id > 0;
```
И реализуйте обработчик клика по этой кнопке
```cs
private void DeleteProductButton_Click(object sender, RoutedEventArgs e)
{
using (var context = new esmirnovContext())
{
try
{
// тут вставляем проверки, требуемые по ТЗ
// + исключение, если есть продажи
var saleCount = context.ProductSales
.Count(ps => ps.ProductId == currentProduct.Id);
if (saleCount > 0)
throw new Exception("Нельзя удалять продукт с продажами");
// - удаление списка материалов продукта (НЕ ПРОВЕРЯЛ)
context.ProductMaterials.RemoveRange(
context.ProductMaterials.Where(pm => pm.ProductId == currentProduct.Id)
);
// - удаление истории изменения цен (если есть)
var product = context.Products.Find(currentProduct.Id);
context.Products.Remove(product);
if (context.SaveChanges() > 0)
{
Close(true);
}
}
catch (Exception ex)
{
// if(ex.InnerException != null)
// MessageBox.Show(ex.InnerException.Message);
// else
// MessageBox.Show(ex.Message);
}
}
}
```
>Метод **RemoveRange** не рекомендуется использовать в больших базах, в инетах пишут, что он вытягивает всю таблицу в ОЗУ. Лучше использовать либо одиночное удаление в цикле, либо "чистый" SQL запрос:
>```cs
>context.Database.ExecuteSqlRaw($"DELETE FROM ProductMaterial WHERE ProductId={currentProduct.Id}");
>// также можно удалить и сам продукт
>context.Database.ExecuteSqlRaw($"DELETE FROM Product WHERE Id={currentProduct.Id}");
>```
>Но в случае использования "чистых" SQL-запросов нужно учитывать, что **EntityFramework** не будет знать сколько записей затронуто и нужно убрать проверку количества измененных записей в **SaveChanges** (собственно и сам этот метод не нужно вызывать, если всё сделано "чистыми" SQL-запросами)