Урок 26. Работа с JSON на форме

В прошлом уроке мы познакомились с возможностями PostgreSQL по работе с JSON-объектами, когда сохраняли заказ и клиента через API-запросы. Платформа Workflow Technology так же поддерживает работу с JSON-объектами, что позволят собирать на форме всю информацию и единым запросом передавать ее на сервер. Это избавляет нас от необходимости создавать черновую запись для работы с сущностями со сложной иерархической структурой (например, сущность заказа).

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

Работа с JSON на форме

Работу с JSON будем рассматривать на примере карточки заказа, так как заказ имеет вложенные сущности и потенциально может иметь больше уровней вложенности. Ранее на примере заказа мы рассматривали паттерн Add/Edit. Теперь будет интересно увидеть, как изменится форма с использованием JSON-объекта.

Построение объекта

С формы заказа на сервер должен уходить JSON-объект подобного вида:

{
   "order_id":null,
   "client_id":7,
   "order_number":"123",
   "order_date":"2022-03-14 10:00:00Z",
   "description":"Сохранение через JSON",
   "order_position":[
      {
         "order_position_id":null,
         "material_id":4,
         "quantity":25.00,
         "unit_price":200.00
      },
      {
         "order_position_id":null,
         "material_id":2,
         "quantity":2.00,
         "unit_price":350.00
      }
   ],
   "order_payment":[
      {
         "order_payment_id":null,
         "cash_id":null,
         "date":"2022-03-14 10:00:00Z",
         "account_id":4,
         "summ":900.00
      }
   ]
}

Так как JSON-объект представляет собой набор пар "ключ-значение", то нам необходимо создать структуру <Structure> типа Dictionary, которую присвоим в объект Variable:

TemplateOrderEdit.xml
<MyObject Name="OrderDictionaryVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Structure Type="Dictionary">
      <Key Name=""></Key>
    </Structure>
  </Value>
</MyObject>

Обратите внимание, что у тэга <MyObject> стоит атрибут ChangeForm со значением False.

Добавим необходимые ключи в структуру и укажем источники данных:

TemplateOrderEdit.xml
<MyObject Name="OrderDictionaryVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Structure Type="Dictionary">
      <Key Name="order_id">
        <DataTypeConvert Type="IntegerDataType">
          <Parameter Name="OrderId" Refresh="False" />
        </DataTypeConvert>
      </Key>
      <Key Name="client_id">
        <Object Name="ClientComboBox" Refresh="False" />
      </Key>
      <Key Name="order_date">
        <Object Name="OrderDateDateTimePicker" Refresh="False" />
      </Key>
      <Key Name="order_number">
        <Object Name="OrderNumberTextBox" Refresh="False" />
      </Key>
      <Key Name="description">
        <Object Name="DescriptionTextBox" Refresh="False" />
      </Key>
      <Key Name="order_position">
        <Array>
          <Source>
            <Object Name="OrderPositionDatabaseTable" Refresh="False">
              <Property Name="ArrayData" />
            </Object>
          </Source>
          <ToDictionary>
            <Key Name="order_position_id" Index="0" />
            <Key Name="material_id" Index="3" />
            <Key Name="quantity" Index="4" />
            <Key Name="unit_price" Index="5" />
          </ToDictionary>
        </Array>
      </Key>
      <Key Name="order_payment">
        <Array>
          <Source>
            <Object Name="OrderPaymentDatabaseTable" Refresh="False">
              <Property Name="ArrayData" />
            </Object>
          </Source>
          <ToDictionary>
            <Key Name="order_payment_id" Index="1" />
            <Key Name="cash_id" Index="2" />
            <Key Name="date" Index="3" />
            <Key Name="account_id" Index="4" />
            <Key Name="summ" Index="6" />
          </ToDictionary>
        </Array>
      </Key>
    </Structure>
  </Value>
</MyObject>

Для полей с массивами объектов используем конструкцию <Array> с преобразованием массива строк из таблицы в массив словарей.

