Przeglądaj źródła

редактирование продукции

Евгений Колесников 1 rok temu
rodzic
commit
2a38b267a3
2 zmienionych plików z 94 dodań i 553 usunięć
  1. 0 402
      articles/cs_edit_product.md
  2. 94 151
      articles/cs_edit_product2.md

+ 0 - 402
articles/cs_edit_product.md

@@ -1,402 +0,0 @@
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_coloring2.md">Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/kotlin.md">Основы языка Kotlin
-</a></td><tr></table>
-
-# Добавление/редактирование продукции
-
->Необходимо добавить возможность редактирования данных существующей продукции, а также добавление новой продукции в новом окне - форме для добавления/редактирования продукции.
->
->Переходы на данное окно должны быть реализованы из главной формы списка: для редактирования - при нажатии на конкретный элемент, для добавления - при нажатии кнопки “Добавить продукцию”.
->
->На форме должны быть предусмотрены следующие поля: артикул, наименование, тип продукта (выпадающий список), изображение, количество человек для производства, номер производственного цеха, минимальная стоимость для агента и подробное описание (с возможностью многострочного ввода).
->
->Также необходимо реализовать вывод списка материалов, используемых при производстве продукции, с указанием количества. В список можно добавлять новые позиции и удалять существующие. При добавлении материалы должны выбираться из выпадающего списка с возможностью поиска по наименованию.
->
->При открытии формы для редактирования все поля выбранного объекта должны быть подгружены в соответствующие поля из базы данных, а таблица заполнена актуальными значениями.
->
->Стоимость продукции может включать сотые части, а также не может быть отрицательной. Система должна проверять существование продукта с введенным артикулом и не давать использовать один
-артикул для нескольких продуктов.
->
->Пользователь может добавить/заменить изображение у продукции.
->
->Для того чтобы администратор случайно не изменял несколько продуктов, предусмотрите невозможность открытия более одного окна редактирования.
->
->В окне редактирования продукта должна присутствовать кнопка “Удалить”, которая удаляет продукт из базы данных. При этом должны соблюдаться следующие условия. Если у продукта есть информация о материалах, используемых при его производстве, или история изменения цен, то эта информация должна быть удалена вместе с продуктом. Но если у продукта есть информация о его продажах агентами, то удаление продукта из базы данных должно быть запрещено. После удаления продукта система должна сразу вернуть пользователя обратно к списку продукции.
->
->После редактирования/добавления/удаления продукции данные в окне списка продукции должны быть обновлены.
-
-Критерий | Баллы
----------|:---:
-Реализован переход на окно добавления | 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. Создайте новое окно: **EditWindow**
-
-2. В классе окна **EditWindow** добавьте свойство *CurrentProduct*, в котором будет храниться добавляемый/редактируемый экземпляр продукции:
-
-    ```cs
-    public Product CurrentProduct { get; set; }
-    ```
-
-    И геттер для названия окна:
-
-    ```cs
-    public string WindowName {
-        get {
-            return CurrentProduct.ID == 0 ? "Новый продукт" : "Редактирование продукта";
-        }
-    }
-    ```
-3. В конструктор окна добавьте параметр типа **Product** и присвойте его ранее объявленному свойству:
-
-    ```cs
-    public EditWindow(Product EditProduct)
-    {
-        InitializeComponent();
-        DataContext = this;
-        CurrentProduct = EditProduct;
-    }
-    ```
-
-4. В разметке окна вместо фиксированного названия вставьте привязку к свойству *WindowName*
-
-    ```xml
-    <Window
-        ...
-        Title="{Binding WindowName}">
-    ```
-
-5. В окне создайте сетку из трёх колонок: в первой у нас будет изображение, во второй редактируемые поля продукта, а в третей список материалов
-
-    ```xml
-    <Grid.ColumnDefinitions>
-        <ColumnDefinition Width="auto"/>
-        <ColumnDefinition Width="*"/>
-        <ColumnDefinition Width="auto"/>
-    </Grid.ColumnDefinitions>
-    ```
-
-6. Во вторую колонку добавьте **StackPanel** с границами (чтобы визуальные компоненты не прилипали к границам окна) и в этом списке разместите редактируемые элементы
-
-    >На форме должны быть предусмотрены следующие поля: артикул, наименование, тип продукта (выпадающий список), изображение, количество человек для производства, номер производственного цеха, минимальная стоимость для агента и подробное описание
-
-    ```xml
-    <StackPanel 
-        Margin="5">
-
-        <Label Content="Артикул"/>
-        <TextBox Text="{Binding CurrentProduct.ArticleNumber}"/>
-
-        <Label Content="Наименование продукта"/>
-        <TextBox Text="{Binding CurrentService.Title}"/>
-
-        ...
-
-    </StackPanel>
-    ```
-
-    Обычные поля наклепайте по шаблону сами, а я подробнее остановлюсь на полях: тип продукта, изображение и описание:
-
-    * Выбор типа продукта из списка
-
-        В классе окна объявляем свойство *ProductTypes* - список типов продукции
-
-        ```cs
-        public IEnumerable<ProductType> ProductTypes { get; set; }
-        ```
-
-        И в конструкторе получаем его из поставщика данных
-
-        ```cs
-        ProductTypes = Globals.DataProvider.GetProductTypes();
-        ```
-
-        В выпадающий список мы должны передать собственно список выбираемых объектов (*ItemsSource*) и текущий объект (*SelectedItem*) из этого списка. Но у нас в модели **Product** нет объекта **ProductType**, есть отдельные поля *ProductTypeID* и *ProductTypeTitle*. Изменим модель, вместо этих полей сделаем поле *CurrentProductType*, значение которого будем получать из списка типов по ID
-
-        В поставщик данных добавим переменную, в которой будет храниться список типов продукции:
-
-        ```cs
-        private List<ProductType> ProductTypes = null;
-        ```
-
-        Исправим метод **GetProductTypes**, чтобы он считывал список только в первый раз (поиск объектов происходит по хешу и нам важно, чтобы этот список был всегда один и тот же)
-
-        ```cs
-        public IEnumerable<ProductType> GetProductTypes()
-        {
-            if (ProductTypes == null)
-            {
-                ProductTypes = new List<ProductType>();
-                ...
-        ```
-
-        В модели **Product** убираем свойства *ProductTypeTitle*, *ProductTypeID* и добавляем *CurrentProductType* (после этого пересоберите проект и исправьте возникшие ошибки)
-
-        ```cs
-        // public string ProductTypeTitle { get; set; }
-        // public int ProductTypeID { get; set; }
-        public ProductType CurrentProductType { get; set; }
-        ```
-
-        И в классе **MySQLDataProvider** переделайте получение типа продукта:
-
-        ```cs
-        // NewProduct.ProductTypeID = Reader.GetInt32("ProductTypeID");
-        // NewProduct.ProductTypeTitle = Reader["ProductTypeTitle"].ToString();
-        NewProduct.CurrentProductType = GetProductType(Reader.GetInt32("ProductTypeID"));
-        ```
-
-        Реализация метода *GetProductType*:
-
-        ```cs
-        private ProductType GetProductType(int Id)
-        {
-            // тут заполнится список типов продукции, если он ещё пустой
-            GetProductTypes();
-            return ProductTypes.Find(pt => pt.ID == Id);
-        }
-        ```
-
-        Теперь в верстке окна редактирования продукции мы можем использовать выпадающий список
-
-        ```xml
-        <ComboBox 
-            ItemsSource="{Binding ProductTypes}"
-            SelectedItem="{Binding CurrentProduct.CurrentProductType}"/>
-        ```
-
-    * смена изображения продукции
-
-        Вывод изображения производится как и в главном окне
-
-        ```xml
-        <Image
-            Width="200" 
-            Height="200"
-            Source="{Binding CurrentProduct.ImagePreview,TargetNullValue={StaticResource defaultImage}}" />
-        ```
-
-        А для смены изображения используем стандартный диалог Windows, повесив его на кнопку *Сменить картинку* (кнопку добавьте сами в **StackPanel**)
-
-        Обработчик кнопки:
-
-        ```cs
-        private void ChangeImage_Click(object sender, RoutedEventArgs e)
-        {
-            OpenFileDialog GetImageDialog = new OpenFileDialog();
-            // задаем фильтр для выбираемых файлов
-            // до символа "|" идет произвольный текст, а после него шаблоны файлов разделенные точкой с запятой
-            GetImageDialog.Filter = "Файлы изображений: (*.png, *.jpg)|*.png;*.jpg";
-            // чтобы не искать по всему диску задаем начальный каталог
-            GetImageDialog.InitialDirectory = Environment.CurrentDirectory;
-            if (GetImageDialog.ShowDialog() == true)
-            {
-                // перед присвоением пути к картинке обрезаем начало строки, т.к. диалог возвращает полный путь
-                CurrentProduct.Image = GetImageDialog.FileName.Substring(Environment.CurrentDirectory.Length);
-                // обратите внимание, это другое окно и другой Invalidate, который реализуйте сами
-                Invalidate();
-            }
-        }
-        ```
-
-    * Многострочное описание
-
-        Тут просто - разрешаем переносы и задаем высоту элемента
-
-        ```xml
-        <Label Content="Описание продукта"/>
-        <TextBox 
-            AcceptsReturn="True"
-            Height="2cm"
-            Text="{Binding CurrentProduct.Description}"/>
-        ```
-
-7. Сохранение введенных данных
-
-    В разметку добавьте кнопку **Сохранить** и напишите обработчик
-
-    ```cs
-    private void Button_Click(object sender, RoutedEventArgs e)
-    {
-        // вся работа с БД должна быть завернута в исключения
-        try
-        {
-            // сюда добавлять проверки
-
-            // метод SaveProduct реализуем ниже
-            Globals.DataProvider.SaveProduct(CurrentProduct);
-            DialogResult = true;
-        }
-        catch (Exception ex)
-        {
-            MessageBox.Show(ex.Message);
-        }
-    }
-    ```
-
-    В интерфейсе **IDataProvider** объявляем метод *SaveProduct*:
-
-    ```cs
-    void SaveProduct(Product ChangedProduct);
-    ```
-
-    И реализуем его в классе поставщика данных 
-
-    ```cs
-    public void SaveProduct(Product ChangedProduct)
-    {
-        Connection.Open();
-        try
-        {
-            if (ChangedProduct.ID == 0)
-            {
-                // новый продукт - добавляем запись
-                string Query = @"INSERT INTO Product
-                (Title,
-                ProductTypeID,
-                ArticleNumber,
-                Description,
-                Image,
-                ProductionPersonCount,
-                ProductionWorkshopNumber,
-                MinCostForAgent)
-                VALUES
-                (@Title,
-                @ProductTypeID,
-                @ArticleNumber,
-                @Description,
-                @Image,
-                @ProductionPersonCount,
-                @ProductionWorkshopNumber,
-                @MinCostForAgent)";
-
-                MySqlCommand Command = new MySqlCommand(Query, Connection);
-                Command.Parameters.AddWithValue("@Title", ChangedProduct.Title);
-                Command.Parameters.AddWithValue("@ProductTypeID", ChangedProduct.CurrentProductType.ID);
-                Command.Parameters.AddWithValue("@ArticleNumber", ChangedProduct.ArticleNumber);
-                Command.Parameters.AddWithValue("@Description", ChangedProduct.Description);
-                Command.Parameters.AddWithValue("@Image", ChangedProduct.Image);
-                Command.Parameters.AddWithValue("@ProductionPersonCount", ChangedProduct.ProductionPersonCount);
-                Command.Parameters.AddWithValue("@ProductionWorkshopNumber", ChangedProduct.ProductionWorkshopNumber);
-                Command.Parameters.AddWithValue("@MinCostForAgent", ChangedProduct.MinCostForAgent);
-                Command.ExecuteNonQuery();
-            }
-            else
-            {
-                // существующий продукт - изменяем запись
-
-                string Query = @"UPDATE Product
-                SET
-                Title = @Title,
-                ProductTypeID = @ProductTypeID,
-                ArticleNumber = @ArticleNumber,
-                Description = @Description,
-                Image = @Image,
-                ProductionPersonCount = @ProductionPersonCount,
-                ProductionWorkshopNumber = @ProductionWorkshopNumber,
-                MinCostForAgent = @MinCostForAgent
-                WHERE ID = @ID";
-
-                MySqlCommand Command = new MySqlCommand(Query, Connection);
-                Command.Parameters.AddWithValue("@Title", ChangedProduct.Title);
-                Command.Parameters.AddWithValue("@ProductTypeID", ChangedProduct.CurrentProductType.ID);
-                Command.Parameters.AddWithValue("@ArticleNumber", ChangedProduct.ArticleNumber);
-                Command.Parameters.AddWithValue("@Description", ChangedProduct.Description);
-                Command.Parameters.AddWithValue("@Image", ChangedProduct.Image);
-                Command.Parameters.AddWithValue("@ProductionPersonCount", ChangedProduct.ProductionPersonCount);
-                Command.Parameters.AddWithValue("@ProductionWorkshopNumber", ChangedProduct.ProductionWorkshopNumber);
-                Command.Parameters.AddWithValue("@MinCostForAgent", ChangedProduct.MinCostForAgent);
-                Command.Parameters.AddWithValue("@ID", ChangedProduct.ID);
-                Command.ExecuteNonQuery();
-            }
-        }
-        finally
-        {
-            Connection.Close();
-        }
-    }
-    ```
-
-8. Открытие окна редактирования для существующей и новой продукции
-
-    * для редактирования существующей продукции в списке продукции реализуем обработчик двойного клика
-
-        ```cs
-        private void ProductListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
-        {
-            // в создаваемое окно передаем выбранный продукт
-            var NewEditWindow = new EditWindow(ProductListView.SelectedItem as Product);
-            if ((bool)NewEditWindow.ShowDialog())
-            {
-                // при успешном сохранении продукта перечитываем список продукции
-                ProductList = Globals.DataProvider.GetProducts();
-            }
-        }
-        ```
-
-    * для создания нового продукта в разметке главного окна создайте кнопку "Добавить продукцию" (либо в верхней панели, либо в левой) и в её обработчике создайте новый экземпляр продукта
-
-        ```cs
-        var NewEditWindow = new EditWindow(new Product());
-        ...
-        ```
-9. Проверки перед сохранением продукта
-
-    Все проверки вставляем в обработчик кнопки "Сохранить" окна редактирования, т.к. это относится к бизнес-логике, до вызова метода сохранения продукта
-
-    * Стоимость продукции не может быть отрицательной
-
-        ```cs
-        if (ChangedProduct.MinCostForAgent < 0)
-            throw new Exception("Цена продукта не может быть отрицательной");
-        ```
-
-    * Стоимость продукции записывается только с точностью до сотых
-
-    * Реализована проверка артикула на уникальность
-
-        Тут по идее надо делать запрос к базе, но у нас есть метод получения списка продукции и мы можем искать в нём используя LINQ-запросы
-
-<table style="width: 100%;"><tr><td style="width: 40%;">
-<a href="../articles/cs_coloring2.md">Подсветка элементов по условию. Дополнительные выборки.Массовая смена цены продукции.
-</a></td><td style="width: 20%;">
-<a href="../readme.md">Содержание
-</a></td><td style="width: 40%;">
-<a href="../articles/kotlin.md">Основы языка Kotlin
-</a></td><tr></table>

