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

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

{% hint style="success" %}
Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье [Разворачивание проекта](https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project).

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела [Ответы](https://wfsys.gitbook.io/wt-practice/lesson_making_api_requests#answer) прошлого урока. Скопируйте папки *Forms*, *Workflow* и *Patterns* в папку с развернутым проектом, например, в папку *D:\WT\Projects\Template\Projects\1. Template*.

Инструкция по подключению шаблонов находится по [ссылке](https://wfsys.gitbook.io/wt-practice/main/lesson_list_form#podklyuchenie-shablonov-k-proektu).
{% endhint %}

## Работа с JSON на форме <a href="#json_on_form" id="json_on_form"></a>

Работу с JSON будем рассматривать на примере карточки заказа, так как заказ имеет вложенные сущности и потенциально может иметь больше уровней вложенности. Ранее на примере заказа мы рассматривали [паттерн Add/Edit](https://wfsys.gitbook.io/wt-practice/main/lesson_pattern_add-edit). Теперь будет интересно увидеть, как изменится форма с использованием JSON-объекта.

### Построение объекта <a href="#building-object" id="building-object"></a>

С формы заказа на сервер должен уходить 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>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/structure) типа [Dictionary](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/structure#dictionary), которую присвоим в объект [Variable](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/variable):

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

Обратите внимание, что у тэга `<MyObject>` стоит атрибут [`ChangeForm`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects#attributes_object) со значением False.

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

Для полей с массивами объектов используем конструкцию [`<Array>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/array) с преобразованием массива строк из таблицы в массив словарей.

У тэгов `<Object>` и `<Parameter>` появился атрибут `Refresh`, который определяет, будет ли обновляться значение у тэгов `<Key>` и `<Array>`, если изменится значение источника. Таким образом, значение объекта OrderDictionaryVariable не будет пересчитываться каждый раз, когда измениться какой-либо источник. Для ручного пересчета OrderDictionaryVariable нужно использовать команду [ValueSetCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/value_set_command) для вызова set-проперти [`Refresh`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/variable#refresh) у объекта Variable:

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

Чтобы словарь преобразовать к JSON-объекту, создадим команду типа [SerializeToJsonCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/serialize_to_json_command):

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

{% hint style="info" %}
Команда SerializeToJsonCommand при формировании JSON-объекта преобразует все даты со временем к UTC относительно пользовательских настроек временной зоны.&#x20;
{% endhint %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

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

{% code title="Template.xml" %}

```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>
```

{% endcode %}

## Самостоятельно <a href="#self-work" id="self-work"></a>

#### Удаление логики черновых записей <a href="#removing-logic-of-draft" id="removing-logic-of-draft"></a>

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

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

#### Правка сохранения изменений <a href="#editing-saving-changes" id="editing-saving-changes"></a>

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

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

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

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

#### Сохранение в базу <a href="#saving-to-database" id="saving-to-database"></a>

Реализуйте функцию template.order\_save(json). Функция должна возвращать orde&#x72;*\_*&#x69;d, который необходимо отлавливать на форме TemplateOrderEdit.xml и писать в параметр OrderId.

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

## Хранение данных на форме <a href="#storing-data-on-form" id="storing-data-on-form"></a>

### Удаление записей <a href="#deleting-entries" id="deleting-entries"></a>

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

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

Добавим преобразующее загружающее соединение с данными типа [ConvertDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/convert_dc):

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

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

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

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

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

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

Скорректируйте функцию template.order\_save(json) так, чтобы в ней обрабатывалось поле deleted у объектов из order\_position и order\_payment.

### Добавление и изменение записей <a href="#adding-and-editing-entries" id="adding-and-editing-entries"></a>

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

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

#### Временный идентификатор <a href="#temporary-id" id="temporary-id"></a>

Для таких целей можно использовать объект типа [CounterVariable](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/counter_variable), который является счетчиком положительных целых чисел. При обращении счетчик автоматически увеличивает значение на один и возвращает это значение:

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

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

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

#### Добавление записи в ConvertDataConnection <a href="#adding-row-to-convert_dc" id="adding-row-to-convert_dc"></a>

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

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

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

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

Так как временный идентификатор является единственным уникальным ключом, по которому мы можем получить конкретную запись, то по нему и будем выделять строку в таблице. В команде используем конструкцию [`<Input>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/input), что позволит передавать нужное значение в момент вызова команды. Так мы сможем использовать значение переменной CounterCopyVariable, если создавали новую позицию заказа. А при редактировании позиции заказа будем использовать значение временного идентификатора редактируемой записи.

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

#### Изменение записи в ConvertDataConnection <a href="#updating-row-in-convert_dc" id="updating-row-in-convert_dc"></a>

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

{% code title="TemplateOrderEdit.xml" %}

```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>
```

{% endcode %}

Обновление данных в строке происходит по ее индексу, который мы можем получить с помощью get-проперти [`RowIndexOf`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable#get_row_index_of) по уникальному значению, которым является временный идентификатор **ID**. Значение временного идентификатора берем из выделенной строки в таблице **OrderPositionDatabaseTable**, так как карточка позиции заказа открывается в модальном режиме. В противном случае, нам пришлось бы передавать на дочернюю форму временный идентификатор и после получать его значение через параметр команды OrderPositionEditFormShowCommand.

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

{% code title="TemplateOrderEdit.xml" %}

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

{% endcode %}

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

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

### Обновление данных <a href="#data-update" id="data-update"></a>

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

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

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

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

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

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

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

```xml
<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 - эти соединения обновляем вручную.

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

## Вложенные сущности третьего уровня <a href="#third-level-nested-entities" id="third-level-nested-entities"></a>

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

```xml
<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>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/convert_dc#manual_refresh) со значением **True**. При этом источник данных (PositionItemPrimaryGetDataConnection) может не иметь вложенный тэг [`<ManualLoad>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/primary_dc#manual_load) со значением True. Следовательно, необходимо создавать команду DataConnectionRefreshCommand для его обновления.

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

```xml
<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>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/array) дополним матрицу новыми значениями для полей ID, OrderPositionID и Deleted. Все строки  в матрице будут иметь уникальные значения в колонке ID.

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

## Итоги <a href="#results" id="results"></a>

В этом уроке мы рассмотрели альтернативу [паттерну Add/Edit](https://wfsys.gitbook.io/wt-practice/main/lesson_pattern_add-edit), который строится на создании черновой записи и работе с этим черновиком в карточке сущности. Плюсом работа с JSON является возможность передавать данные сущности со сложной иерархической структурой единым запросом, что сохраняет целостность информации в базе данных. Минусом - такой подход усложняет работу с данными на формах, так как необходимо передавать их с родительской формы на дочернюю и обратно, а также следить за связанностью данных при хранении их на форме. И сам SQL-запрос на сохранение в базу данных становится громоздким и сложным.

## Ответы <a href="#answer" id="answer"></a>

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

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th data-hidden data-type="content-ref"></th></tr></thead><tbody><tr><td>lesson26-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson26-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson26-answer.zip</a></td></tr></tbody></table>