У тэгов <Object> и <Parameter> появился атрибут Refresh, который определяет, будет ли обновляться значение у тэгов <Key> и <Array>, если изменится значение источника. Таким образом, значение объекта OrderDictionaryVariable не будет пересчитываться каждый раз, когда измениться какой-либо источник. Для ручного пересчета OrderDictionaryVariable нужно использовать команду ValueSetCommand для вызова set-проперти Refresh у объекта Variable:

TemplateOrderEdit.xml
<Command Name="OrderDictionaryVariableRefreshValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="OrderDictionaryVariable">
    <Property Name="Refresh" />
  </Object>
</Command>

Чтобы словарь преобразовать к JSON-объекту, создадим команду типа SerializeToJsonCommand:

TemplateOrderEdit.xml
<Command Name="OrderDictionaryVariableSerializeToJsonCommand" Type="SerializeToJsonCommand" Assembly="Commands">
  <Variable>
    <Object Name="OrderDictionaryVariable" />
  </Variable>
</Command>

Команда SerializeToJsonCommand при формировании JSON-объекта преобразует все даты со временем к UTC относительно пользовательских настроек временной зоны.

Добавим вызов обеих команд в SaveSequentialCommand.

TemplateOrderEdit.xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands Lock="True">
    <Command Name="OrderDictionaryVariableRefreshValueSetCommand" />
    <Command Name="OrderDictionaryVariableSerializeToJsonCommand" />
    <Command Name="OrderUpdateSaveCommand" />
    <Command Name="EditTrueValueSetCommand" />
    <Command Name="FormChangedFalseValueSetCommand" />
    <Command Name="UpdatedTrueValueSetCommand" />
  </Commands>
</Command>

Переделаем соединение с данными OrderUpdateSetDataConnection, убрав все параметры и добавив один параметр Model, в который будет передаваться результат команды сериализации объекта в JSON. Переименуем OrderUpdateSetDataConnection в OrderSaveSetDataConnection:

TemplateOrderEdit.xml
<DataConnection Name="OrderSaveSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <SqlQueries>
    <SqlQuery Name="OrderSaveSqlQuery" Type="Update" />
  </SqlQueries>
  <Parameters>
    <Parameter NativeName="Model">
      <Value>
        <Command Name="OrderDictionaryVariableSerializeToJsonCommand" />
      </Value>
    </Parameter>
  </Parameters>
</DataConnection>

Также переименуем команду OrderUpdateSaveCommand в OrderSaveCommand.

Перейдем в серверный xml-файл (Template.xml). Скорректируем текст запроса OrderUpdateSqlQuery и переименуем его в OrderSaveSqlQuery:

Template.xml
<SqlQuery Name="OrderSaveSqlQuery">
  <Events>
    <Event Name="ChangedNumberOfOrders">
      <Parameters>
        <Parameter Name="NumberOfOrders">
          SELECT
            count(*)
          FROM
            template.order O
          WHERE
            O.added AND
            NOT O.deleted;
        </Parameter>
      </Parameters>
    </Event>
  </Events>
  <Text>
    SELECT template.order_save({Model}::json);
  </Text>
</SqlQuery>

Самостоятельно

Удаление логики черновых записей

Удалите всю логику, связанную с черновыми записями заказов на главной форме и в карточке заказа.

Удалите колонку added из таблицы template.order и всех запросов, которые ее проверяют или обновляют.

Правка сохранения изменений

Удалите запросы OrderPositionInsertSqlQuery и OrderPositionUpdateSqlQuery и связанные с ними команды. В карточке позиции заказа создайте параметры формы MaterialId, Quantity и UnitPrice. Через эти параметры будем передавать значения между родительской формой и карточкой позиции. Пока можете не реализовывать добавление значений в таблицу на родительской форме - в следующем разделе мы сделаем это вместе.

Удалите запрос OrderPositionByIdSelectSqlQuery и OrderPositionPrimaryGetDataConnection. Объекты MaterialComboBox, QuantityNumericBox и UnitPriceNumericBox заполняйте значениями из параметров формы.