+ 94 - 151
articles/cs_edit_product2.md

@@ -64,14 +64,14 @@
 
 ## Создание окна редактирования продукции
 
-Для добавления и редактирования мы будем использовать одно и то же окно. Заголовок окна будем вычислять по наличию ID у продукции (у новой записи это поле равно `0`)
+Для добавления и редактирования мы будем использовать одно и то же окно. Заголовок окна будем вычислять по наличию поля **ID** у продукции (у новой записи это поле равно `0`)
 
 1. Создайте новое окно: **EditProductWindow** (в папке **Windows** - не забываем про логическую структуру проекта)
 
 1. В классе окна **EditProductWindow** добавьте **свойство** *currentProduct*, в котором будет храниться добавляемый/редактируемый экземпляр продукции:
 
     ```cs
-    public Product currentProduct { get; set; };
+    public Product currentProduct { get; set; }
     ```
 
 1. В разметке окна вставьте аттрибут *Name*
@@ -85,11 +85,12 @@
 1. В конструктор окна добавьте параметр типа **Product**, присвойте его ранее объявленному свойству. Задайте заголовок окна:
 
     ```cs
-    public EditWindow(Product EditProduct)
+    public EditProductWindow(Product editProduct)
     {
-        currentProduct = editProduct;
         InitializeComponent();
-        root.Title = currentProduct.Id == 0 ? "Новый продукт" : "Редактирование продукта";
+        DataContext = this;
+        currentProduct = editProduct;
+        root.Title = currentProduct.ID == 0 ? "Новый продукт" : "Редактирование продукта";
     }
     ```
 
