Урок 10. Паттерн Add/Edit
В этом уроке мы рассмотрим подход к работе со сложными сущностями с иерархической структурой.
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.
При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.
Инструкция по подключению шаблонов находится по ссылке.
Подготовка карточки заказа
Форма заказа
Изменим карточку заказа. Добавим в нее таблицу с позициями заказа и итоговой суммой к оплате.
В базе данных создадим таблицу:
Перейдите в файл TemplateOrderEdit.xml и создайте в ContentPanel две панели:
CommonPanel - панель для отображения общих данных, таких как информация о клиенте, номер и дата заказа, комментарий к заказу и сумма к оплате;
OrderPositionPanel - панель для таблицы с позициями в заказе.
В панели CommonPanel создайте три панели, которые отделите друг от друга разделителями:
ClientPanel - сюда перенесите поля связанные с клиентом;
OrderPanel - сюда перенесите поля номер и дату заказа и комментарий к заказу;
TotalPanel - Здесь создайте текстовое поле "Всего к оплате" (TotalSumToPayTextBox).
В панели OrderPositionPanel создайте таблицу с помощью паттерна Table.
В итоге у вас должна получиться следующая форма:
Добавьте атрибут ChangeForm="False"
для таблицы позиций заказа, чтобы изменение ее источника данных не приводило к изменению формы и активации кнопки "Сохранить" - сохранение позиции заказа уже было на дочерней форме.
Добавим в таблицу OrderPositionDatabaseTable колонки:
А чтобы текст в заголовке колонки UnitPrice полностью отображался, добавим в описание таблицы тэг <ColumnHeadersHeight>
со значением атрибута Value
равным 50.
Создадим переменную Variable, в которой будем считать сумму значений во всех строках колонки TotalPrice таблицы OrderPositionDatabaseTable:
Укажем эту переменную в качестве значения текстового поля "Всего к оплате". Таким образом, полный синтаксис объекта TotalSumToPayTextBox будет выглядеть так:
Здесь используем универсальное значение DataTypeFormat для задания формата строки, в которую будет преобразовано значение переменной.
Запустите приложение и проверьте загрузку формы заказа и отображение элементов на ней:
Перейдем в серверный xml-файл. Переименуем запрос OrderPositionSelectSqlQuery в OrderPositionByOrderIdSelectSqlQuery и перепишем его текст:
Из запроса мы убрали поле Title, так как одноименную колонку в таблице OrderPositionDatabaseTable будем заполнять с помощью тэга <Substitution>
по колонке MaterialId. Для этого подготовим запрос для получения списка ТМЦ:
В запрос добавили поля MaterialCategoryTitle и UnitShortTitle - они нам позже понадобятся.
В следующем разделе создадим Permission, в который добавим запрос MaterialSimpleSelectSqlQuery.
В запросе OrderPositionDeleteSqlQuery замените вызов функции template.order_position_try_delete() на DELETE-запрос.
Вернемся в файл TemplateOrderEdit.xml. Скорректируем соединение с данными OrderPositionPrimaryGetDataConnection, переделав его на переименованный запрос и добавив новые поля:
А так же добавим новое соединение с данными, которое будем использовать в тэге <Substitution>
колонки Title таблицы OrderPositionDatabaseTable:
Заменим описание колонки Title на код:
Команды на открытие формы TemplateOrderPositionEdit.xml сделаем модальными.
А в команду OrderPositionAddFormShowCommand добавим параметр OrderId, т.к. позиции заказа должны быть привязаны к конкретному идентификатору заказа. Сразу создайте параметр форм OrderId в карточке заказа.
Карточка позиции заказа
Перейдем в серверный xml-файл и скорректируем запрос OrderPositionByIdSelectSqlQuery:
Перейдем в файл TemplateOrderPositionEdit.xml и скорректируем первичное соединение с данными:
Удалите объекты TitleLabel и TitleTextBox и весь код, ссылающийся на них.
Петтерн редактируемого выпадающего списка
Скачайте новый архив с паттернами:
Начнем с выпадающего списка для выбора товарно-материальной ценности. Для этого воспользуемся паттерном, который сразу создаст выпадающий список и кнопки для его редактирования и выбора с формы списка ТМЦ:
Выполним следующие настройки паттерна:
Так как это первое поле на форме, то последние два поля оставим пустыми.
После применения паттерна перейдем в серверный xml-файл и внесем некоторые изменения.
Созданный OrderPositionMaterialViewSqlQueryPermission добавим в роли OrderViewRole и OrderEditRole. Добавим в этот Permission запрос MaterialSimpleSelectSqlQuery.
Паттерн создал команды на открытие формы для добавления/редактирования ТМЦ и формы списка ТМЦ. На обе формы передается параметр MaterialTitle. Добавьте этот параметр в описание форм и подставляйте его значение в соответствующее поле в карточке ТМЦ.
Скорректируем запрос MaterialShortSelectSqlQuery, который был создан при выполнении паттерна, добавив необходимые поля:
Вернемся в файл TemplateOrderPositionEdit.xml и поправим соединение с данными для выпадающего списка:
И добавим вторичное соединение с данными, которое будет источником данных для некоторых полей формы:
Скорректируем значение у тэгов <Top>
и <Left>
нового объекта MaterialLabel, присвоив им константы 5 и 10 соответственно.Таким образом, синтаксис объекта будет иметь вид:
Добавьте оставшиеся объекты на форму:
Поле "Количество" (QuantityNumericBox) с признаками Minimum - 0, Increment - 5, DecimalPlaces - 2 и ThousandsSeparator - True. Заполняется на основе OrderPositionPrimaryGetDataConnection;
Поле "Цена продажи за единицу" (UnitPriceNumericBox) с признаками Minimum - 0, Increment - 100, DecimalPlaces - 2 и ThousandsSeparator - True. Заполняется на основе OrderPositionPrimaryGetDataConnection;
Поле "Категория материала" (MaterialCategoryTextBox) заполняется на основе MaterialSecondaryGetDataConnection. Только для чтения;
Поле "Единица измерения материала" (UnitTextBox) заполняется на основе MaterialSecondaryGetDataConnection. Только для чтения;
Поле "Итого" (TotalPriceTextBox) является произведением значений из полей QuantityNumericBox и UnitPriceNumericBox. Только для чтения.
У вас должна получиться форма вида:
Поправьте OrderPositionInsertSetDataConnection и OrderPositionUpdateSetDataConnection так, чтобы в параметры запросов передавались значения из редактируемых полей. Скорректируйте текст самих запросов. Не забудьте передавать параметр OrderId при сохранении новой позиции заказа.
Если попробуете создать заказ и добавить в него позицию, то поймаете ошибку: null value in column "order_id" violates not-null constraint - так как в параметре OrderId с родительской формы приходит значение null. Ниже мы рассмотрим, как решить эту проблему, а пока продолжим работать с карточкой позиции заказа.
Вы можете заметить, что при выборе ТМЦ в поле "Цена продажи за единицу" не подставляется значение цены, которое указывали для ТМЦ при редактировании списка. Давайте это исправим.
Первым делом создадим условие ChangedCondition, которое отслеживает изменение значения выпадающего списка:
Добавим в описание MaterialComboBox вложенный тэг <Change>
со значениями:
Так мы исключим срабатывание условия MaterialComboBoxChangedCondition при обновлении источника данных, что может мешать при открытии карточки существующей позиции заказа.
Создадим команду типа ValueSetCommand, в которой объекту UnitPriceNumericBox будем присваивать значение цены из выбранного в выпадающем списке ТМЦ:
Создадим <Execution>
на условие MaterialChangedCondition с вызовом этой команды:
Также добавим вызов команды UnitPriceNumericBoxValueSetCommand в <Execution>
, который отлавливает параметр Updated из команды MaterialEditFormShowCommand.
В случаях добавления ТМЦ или редактирования списка, мы вызываем команду MaterialComboBoxValueSetCommand, которая спровоцирует условие MaterialChangedCondition на рассылку события. Поэтому в эти <Execution>
не будем добавлять вызов команды UnitPriceNumericBoxValueSetCommand.
Самостоятельно
Реализуйте на форме списка товарно-материальных ценностей (TemplateMaterialList.xml) режим выбора. Этот режим должен работать только для таблицы ТМЦ. Список категорий остается без изменений.
Сохранение вложенной сущности
Проблема
Если мы сейчас запустим приложение и попытаемся добавить позицию в ранее созданный заказ, то у нас все получится, и запись будет корректно сохранена в таблице в базе данных. Но если захотим создать новый заказ, то при сохранении мы словим ошибку вида:
Как видно из сообщения об ошибке, значением параметра OrderId является NULL, а это противоречит ограничению NOT NULL
в описании колонки order_id в таблице template.order_position. Иными словами, мы не можем сохранить позицию заказа в базе данных, пока не сохраним сам заказ и не получим его идентификатор.
Есть два варианта решения сложившейся ситуации:
Решение 1. Сохранять с родительской формы
Суть решения заключается в том, что мы передаем данные на родительскую форму, пишем их в таблицу DatabaseTable через ее свойства AddRow, AddRows, UpdateRow, UpdateRows и DeleteRowsByIndices, как мы делали на форме списка городов. При попытке сохранить новый заказ выполняем последовательность из двух команд типа SaveCommand:
первая для SetDataConnection на вставку самой записи заказа, с возвратом на форму идентификатора нового заказа в результат команды SaveCommand;
вторая для DatabaseTableSetDataConnection для сохранения изменений позиций заказа, в котором используем результат первой команды.
В нашем случае такой подход сработает, т.к. сущность позиции заказа простая и имеет скалярные поля. Но что, если позиция заказа будет содержать вложенные сущности?
Тогда появляется необходимость хранить на родительской форме (карточке заказа) данные всех вложенных сущностей и при сохранении в базу данных гарантировать целостность записей и следить за правильным соотношением идентификаторов. В таком случае будет удобнее работать с JSON-объектом, который нужно будет собирать вручную из различных источников и передавать на сервер единым запросом.
Этому решению будет посвящен другой урок, а пока рассмотрим простое решение, которого будет вполне достаточно для текущей версии карточки заказа.
Решение 2. Паттерн Add/Edit
Суть паттерна в том, чтобы при открытии карточки сущности создавать черновую запись заказа, получать ее идентификатор и передавать его в карточку редактирования. Таким образом, карточка сущности в режиме добавления и редактирования будет работать одинаково. А при попытке сохранить изменения всегда будет вызываться запрос на обновление, в котором будем переводить запись из черновика в "добавленную".
Для обозначения черновых записей в таблице в базе данных используется колонка added типа boolean со значением false. Когда сохраняются изменения, значение в этой колонке меняется на true.
Если форма карточки сущности, открытая на добавление новой записи, будет закрыта без сохранения изменений, то черновая запись должна быть удалена вместе со всеми вложенными записями.
Паттерн Add/Edit
Подготовка
Добавим в таблицу заказов колонку added:
Всем существующим заказам в колонке added присвоим значение true,
Пока что мы не сможем создать черновую запись заказа, т.к. в таблице в базе данных стоят ограничения на клиента, номер и дату заказа, которые мы не может задать для черновой записи.
Поэтому нам необходимо с колонок client_id, order_number и order_date убрать ограничения NOT NULL
, вместо которых добавим проверку этих колонок на NULL при условии, что флаг added равен true:
Такая проверка позволит создавать черновые записи, а в настоящих заказах не позволит оставлять пустые данные о клиенте, номере и дате заказа.
Создание черновика
Для создания черновой записи заказа переделаем запрос OrderInsertSqlQuery и переименуем его в EmptyOrderInsertSqlQuery:
В запрос OrderSelectSqlQuery добавим условие O.added AND
, чтобы исключить черновые заказы из таблицы на главной форме. А в запрос OrderUpdateSqlQuery добавим обновление значения в колонке added на true.
Перейдем на стартовую форму (TemplateStart.xml).
Создадим SetDataConnection для этого запроса:
Создадим команду EmptyOrderInsertSaveCommand типа SaveCommand, в которой будем вызывать EmptyOrderInsertSetDataConnection.
Переделаем команду OrderAddFormShowCommand, добавив параметр OrderId:
На кнопку OrderAddButton перед командой OrderAddFormShowCommand добавим вызов новой команды EmptyOrderInsertSaveCommand.
Перейдем в файл карточки заказа (TemplateOrderEdit.xml), из которой удалим OrderInsertSetDataConnection и команды OrderInsertSaveCommand и OrderIdValueSetCommand.
Таким образом, команда SaveSequentialCommand примет вид:
Удаление черновика
Важный момент: если мы закроем без сохранения форму добавления нового заказа, то мы должны удалить черновую запись заказа и все связанные с ней записи позиций заказов. Это необходимо, чтобы в базе данных не скапливался "мусор".
Перейдем в серверный xml-файл и добавим запрос:
Вернемся в файл карточки заказа (TemplateOrderEdit.xml) и создадим соединение с данными EmptyOrderDeleteSetDataConnection для этого запроса и команду EmptyOrderDeleteSaveCommand. Скорректируем Execution на закрытие формы, добавив в него вызов команды на удаление. Таким образом, мы получим:
Перезапустите приложение и проверьте работу с черновиками заказов.
Итоги
В уроке мы рассмотрели решения, позволяющие грамотно сохранять записи со сложной иерархической структурой. Одно из решений - паттерн Add/Edit - реализовали в нашем приложении в карточке заказа.
Суть паттерна Add/Edit заключается в создании черновой записи перед открытием карточки сущности на добавление, получение на форме ее идентификатора и использование его для сохранения вложенных сущностей.
В следующем уроке рассмотрим выгрузку данных в текстовый документ.
Ответы
В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.
Last updated