Удаление записей также рассмотрим дальше в уроке.

Аналогично сделайте для оплат в заказе.

Сохранение в базу

Реализуйте функцию template.order_save(json). Функция должна возвращать order_id, который необходимо отлавливать на форме TemplateOrderEdit.xml и писать в параметр OrderId.

Не забывайте про работу с датой со временем и временные зоны. В JSON-строке на сервер даты со временем придут в UTC. Для приведения времени во временную зону сервера используйте функцию public.convert_date_json(timestamp without time zone).

Хранение данных на форме

Удаление записей

Вернемся в файл TemplateOrderEdit.xml и на примере позиций заказа рассмотрим, как реализовать удаление через JSON-объект.

Удалим запрос OrderPositionDeleteSqlQuery и связанные с ним соединение с данными OrderPositionDeleteDataConnection и команду.

Добавим преобразующее загружающее соединение с данными типа ConvertDataConnection:

TemplateOrderEdit.xml
<DataConnection Name="OrderPositionConvertDataConnection" Type="ConvertDataConnection" Assembly="WorkflowServer">
  <SourceDataConnection Name="OrderPositionPrimaryGetDataConnection" />
  <Fields>
    <Field Name="OrderPositionId" />
    <Field Name="MaterialId" />
    <Field Name="Quantity" />
    <Field Name="UnitPrice" />
    <Field Name="Deleted" Type="Value">False</Field>
  </Fields>
</DataConnection>

С его помощью мы дополняем исходный набор данных из соединения OrderPositionPrimaryGetDataConnection колонкой Deleted со значением False по умолчанию. С OrderPositionConvertDataConnection мы можем работать так же, как с любым другим загружающим DataConnection. Например, мы можем использовать его в качестве источника данных для объектов или другого DataConnection.

Создадим SecondaryGetDataConnection, чтобы отфильтровать записи по колонке Deleted:

TemplateOrderEdit.xml
<DataConnection Name="OrderPositionSecondaryGetDataConnection" Type="SecondaryGetDataConnection" Assembly="DataConnections">
  <SourceDataConnection Name="OrderPositionConvertDataConnection" />
  <Filter>
    <Field NativeName="Deleted" />
    <Value>False</Value>
    <DataType Type="BooleanDataType" />
  </Filter>
</DataConnection>

И укажем OrderPositionSecondaryGetDataConnection в качестве источника данных для таблицы OrderPositionDatabaseTable. Таким образом, в соединении с данными OrderPositionConvertDataConnection будут храниться все записи, полученные из базы данных, а в таблице будут отображаться только неудаленные.

Теперь займемся удалением записей из OrderPositionConvertDataConnection. Важно то, что в нем могут быть записи из таблицы в базе данных (у таких записей будет значение в поле OrderPositionId) и новые записи, добавленные на форме. Для первых мы должны обновлять значение в поле Deleted, а для вторых будем использовать set-проперти DeleteRowsByIndices.

Первым делом создадим условие IsNullCondition для проверки OrderPositionId:

TemplateOrderEdit.xml
<Condition Name="OrderPositionIdIsNullCondition" Type="IsNullCondition" Assembly="Conditions">
  <Items>
    <Item>
      <Object Name="OrderPositionDatabaseTable">
        <Property Name="SelectedRowCellValueByColumnName">
          <Parameters>
            <Parameter Name="ColumnName">OrderPositionId</Parameter>
          </Parameters>
        </Property>
      </Object>
    </Item>
  </Items>
</Condition>

Если запись не имеет идентификатора, то мы будем удалять ее из OrderPositionConvertDataConnection:

TemplateOrderEdit.xml
<Command Name="OrderPositionDeleteRowCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="OrderPositionConvertDataConnection">
    <Property Name="DeleteRowsByIndices">
      <DataConnection SourceDataConnection="OrderPositionConvertDataConnection">
        <Property Name="RowIndexOf">
          <Parameters>
            <Parameter Name="ColumnNames">
              <Structure Type="List">
                <Item>ID</Item>
              </Structure>
            </Parameter>
            <Parameter Name="Values">
              <Structure Type="List">
                <Item>
                  <Object Name="OrderPositionDatabaseTable">
                    <Property Name="SelectedRowCellValueByColumnName">
                      <Parameters>
                        <Parameter Name="ColumnName">ID</Parameter>
                      </Parameters>
                    </Property>
                  </Object>
                </Item>
              </Structure>
            </Parameter>
          </Parameters>
        </Property>
      </DataConnection>
    </Property>
  </DataConnection>
</Command>

Если запись имеет идентификатор, то мы меняем значение в колонке Deleted:

TemplateOrderEdit.xml
<Command Name="OrderPositionDeleteUpdateRowValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="OrderPositionConvertDataConnection">
    <Property Name="UpdateRow">
      <Parameters>
        <Parameter Name="RowIndex">
          <DataConnection SourceDataConnection="OrderPositionConvertDataConnection">
            <Property Name="RowIndexOf">
              <Parameters>
                <Parameter Name="ColumnNames">
                  <Structure Type="List">
                    <Item>ID</Item>
                  </Structure>
                </Parameter>
                <Parameter Name="Values">
                  <Structure Type="List">
                    <Item>
                      <Object Name="OrderPositionDatabaseTable">
                        <Property Name="SelectedRowCellValueByColumnName">
                          <Parameters>
                            <Parameter Name="ColumnName">ID</Parameter>
                          </Parameters>
                        </Property>
                      </Object>
                    </Item>
                  </Structure>
                </Parameter>
              </Parameters>
            </Property>
          </DataConnection>
        </Parameter>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>Deleted</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Structure Type="List">
            <Item>True</Item>
          </Structure>
        </Parameter>
      </Parameters>
    </Property>
  </DataConnection>
</Command>

Скорректируем Execution на удаление позиции заказа:

TemplateOrderEdit.xml
<Execution>
  <ConditionExpression>
    <Command Name="OrderPositionDeleteMessageBoxCommand" Parameter="Yes" />
  </ConditionExpression>
  <Commands>
    <If>
      <When>
        <Condition Name="OrderPositionIdIsNullCondition" />
      </When>
      <Then>
        <Command Name="OrderPositionDeleteRowCommand" />
      </Then>
      <Else>
        <Command Name="OrderPositionDeleteUpdateRowValueSetCommand" />
      </Else>
    </If>
    <Command Name="UpdatedTrueValueSetCommand" />
  </Commands>
</Execution>

Скорректируем OrderDictionaryVariable, заменив источник значения для ключа order_position и добавив колонку Deleted:

TemplateOrderEdit.xml
<MyObject Name="OrderDictionaryVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value>
    <Structure Type="Dictionary">
      <Key Name="order_id">
        <DataTypeConvert Type="IntegerDataType">
          <Parameter Name="OrderId" Refresh="False" />
        </DataTypeConvert>
      </Key>
      <Key Name="client_id">
        <Object Name="ClientComboBox" Refresh="False" />
      </Key>
      <Key Name="order_date">
        <Object Name="OrderDateDateTimePicker" Refresh="False" />
      </Key>
      <Key Name="order_number">
        <Object Name="OrderNumberTextBox" Refresh="False" />
      </Key>
      <Key Name="description">
        <Object Name="DescriptionTextBox" Refresh="False" />
      </Key>
      <Key Name="order_position">
        <Array>
          <Source>
            <DataConnection SourceDataConnection="OrderPositionConvertDataConnection" Refresh="False">
              <Fields>
                <Field Name="OrderPositionId" />
                <Field Name="MaterialId" />
                <Field Name="Quantity" />
                <Field Name="UnitPrice" />
                <Field Name="Deleted" />
              </Fields>
            </DataConnection>
          </Source>
          <ToDictionary>
            <Key Name="order_position_id" Index="0" />
            <Key Name="material_id" Index="1" />
            <Key Name="quantity" Index="2" />
            <Key Name="unit_price" Index="3" />
            <Key Name="deleted" Index="4" />
          </ToDictionary>
        </Array>
      </Key>
      <Key Name="order_payment">
        <Array>
          <Source>
            <Object Name="OrderPaymentDatabaseTable" Refresh="False">
              <Property Name="ArrayData" />
            </Object>
          </Source>
          <ToDictionary>
            <Key Name="order_payment_id" Index="1" />
            <Key Name="cash_id" Index="2" />
            <Key Name="date" Index="3" />
            <Key Name="account_id" Index="4" />
            <Key Name="summ" Index="6" />
          </ToDictionary>
        </Array>
      </Key>
    </Structure>
  </Value>