@@ -114,60 +115,53 @@
 
         <Label Content="Артикул"/>
         <TextBox
-            Text="{Binding #root.currentProduct.ArticleNumber}"/>
+            Text="{Binding currentProduct.ArticleNumber}"/>
 
         <Label Content="Наименование продукта"/>
-        <TextBox Text="{Binding #root.currentProduct.Title}"/>
+        <TextBox Text="{Binding currentProduct.Title}"/>
 
         ...
 
     </StackPanel>
     ```
 
-    Обычные поля наклепайте по шаблону сами, а я подробнее остановлюсь на полях: *тип продукта*, *изображение* и *описание*:
+    Обычные поля (числовые и строковые) наклепайте по шаблону сами, а я подробнее остановлюсь на полях: _тип продукта_, _изображение_ и _описание_:
 
     * Выбор типа продукта из списка
 
-        В классе окна объявляем свойство *productTypeList* - список типов продукции
+        В классе окна объявляем свойство _productTypeList_ - список типов продукции
 
         ```cs
-        public IEnumerable<ProductType> productTypeList { get; set; }
+        public List<ProductType> productTypeList { get; set; }
         ```
 
         И в конструкторе получаем его из БД:
 
         ```cs
-        using (var context = new ksmirnovContext())
-        {
-            productTypeList = context.ProductTypes.ToList();
-        }        
+        productTypeList = Globals.dataProvider.getProductTypes().ToList();
         ```
 
-        В вёрстке окна редактирования продукции мы можем использовать выпадающий список, атрибут *SelectedItem* которого позволяет отобразить **текущее** значение редактируемого элемента
+        В вёрстке окна редактирования продукции мы можем использовать выпадающий список, атрибут _SelectedIndex_ которого позволяет отобразить **текущее** значение редактируемого элемента
 
         ```xml
         <ComboBox 
