Евгений Колесников 2 år sedan
förälder
incheckning
595c59bcac
8 ändrade filer med 271 tillägg och 4 borttagningar
  1. 90 1
      articles/cs_misc.md
  2. 178 1
      articles/sprint_lab.md
  3. BIN
      img/imp01.png
  4. BIN
      img/imp04.png
  5. BIN
      img/imp2.png
  6. BIN
      img/imp3.png
  7. BIN
      img/sl001.png
  8. 3 2
      readme.md

+ 90 - 1
articles/cs_misc.md

@@ -3,6 +3,9 @@
 В этой лекции будут свалены материалы, не вошедшие в другие лекции, но всплавшие при решении лабораторных работ
 
 * [Всплывающие подсказки](#всплывающие-подсказки)
+* [Сохранение CSV](#сохранение-csv)
+* [Выбор из списка с возможностью редактирования](#выбор-из-списка-с-возможностью-редактирования)
+* [Ввод даты или времени](#ввод-даты-или-времени)
 
 <!-- TODO 
 - контекстное меню 
@@ -45,4 +48,90 @@
     </Image>
     ```
 
-Естественно вместо фиксированного текста можете сделать привязку (binding) к вычисляемому свойству класса.
+Естественно вместо фиксированного текста можете сделать привязку (binding) к вычисляемому свойству класса.
+
+## Сохранение CSV
+
+Задача состоит из двух подзадач:
+
+- вызов стандартного диалога сохранения для получения полного имени файла
+- собственно сохранение данных
+
+### Вызов стандартного диалога сохранения
+
+```cs
+// создание диалога и инициализация
+var saveFileBox = new SaveFileDialog();
+saveFileBox.Title = "Сохранить список мероприятий";
+saveFileBox.InitialFileName = "events.csv"; 
+var settingsFileName = await saveFileBox.ShowAsync(this);
+
+// если выбор успешный, то в переменной имя целевого файла
+if (settingsFileName != null) {
+    // тут логика по сохранению данных
+}
+```
+
+### Сохранение CSV
+
+В принципе мы это [проходили на 2-м курсе](https://github.com/kolei/OAP/blob/master/articles/t5_file_types.md#CSV)
+
+```cs
+// тут копипаста из лекции
+using (var writer = new StreamWriter(settingsFileName)){
+    var enCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
+
+    //TODO тут вставить выбор всех мероприятий из БД и перебор в цикле
+    
+    // строковое поле выводим в кавычках
+    // вместо фиксированных значений вставляем данные из модели  
+    writer.WriteLine("{0},{1},\"{2}\"", 
+        1,
+        // дробные числа выводим с точкой
+        2.0.ToString("0.00", enCulture),
+        "qq");
+}
+```
+
+## Выбор из списка с возможностью редактирования
+
+Компонент **AutoCompleteBox** выглядит как текстовое поле, но при вводе данных показывает (и позволяет выбрать) варианты из списка:
+
+```xml
+<AutoCompleteBox 
+    Name="autoCompleteBox"
+    Text="{Binding #root.currentCity}"
+    ItemsSource="{Binding #root.cityList}" />
+```
+
+* cityList - массив строк (либо делаем отдельный список названий городов, либо переопределяем toString)
+
+При сохранении смотрим что введено в *autoCompleteBox.Text*, если название есть в списке городов, то сразу берем его `id`, если нет, то сначала добавляем город в базу, потом смотрим на его `id`
+
+## Ввод даты или времени
+
+### DatePicker
+
+Для ввода даты можно использовать компонент **DatePicker** (есть еще **CalendarDatePicker**, но его я не копал):
+
+```xml
+<DatePicker 
+    Name="datePicker"
+    SelectedDate="{Binding #root.currentDate}"/>
+```
+
+Тип данных для атрибута *SelectedDate*: **DateTimeOffset**, поэтому напрямую поле БД ему скармливать нельзя.
+
+Ниже пример для свойства класса окна
+
+```cs
+public DateTimeOffset currentDate { get; set; } = new DateTimeOffset( DateTime.Now);
+```
+
+вы можете при создании окна присвоить текущее значение даты этому свойству, или в вёрстке поменять на геттер *currentUser.bithDayDTO*. Где суффикс **DTO** означает, что это не оригинальное значение даты из базы, а преобразованное в **DateTimeOffset**. И не забыть при сохранении преобразовать обратно в дату.
+
+### TimePicker
+
+Для ввода времени можно использовать компонент **TimePicker** (как пользоваться раскопайте сами).
+
+

+ 178 - 1
articles/sprint_lab.md

@@ -12,9 +12,186 @@
 
 1. На сервере **mysql** кодировка баз по-умолчанию *latin*, перед созданием своих таблиц поменяйте на *utb8mb4* (либо при создании таблиц указывайте эту кодировку).
 
-1. Часто встречается задача типа "продукт может иметь связанные продукты". Это классическая связь *многие-ко-многим*, т.е. у продукта может быть много связанных продуктов и у связанного продукта может быть много родителей (продуктов, к которым он привязан). Решается добавлением таблицы связей, внешние ключи которой (например, *parent_id* и *child_id*) ссылаются на одну и ту же таблицу продукции.
+1. Часто встречается задача типа "продукт может иметь связанные продукты". Это классическая связь *многие-ко-многим*, т.е. у продукта может быть много связанных продуктов и у связанного продукта может быть много родителей (продуктов, к которым он привязан). Решается добавлением таблицы связей, внешние ключи которой (например, *parent_product_id* и *child_product_id*) ссылаются на одну и ту же таблицу продукции.
 
 1. При клике кнопкой мыши на визуальный объект параметр *sender* в обработчике указывает на объект по которому кликнули. Если это элемент списка (**ListBox**), то можно явно приводить к нужному типу, а если просто кнопка ("массовая смена продукции", "удаление продукции" и т.п.), то для получения экземпляра объекта вы должны использовать не *sender*, а нужный объект (например, **ProductListBox**), причём сначала убедиться, что в списке есть выделенный элемент (**SelectedItem**).
 
 1. У **ListBox**-а два очень похожих по названию свойства: *SelectedItem* (активный элемент списка) и _SelectedItem**s**_ (несколько активных элементов списка, если разрешен мультивыбор).
 
+1. Учитесь работать с GIT: отладили кусок кода - сделали коммит. Если время вышло, а проект "не собирается", то просто откатываем изменения на последний рабочий коммит и его публикуем.
+
+1. Выпадающие списки (**ComboBox**) и поля с автозаполнением (**AutoCompleteBox**). С ними может быть связано два типа проблем:
+
+    * в выпадающем списке показывает не содержимое поля из базы, а название класса:
+
+        ![](../img/sl001.png)
+
+        Проблема в том, что компонент **ComboBox** рассчитан на показ списка **строк** и при выводе элемента другого типа использует метод *ToString*. Соответственно, исправить это просто - переопределить метод *ToString* в нашем классе:
+
+        ```cs
+        public override string ToString() {
+            return Title;
+        }
+        ```
+
+        У компонента **AutoCompleteBox** проблема та же, просто он показывает не весь список, а только те элементы, которые похожи на вводимую строку и тут накладывается и вторая проблема - сравнение объектов. 
+
+    * если к такому списку "привязано" поле в форме редактирования, то не показывает текущее значение.
+
+        Проблема в том, что напрямую в C# сравниваются только **скалярные** типы (целое, строка и т.п.). А при сравнении объектов используется метод *Equals*, который, по-умолчанию, сравнивает уникальные идентификаторы объектов.
+
+        Решение то же - переопределить метод, в котором сравнивать идентификаторы (можно и названия, но сравнение целых работает быстрее):
+
+        ```cs
+        public override bool Equals(object obj)
+        {
+            return (obj != null) && 
+               (obj is ProductType) && 
+               (this.Id == (obj as ProductType).Id);
+        }
+        ```
+
+    Выше был "правильный" вариант, но можно упростить задачу и в списке выводить строки. Для этого выбирать не целиком объект **ProductType**, а только его название:
+
+    ```cs
+    public List<string> productTypeStringList { get; set; }
+
+    ...
+
+    productTypeStringList = context.ProductTypes
+        .Select(pt => pt.TitleType)
+        .ToList();
+    ```
+
+    Вывод списка и поиск совпадений будет работать автоматически без дополнительных телодвижений. Но, естественно, есть и ложка дёгтя: когда мы захотим сохранить объект (например, **Product** из лекций) у нас не будет полной информации о типе продукции (мы же выбирали только названия, а поля `id` у нас нет). Придётся искать в словаре типов по названию.
+
+1. Удаление записей, на которые есть ссылки из других таблиц (продукция, по которой были продажи; активности, которым назначено жюри)
+
+    Прежде чем удалять основной объект, нужно удалить связанные объекты (конечно, если это допускается логикой задачи). Для массового удаления есть метод **RemoveRange**:
+
+    ```cs
+    context.ProductMaterials.RemoveRange(
+        context.ProductMaterials.Where(pm => pm.ProductId == currentProduct.Id)
+    );
+    ```
+
+    можно делать и через RAW запросы:
+
+    ```cs
+    context.Database
+        .ExecuteSqlRaw($"DELETE FROM ProductMaterial WHERE ProductId={currentProduct.Id}");
+
+    // также можно удалить и сам продукт
+    context.Database
+        .ExecuteSqlRaw($"DELETE FROM Product WHERE Id={currentProduct.Id}");
+    ```
+
+1. Dapper
+
+   Как показал опыт, **EntityFramework** очень громоздкий и не всегда понятный.
+
+   Есть более легковесные альтернативы, например **Dapper**. Это микро-фреймворк, который тоже может результат SQL-запроса поместить в модель, но при этом модель мы должны "нарисовать" сами и знать SQL-синтаксис.
+
+   * Установка
+
+        Через **NuGet** или в консоли установить пакеты: **MySqlConnector** и **Dapper**
+
+        ```
+        dotnet add package MySqlConnector
+        dotnet add package Dapper
+        ```
+
+    * В приложении прописать строку подключения (где-нибудь в глобальном статическом классе)
+
+        ```cs
+        static string connectionString = "Server=kolei.ru; User ID=esmirnov; Password=111103; Database=esmirnov";
+        ```
+
+    * Создать модели для нужных таблиц (мне лень все поля переписывать, смысл должен быть понятен).
+
+        ```cs
+        public class Product
+        {
+            public int ID { get; set; }
+            public string Title { get; set; }
+            
+            public override string ToString() {
+                return Title;
+            }
+        }
+        ```
+
+    * Чтение данных:
+
+        ```cs
+        public List<Product> productList { get; set; }
+
+        ...
+
+        using (MySqlConnection db = new MySqlConnection(connectionString))
+        {
+             productList = db.Query<Product>(
+                "SELECT ID,Title FROM Product")
+                .ToList();
+        }
+        ```
+
+        То есть мы считываем данные сразу в список продукции. Обратите внимание, названия полей в БД должны соответствовать свойствам модели. В этом ничего страшного нет, названия полей можно поменять при выборке используя конструкцию `AS`, например: `SELECT id AS ID FROM Product`.
+
+    * Получение одной записи (здесь и далее непроверенный код из гугла)
+
+        ```cs
+        db.Query<User>(
+            "SELECT * FROM Users WHERE Id = @id", new { id })
+            .FirstOrDefault();
+        ```
+
+        Тут видно, как выполнять запросы с параметрами, вторым параметром в методе **Query** передаётся объект с параметрами, которые заменяют одноименные литералы в тексте запроса.
+
+        Естественно, в качестве такого объекта может быть экземпляр соответствующей модели (см. следующий запрос)
+
+    * Добавление записи в таблицу (в этом и последующих запросах результат не нужен, поэтому используется метод **Execute**)
+
+        ```cs
+        db.Execute(
+            "INSERT INTO Users (Name, Age) VALUES(@Name, @Age)", 
+            user);
+        ```
+
+        Здесь данные для запроса беруться сразу из модели (*user* это экземпляр класса **User**)
+
+    * Редактирование записи:
+
+        ```cs
+        db.Execute(
+            "UPDATE Users SET Name = @Name, Age = @Age WHERE Id = @Id", 
+            user);
+        ```
+
+    * Удаление записи:
+
+        ```cs
+        db.Execute(
+            "DELETE FROM Users WHERE Id = @id", 
+            new { id });
+        ```
+
+    **Итого:** с одной стороны нужно вручную создавать модели, с другой не нужно делать реконструирование при смене структуры и сам код более "прозрачный".
+
+    Для того, чтобы получить связанные данные, достаточно добавить связанные поля в модель и выбрать их в запросе.
+
+    Например, мы хотим в продукте получать сразу название **типа** продукции:
+
+    ```cs
+    // в модели добавляем свойство 
+    public string TypeTitle { get; set; }
+
+    // при запросе получаем его
+    productList = db.Query<Product>(
+            "SELECT p.ID, p.Title, pt.TitleType AS TypeTitle 
+            FROM Product p, ProductType pt 
+            WHERE p.ProductTypeID=pt.ID")
+        .ToList();
+    ```
+
+    Получается, что у нас модель **Product** уже не совсем соответствует таблице, зато мы получаем все нужные данные одним запросом.

BIN
img/imp01.png


BIN
img/imp04.png


BIN
img/imp2.png


BIN
img/imp3.png


BIN
img/sl001.png


+ 3 - 2
readme.md

@@ -155,6 +155,7 @@ http://sergeyteplyakov.blogspot.com/2014/01/microsoft-fakes-state-verification.h
     + [Подсветка элементов по условию. Массовая смена цены продукции.](./articles/cs_coloring2.md)
     + [Создание, изменение, удаление продукции](./articles/cs_edit_product2.md)
     + [Вывод списка материалов продукта. CRUD материалов продукта](./articles/cs_product_material.md)
+    + [Разное](./articles/cs_misc.md)
 
   - [Разработка API](#разработка-своего-api)
 
@@ -347,6 +348,8 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [Вывод списка материалов продукта. CRUD материалов продукта](./articles/cs_product_material.md)
 
+1. [Разное](./articles/cs_misc.md)
+
 ### Разработка своего API.
 
 1. [API. REST API. Создание сервера ASP.NET Core. Swagger.](./articles/api_asp_net_core.md)
@@ -355,8 +358,6 @@ https://office-menu.ru/uroki-sql Уроки SQL
 
 1. [HTTP запросы в C#. Получение списка материалов выбранного продукта. Авторизация.](./articles/cs_http.md)
 
-1. [Разное](./articles/cs_misc.md)
-
 **Лабораторные работы**
 
 1. [Замечания по итогам и предисловие для следующих поколений](./articles/sprint_lab.md)