</MyObject>

Аналогичным образом измените логику работы с оплатами в заказе. Для хранения данных об оплатах в карточке заказа создайте преобразующее загружающее соединение с данными OrderPaymentConvertDataConnection.

Скорректируйте функцию template.order_save(json) так, чтобы в ней обрабатывалось поле deleted у объектов из order_position и order_payment.

Добавление и изменение записей

Добавление и изменение записей будем рассматривать с заделом на несколько уровней вложенности. Отдельную сложность представляют сущности третьего и более уровней вложенности.

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

Временный идентификатор

Для таких целей можно использовать объект типа CounterVariable, который является счетчиком положительных целых чисел. При обращении счетчик автоматически увеличивает значение на один и возвращает это значение:

TemplateOrderEdit.xml
<MyObject Name="CounterVariable" Type="CounterVariable" Assembly="WorkflowServer" ChangeForm="False">
  <Value />
</MyObject>

Переменная CounterCopyVariable необходима для сохранения текущего значения счетчика:

TemplateOrderEdit.xml
<MyObject Name="CounterCopyVariable" Type="Variable" Assembly="SimpleControls" ChangeForm="False">
  <Value />
</MyObject>

В OrderPositionConvertDataConnection и OrderPaymentConvertDataConnection добавляем поле ID, которое будет хранить временный идентификатор, и укажем в качестве значения объект CounterVariable:

TemplateOrderEdit.xml
<DataConnection Name="OrderPositionConvertDataConnection" Type="ConvertDataConnection" Assembly="WorkflowServer">
  <SourceDataConnection Name="OrderPositionPrimaryGetDataConnection" />
  <Fields>
    <Field Name="OrderPositionId" />
    <Field Name="MaterialId" />
    <Field Name="Quantity" />
    <Field Name="UnitPrice" />
    <Field Name="Deleted" Type="Value">False</Field>
    <Field Name="ID" Type="Value">
      <Object Name="CounterVariable" />
    </Field>
  </Fields>
</DataConnection>

<DataConnection Name="OrderPaymentConvertDataConnection" Type="ConvertDataConnection" Assembly="WorkflowServer">
  <SourceDataConnection Name="OrderPaymentPrimaryGetDataConnection" />
  <Fields>
    <Field Name="OrderPaymentId" />
    <Field Name="CashId" />
    <Field Name="Date" />
    <Field Name="AccountId" />
    <Field Name="Summ" />
    <Field Name="Deleted" Type="Value">False</Field>
    <Field Name="ID" Type="Value">
      <Object Name="CounterVariable" />
    </Field>
  </Fields>
</DataConnection>

После загрузки формы и получении данных в PrimaryGetDataConnection оба ConvertDataConnection построят свои внутренние таблицы, и в каждой строчке в поле ID будут проставлены уникальные значения.

В таблицы OrderPositionDatabaseTable и OrderPaymentDatabaseTable добавим колонки ID.

Создадим команды ValueSetCommand для добавления и изменения записей в OrderPositionConvertDataConnection. Для этого будем использовать set-проперти AddRow и UpdateRow у DataConnection.

Добавление записи в ConvertDataConnection

В команде на добавление записей будем обращаться к CounterCopyVariable для заполнения колонки ID уникальным временным идентификатором:

TemplateOrderEdit.xml
<Command Name="OrderPositionAddRowValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="OrderPositionConvertDataConnection">
    <Property Name="AddRow">
      <Parameters>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>ID</Item>
            <Item>MaterialId</Item>
            <Item>Quantity</Item>
            <Item>UnitPrice</Item>
            <Item>Deleted</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Structure Type="List">
            <Item>
              <Object Name="CounterCopyVariable" />
            </Item>
            <Item>
              <Command Name="OrderPositionAddFormShowCommand" Parameter="MaterialId" />
            </Item>
            <Item>
              <Command Name="OrderPositionAddFormShowCommand" Parameter="Quantity" />
            </Item>
            <Item>
              <Command Name="OrderPositionAddFormShowCommand" Parameter="UnitPrice" />
            </Item>
            <Item>False</Item>
          </Structure>
        </Parameter>
      </Parameters>
    </Property>
  </DataConnection>
</Command>

Для новой строки в колонке Deleted сразу прописываем значение False.

Добавим команду, с помощью которой будем сохранять текущее значение переменной-счетчика:

TemplateOrderEdit.xml
<Command Name="CounterCopyVariableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="CounterCopyVariable">
    <Object Name="CounterVariable" />
  </Object>
</Command>

Скорректируем команду для выделения новой строки в таблице:

TemplateOrderEdit.xml
<Command Name="OrderPositionSelectInTableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="OrderPositionDatabaseTable">
    <Property Name="SelectRowByFieldValue">
      <Parameters>
        <Parameter Name="ColumnName">ID</Parameter>
        <Parameter Name="Value">
          <Input Name="ID" />
        </Parameter>
      </Parameters>
    </Property>
  </Object>
</Command>

Так как временный идентификатор является единственным уникальным ключом, по которому мы можем получить конкретную запись, то по нему и будем выделять строку в таблице. В команде используем конструкцию <Input>, что позволит передавать нужное значение в момент вызова команды. Так мы сможем использовать значение переменной CounterCopyVariable, если создавали новую позицию заказа. А при редактировании позиции заказа будем использовать значение временного идентификатора редактируемой записи.

Скорректируем Execution, который отрабатывает результат выполнения команды OrderPositionAddFormShowCommand:

TemplateOrderEdit.xml
<Execution>
  <ConditionExpression>
    <Command Name="OrderPositionAddFormShowCommand" Parameter="Updated" />
  </ConditionExpression>
  <Commands>
    <Command Name="CounterCopyVariableValueSetCommand" />
    <Command Name="OrderPositionAddRowValueSetCommand" />
    <Command Name="OrderPositionSelectInTableValueSetCommand">
      <Input Name="ID">
        <Object Name="CounterCopyVariable" />
      </Input>
    </Command>
    <Command Name="UpdatedTrueValueSetCommand" />
  </Commands>
</Execution>

Изменение записи в ConvertDataConnection

Создадим команду обновления строки:

TemplateOrderEdit.xml
<Command Name="OrderPositionUpdateRowValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="OrderPositionConvertDataConnection">
    <Property Name="UpdateRow">
      <Parameters>
        <Parameter Name="RowIndex">
          <DataConnection SourceDataConnection="OrderPositionConvertDataConnection">
            <Property Name="RowIndexOf">
              <Parameters>
                <Parameter Name="ColumnNames">
                  <Structure Type="List">
                    <Item>ID</Item>
                  </Structure>
                </Parameter>
                <Parameter Name="Values">
                  <Structure Type="List">
                    <Item>
                      <Object Name="OrderPositionDatabaseTable">
                        <Property Name="SelectedRowCellValueByColumnName">
                          <Parameters>
                            <Parameter Name="ColumnName">ID</Parameter>
                          </Parameters>
                        </Property>
                      </Object>
                    </Item>
                  </Structure>
                </Parameter>
              </Parameters>
            </Property>
          </DataConnection>
        </Parameter>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>MaterialId</Item>
            <Item>Quantity</Item>
            <Item>UnitPrice</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Structure Type="List">
            <Item>
              <Command Name="OrderPositionEditFormShowCommand" Parameter="MaterialId" />
            </Item>
            <Item>
              <Command Name="OrderPositionEditFormShowCommand" Parameter="Quantity" />
            </Item>
            <Item>
              <Command Name="OrderPositionEditFormShowCommand" Parameter="UnitPrice" />
            </Item>
          </Structure>
        </Parameter>
      </Parameters>
    </Property>
  </DataConnection>