-            ItemsSource="{Binding #root.productTypeList}"
-            SelectedItem="{Binding #root.currentProduct.ProductType}"/>
+            ItemsSource="{Binding productTypeList}"
+            SelectedIndex="{Binding selectedProductIndex}"/>
         ```
 
-        >Из документации: *Класс 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);
+        // в классе окна
+        public int selectedProductIndex { get; set; } = -1;
+        ...
+
+        // в конструкторе окна
+        if (currentProduct.ID > 0) {
+            selectedProductIndex = productTypeList.FindIndex(pt =>  pt.ID == currentProduct.ProductTypeID);
         }
         ```
 
-        Здесь мы проверяем определён ли вообще объект (у нового продукта его ещё нет), нужного ли он типа и совпадет ли его **Id** с **Id** текущего типа продукции 
-
     * смена изображения продукции
 
         Вывод изображения производится как и в главном окне
@@ -177,7 +171,7 @@
             Name="CurrentProductImage"
             Width="200" 
             Height="200"
-            Source="{Binding #root.currentProduct.ImageBitmap}" />
+            Source="{Binding currentProduct.ImageUri}" />
         ```
 
         А для смены изображения используем стандартный диалог открытия файлов, повесив его на кнопку *Сменить картинку* (кнопку добавьте сами в центральную колонку)
@@ -187,31 +181,21 @@
         ```cs
         private void ChangeImage_Click(object sender, RoutedEventArgs e)
         {
-            var getImageDialog = new OpenFileDialog();
+            OpenFileDialog GetImageDialog = new OpenFileDialog();
 
             // задаем фильтр для выбираемых файлов
-            getImageDialog.Filters.Add(
-                new FileDialogFilter() {
-                    Name = "Файлы изображений", 
-                    Extensions = {"png", "jpg", "jpeg"}});
+            // до символа "|" идет произвольный текст, а после него шаблоны файлов разделенные точкой с запятой
+            GetImageDialog.Filter = "Файлы изображений: (*.png, *.jpg)|*.png;*.jpg";
 
             // чтобы не искать по всему диску задаем начальный каталог
-            // В ТЕКУЩЕЙ ВЕРСИИ АВАЛОНИИ ТАКОГО СВОЙСТВА НЕТ
-            // getImageDialog.InitialDirectory = Environment.CurrentDirectory;
-
-            // открываем СИНХРОННО диалог
-            // в ответ получим массив выбранных файлов
-            var files = await getImageDialog.ShowAsync(this);
-            if (files != null)
+            GetImageDialog.InitialDirectory = Environment.CurrentDirectory;
+            if (GetImageDialog.ShowDialog() == true)
             {
                 // перед присвоением пути к картинке обрезаем начало строки, т.к. диалог возвращает полный путь
-                currentProduct.Image = files[0].Substring(Environment.CurrentDirectory.Length);
+                currentProduct.Image = GetImageDialog.FileName.Substring(Environment.CurrentDirectory.Length);
 
-                // тут нужно либо реализовать интерфейс InotifyPropertyChanged
-                // и обновить информацию о картинке
-
-                // либо тупо поменять свойство нужного контрола
-                CurrentProductImage.Source = currentProduct.ImageBitmap;
+                // обратите внимание, это другое окно и другой Invalidate, который реализуйте сами
+                Invalidate();
             }
         }
         ```
@@ -225,7 +209,7 @@
         <TextBox 
             AcceptsReturn="True"
             Height="200"
-            Text="{Binding #root.currentProduct.Description}"/>
+            Text="{Binding currentProduct.Description}"/>
         ```
 
 1. Сохранение введенных данных