</Command>

Обновление данных в строке происходит по ее индексу, который мы можем получить с помощью get-проперти RowIndexOf по уникальному значению, которым является временный идентификатор ID. Значение временного идентификатора берем из выделенной строки в таблице OrderPositionDatabaseTable, так как карточка позиции заказа открывается в модальном режиме. В противном случае, нам пришлось бы передавать на дочернюю форму временный идентификатор и после получать его значение через параметр команды OrderPositionEditFormShowCommand.

Скорректируем Execution, который отрабатывает результат выполнения команды OrderPositionEditFormShowCommand:

TemplateOrderEdit.xml
<Execution>
  <ConditionExpression>
    <Command Name="OrderPositionEditFormShowCommand" Parameter="Updated" />
  </ConditionExpression>
  <Commands>
    <Command Name="OrderPositionUpdateRowValueSetCommand" />
    <Command Name="UpdatedTrueValueSetCommand" />
  </Commands>
</Execution>

Здесь так же можно было добавить команду выделения редактируемой строки, если бы команда OrderPositionEditFormShowCommand открывала окно в немодальном режиме.

Аналогичным образом измените логику добавления и редактирования оплат в заказе.

Обновление данных

После внесенных изменение команды OrderPositionDataConnectionRefreshCommand и OrderPaymentDataConnectionRefreshCommand не используются - их можно удалить. Но обновлять соединения с данными для позиций заказа и оплат необходимо, чтобы подтягивать из базы данных на форму идентификаторы новых записей. Так как после сохранения форма остается открытой, и пользователь может редактировать новые записи, то важно иметь оригинальные идентификаторы, чтобы новые изменения правильно сохранялись в базу данных.

Создадим новую команду типа DataConnectionRefreshCommand:

<Command Name="AllDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="OrderPositionPrimaryGetDataConnection" />
    <DataConnection Name="OrderPaymentPrimaryGetDataConnection" />
  </DataConnections>
</Command>

Так же необходимо создать команду, сохраняющую идентификатор нового заказа в параметр формы:

<Command Name="OrderIdValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Parameter Name="OrderId">
    <Command Name="OrderSaveCommand" />
  </Parameter>
</Command>

Раньше в параметре OrderId всегда было значение, так как при открытии формы на создание заказа, на родительской форме создавался черновик заказа, идентификатор которого передавали в параметр. Теперь мы удалили логику черновиков, и параметр OrderId будет иметь пустое значение при создании заказа. А так как после сохранения изменений форма остается открытой, то повторное сохранение изменений будет приводить к дублированию записей в базе данных. Чтобы этого не происходило, мы и будем сохранять идентификатор нового заказа.

Добавим новые команды в команду SaveSequentialCommand:

<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands Lock="True">
    <Command Name="OrderDictionaryVariableRefreshValueSetCommand" />
    <Command Name="OrderDictionaryVariableSerializeToJsonCommand" />
    <Command Name="OrderSaveCommand" />
    <Command Name="OrderIdValueSetCommand" />
    <Command Name="AllDataConnectionRefreshCommand" />
    <Command Name="UpdatedTrueValueSetCommand" />
    <Command Name="EditTrueValueSetCommand" />
    <Command Name="FormChangedFalseValueSetCommand" />
  </Commands>
</Command>

В соединение с данными OrderPrimaryGetDataConnection добавим атрибут RefreshQuery="False" на параметр OrderId, чтобы DataConnection не обновлялся автоматически при изменении параметра формы OrderId. Такие же изменения внесем и в OrderPositionPrimaryGetDataConnection и OrderPaymentPrimaryGetDataConnection - эти соединения обновляем вручную.

Запустите приложение и проверьте работу формы заказа.

Вложенные сущности третьего уровня

Если у позиции заказа будут вложенные сущности, то для их хранения будет использоваться отдельный ConvertDataConnection, например:

<DataConnection Name="PositionItemConvertDataConnection" Type="ConvertDataConnection" Assembly="WorkflowServer">
  <ManualRefresh>True</ManualRefresh>
  <SourceDataConnection Name="PositionItemPrimaryGetDataConnection" />
  <Fields>
    <Field Name="PositionItemId" />
    <Field Name="OrderPositionId" />
    <Field Name="Field1" />
    <Field Name="ID" Type="Value" DataType="IntegerDataType">
      <Object Name="CounterVariable" />
    </Field>
    <Field Name="OrderPositionID" Type="Substitution" Field="OrderPositionId">
      <DataConnection SourceDataConnection="OrderPositionConvertDataConnection">
        <Fields>
          <Field Name="OrderPositionId" />
          <Field Name="ID" />
        </Fields>
      </DataConnection>
    </Field>
    <Field Name="Deleted" Type="Value" DataType="BooleanDataType">False</Field>
  </Fields>
</DataConnection>

В поле OrderPositionID типа Substitution будет подставляться значение из поля ID в OrderPositionConvertDataConnection на основе полей OrderPositionId в обоих соединениях.

Обратите внимание, что соединение с данными PositionItemConvertDataConnection имеет вложенный тэг <ManualRefresh> со значением True. При этом источник данных (PositionItemPrimaryGetDataConnection) может не иметь вложенный тэг <ManualLoad> со значением True. Следовательно, необходимо создавать команду DataConnectionRefreshCommand для его обновления.

Команда добавление записей в PositionItemConvertDataConnection может иметь вид:

<Command Name="PositionItemAddCommand" Type="ValueSetCommand" Assembly="Commands">
  <DataConnection Name="PositionItemConvertDataConnection ">
    <Property Name="AddRows">
      <Parameters>
        <Parameter Name="ColumnNames">
          <Structure Type="List">
            <Item>ID</Item>
            <Item>OrderPositionID</Item>
            <Item>Field1</Item>
            <Item>Deleted</Item>
          </Structure>
        </Parameter>
        <Parameter Name="Values">
          <Array>
            <Source>
              <Object Name="PositionItemArrayToAddVariable" />
            </Source>
            <Select>
              <Items>
                <Item Type="Value">
                  <Object Name="CounterVariable" />
                </Item>
                <Item Type="Value">
                  <Input Name="OrderPositionID" />
                </Item>
                <Item Type="Index">0</Item>
                <Item Type="Value">False</Item>
              </Items>
            </Select>
          </Array>
        </Parameter>
      </Parameters>
    </Property>
  </DataConnection>
</Command>

Объект PositionItemArrayToAddVariable будет содержать массив объектов (точнее, матрицу значений), которые описывают вложенные сущности позиции заказа. Такой массив будет возвращаться с дочерней формы (карточки позиции заказа). С помощью конструкции <Array> дополним матрицу новыми значениями для полей ID, OrderPositionID и Deleted. Все строки в матрице будут иметь уникальные значения в колонке ID.

В момент вызова функции в качестве значения <Input Name="OrderPositionID"> будем передавать либо значение CounterCopyVariable, которое сохранили перед добавлением новой позиции заказа, либо значение параметра OrderPositionID из команды OrderPositionEditFormShowCommand.

Итоги

В этом уроке мы рассмотрели альтернативу паттерну Add/Edit, который строится на создании черновой записи и работе с этим черновиком в карточке сущности. Плюсом работа с JSON является возможность передавать данные сущности со сложной иерархической структурой единым запросом, что сохраняет целостность информации в базе данных. Минусом - такой подход усложняет работу с данными на формах, так как необходимо передавать их с родительской формы на дочернюю и обратно, а также следить за связанностью данных при хранении их на форме. И сам SQL-запрос на сохранение в базу данных становится громоздким и сложным.

Ответы

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

Last updated