@@ -233,50 +217,56 @@
     В разметку добавьте кнопку **Сохранить** и напишите обработчик
 
     ```cs
-    private void Button_Click(object sender, RoutedEventArgs e)
+    private void SaveButton_Click(object sender, RoutedEventArgs e)
     {
-        using (var context = new esmirnovContext())
+        // перед сохранением мы должны выполнить несколько проверок, поэтому заворачиваем в исключение
+        try
         {
-            // вся работа с БД должна быть завернута в исключения
-            try
+            // могли изменить тип продукта - сохраняем ID
+            if (ProductTypeComboBox.SelectedItem != null) 
+            {
+                currentProduct.ProductTypeID = ((ProductType)ProductTypeComboBox.SelectedItem).ID;
+            } else
             {
-                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);
-                    }
-                    
-                }
+                throw new Exception("Не выбран тип продукта");
             }
-            catch (Exception ex)
+
+            // TODO проверить стоимость продукции (не более двух знаков после запятой и положительное число)
+
+            // TODO проверка артикула (найти в базе запись с таким же артикулом и отличающимся ID)
+
+            Globals.dataProvider.saveProduct(currentProduct);
+            DialogResult = true;
+        }
+        catch (Exception ex)
+        {
+            MessageBox.Show(ex.Message);
+        }   
+    }
+    ```
+
+1. Реализация метода _saveProduct_
+
+    ```cs
+    public void saveProduct(Product product)
+    {
+        using (MySqlConnection db = new MySqlConnection(connectionString))
+        {
+            if (product.ID == 0)
+            {
+                // TODO новый продукт
+            }
+            else
             {
-                // if(ex.InnerException != null)
-                //     MessageBox.Show(ex.InnerException.Message);
-                // else
-                //     MessageBox.Show(ex.Message);            
+                // изменение существующего
+                db.Execute("UPDATE Product SET Title=@Title, ProductTypeID=@ProductTypeID, " +
+                    "ArticleNumber=@ArticleNumber, Description=@Description, " +
+                    "Image=@Image, ProductionPersonCount=@ProductionPersonCount, " +
+                    "ProductionWorkshopNumber=@ProductionWorkshopNumber, " +
+                    "MinCostForAgent=@MinCostForAgent " +
+                    "WHERE ID=@ID", product);
             }
-        }    
+        }
     }
     ```
 
@@ -287,15 +277,13 @@
     ```cs
     private void ProductListView_MouseDoubleClick(object sender, MouseButtonEventArgs e)
     {
-        var currentProduct = (sender as ListBox).SelectedItem as Product;
-        // в создаваемое окно передаем выбранный продукт
+        var product = (sender as ListBox).SelectedItem as Product;
 
+        // в создаваемое окно передаем выбранный продукт
         var newEditWindow = new EditProductWindow(product);
-        var res = await newEditWindow.ShowDialog<bool>(this);
-        if (res)
+        if ((bool)newEditWindow.ShowDialog())
         {
-            // эту функцию реализуйте сами - тут перечитывание списка продукции из базы
-            getProductList();   
+            Invalidate();
         }
     }
     ```
@@ -307,41 +295,11 @@
     ...
     ```
 
-## Проверки перед сохранением продукта
-
-Все проверки вставляем в обработчик кнопки "Сохранить" окна редактирования,  до вызова метода сохранения продукта
-
-* Стоимость продукции не может быть отрицательной
-
-    ```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;
-```
+Добавьте кнопку *Удалить* в среднюю колоку (рядом с кнопкой *сохранить*). Атрибут **Visible** привяжите к **ID** продукта (т.е. у нового продукта кнопки удалить быть не должно). Можно реализовать через _Binding_, но можно задать в конструкторе
 
 И реализуйте обработчик клика по этой кнопке
 
@@ -352,49 +310,34 @@ private void DeleteProductButton_Click(object sender, RoutedEventArgs e)
     {
         try
         {
-            // тут вставляем проверки, требуемые по ТЗ
+            // проверки, требуемые по ТЗ
 
-            // + исключение, если есть продажи
-            var saleCount = context.ProductSales
-                .Count(ps => ps.ProductId == currentProduct.Id);
+            // TODO исключение, если есть продажи
+            var saleCount = Globals.dataProvider.saleCount(currentProduct.ID);
 
             if (saleCount > 0)
                 throw new Exception("Нельзя удалять продукт с продажами");
 
-            // - удаление списка материалов продукта (НЕ ПРОВЕРЯЛ)
-            context.ProductMaterials.RemoveRange(
-                context.ProductMaterials.Where(pm => pm.ProductId == currentProduct.Id)
-            );
+            // TODO удаление списка материалов продукта
+            Globals.dataProvider.removeProductMaterial(currentProduct.ID);
 
-            // - удаление истории изменения цен (если есть)
+            // TODO удаление истории изменения цен (если есть)
 
-            var product = context.Products.Find(currentProduct.Id);
-            context.Products.Remove(product);
-            if (context.SaveChanges() > 0)
-            {
-                Close(true);
-            }
+            // TODO удаление продукта
+            Globals.dataProvider.removeProduct(currentProduct.ID);
+            DialogResult = true;
         }
         catch (Exception ex)
         {
-            // if(ex.InnerException != null)
-            //    MessageBox.Show(ex.InnerException.Message);
-            // else
-            //    MessageBox.Show(ex.Message);            
+            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-запросами)
+**Задание**
 
-[Подробнее про RAW SQL](https://learn.microsoft.com/en-us/ef/core/querying/sql-queries)
+Самостоятельно реализовать код помеченный комментарием **TODO**
 
 ---