# Урок 3. Выпадающий список

Продолжим совершенствовать наш проект. Создадим форму для списка клиентов и карточку клиента. И заменим стартовую форму проекта. Теперь это будет форма списка клиентов.

На этом уроке:

* познакомимся со сниппетами, которые ускоряют разработку форм;
* рассмотрим применение [SetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/set_dc);
* рассмотрим возможность передавать значения параметров в момент вызова команды.

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

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

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

## Форма списка клиентов

Форма списка клиентов должна содержать таблицу и три кнопки редактирования списка. Сохранение данных в таблицу в базе данных будет происходить на форме карточки клиента. Поэтому на форме списка кнопка "Сохранить" и FootPanel будут отсутствовать.

### Создание формы

Для создания формы воспользуйтесь паттерном **Empty Form** (пустая форма с хэдером).

<figure><img src="/files/jtB3nyCNebafw9htoRaV" alt=""><figcaption></figcaption></figure>

После того как создали файл формы, необходимо подменить стартовую форму. В конфигурационном файле клиентской части (WorkflowForms.dll.config) в параметре **StartFormFileName** указан путь до стартовой формы. Его оставим без изменений. А переименуем сами xml-файлы форм. Для этого перейдем в папку форм проекта Template\Projects\1. Template\Forms\ и переименуем файлы:

* файл TemplateStart.xml переименуем в TemplateCityList.xml, т.к. это форма списка городов;
* а только что созданный TemplateClientList.xml переименуем в TemplateStart.xml, таким образом будущий список клиентов станет стартовой формой.

После этого перейдем в Eclipse и обновим состояние нашего проекта в окне Project Explorer, чтобы редактор увидел изменения в именах файлов. Для этого выберем наш проект и нажмем клавишу **F5**, либо кликнем правой кнопкой мыши на имени проекта и в открывшемся меню выберем пункт **Refresh**. Затем перейдем в файл формы списка городов и в тэге `<Form>` исправим значение атрибута **Name** на **TemplateCityListForm**. Это необходимо, чтобы в журнале событий ошибки имели правильное и уникальное имя формы, и мы могли быстро найти нужный файл.

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

Самостоятельно создайте необходимые элементы формы:&#x20;

* таблицу клиентов с колонками ClientId, № (RowNumber), ФИО (Name), Город (CityTitle) и Телефон (Phone);
* три кнопки редактирования списка. Не забудьте про условие активности кнопок, если выделена строка таблицы.

Пока нужен только интерфейс.

В результате у вас должна получиться похожая форма:

<figure><img src="/files/SUtnWy2SewTyDE570yS7" alt=""><figcaption></figcaption></figure>

### Загрузка данных из базы данных на форму

#### Создание таблицы client в базе данных

Перейдем в программу для управления СУБД PostgreSQL. И для нашей базы данных **template\_project** выполним следующий скрипт:

```sql
  CREATE SEQUENCE template.client_id_seq;
  
  CREATE TABLE template.client
  (
    client_id bigint NOT NULL DEFAULT nextval('template.client_id_seq'::regclass),
    city_id smallint,
    date_of_birth date,
    title character varying,
    email character varying,
    phone character varying,
    CONSTRAINT pk_client_id PRIMARY KEY (client_id),
    CONSTRAINT fk_city_id FOREIGN KEY (city_id)
      REFERENCES template.city (city_id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
  );
```

{% hint style="info" %}
Обратите внимание, что для колонки **client\_id** используем тип **bigint**, а для **city\_id** используем тип **smallint**. Это связано с тем, что таблица **template.client** потенциально может иметь количество записей, которое превысит максимальное значение типа smallint, равное 32767. В то время, как для таблицы **template.city** тип **bigint** будет избыточным.
{% endhint %}

#### Описание запроса для чтения таблицы client базы данных

Перейдем в файл описания работы серверной части приложения (Template.xml). Добавим запрос для получения списка клиентов:

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

```markup
<SqlQuery Name="ClientSelectSqlQuery">
  <Text>
    SELECT
      client.client_id AS "ClientId",
      client.title AS "Name",
      city.title AS "CityTitle",
      client.phone AS "Phone"
    FROM
      template.client
      LEFT JOIN template.city USING(city_id)
    ORDER BY client.title;
  </Text>
</SqlQuery>
```

{% endcode %}

Создайте ClientViewSqlQueryPermission с этим запросом и дайте права доступа группе GuestGroup.

На форме списка клиентов (TemplateStart.xml) создайте [PrimaryGetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/primary_dc) на этот запрос и укажите его в тэге `<SourceDataConnection>`для таблицы **ClientDatabaseTable**.

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

Отлично! Теперь можем заняться формой для карточки клиента.

## Карточка клиента

Карточка клиента должна содержать поля: ФИО, Дата рождения, Город, Контактный телефон и E-mail. Поле "Город" будет представлено в виде выпадающего списка. Справа от поля "Город" будет кнопка, по которой будет открываться существующая форма со списком городов.

Скачайте новый архив с шаблонами по ссылке ниже. Замените содержимое папки с шаблонами, на которую ссылаются свойства проекта, на содержимое из архива.

{% file src="/files/8dXDgabBxJIuTsJhW9oX" %}
Шаблоны форм
{% endfile %}

С помощью шаблона **Entity Form** создайте форму для карточки клиента со следующими параметрами:

<figure><img src="/files/J5Zvb9U0LhNHvnKVf58K" alt=""><figcaption></figcaption></figure>

### Одна команда на открытие формы

Вернемся в файл стартовой формы (TemplateStart.xml). Создадим команду FormShowCommand  для формы карточки клиента, в которой сразу укажем два параметра:

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

```markup
<Command Name="ClientEditFormShowCommand" Type="FormShowCommand" Assembly="Commands">
  <Xml Type="Path">TemplateClientEdit.xml</Xml>
  <Show Type="None" />
  <Parameters>
    <Parameter Name="Edit"> </Parameter>
    <Parameter Name="ClientId"> </Parameter>
  </Parameters>
</Command>
```

{% endcode %}

Форма списка клиентов в таблице содержит колонки не для всех полей, которые описаны в задании на карточку клиента. Нет смысла перегружать sql-запрос для списка клиентов лишними полями,  которые понадобятся на другой форме. В карточке клиента будем использовать дополнительный sql-запрос, в котором для конкретного клиента будем получать все необходимые данные. Для этого нам и понадобится параметр **ClientId**. В файле карточки клиента (TemplateClientEdit.xml) эти параметры уже есть.

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

Для параметра **Edit** в качестве значения укажем `<Input>` со значением по умолчанию **False**. А для параметра **ClientId** укажем `<Input>` без значения.

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

```markup
<Command Name="ClientEditFormShowCommand" Type="FormShowCommand" Assembly="Commands">
  <Xml Type="Path">TemplateClientEdit.xml</Xml>
  <Show Type="None" />
  <Parameters>
    <Parameter Name="Edit">
      <Input Name="Edit">False</Input>
    </Parameter>
    <Parameter Name="ClientId">
      <Input Name="ClientId" />
    </Parameter>
  </Parameters>
</Command>
```

{% endcode %}

Для открытия формы в режиме добавления записи нам не нужно передавать **ClientId**. А для параметра **Edit** мы уже указали значение по умолчанию. Таким образом, на кнопке **ClientAddButton** в тэге `<Commands>` пропишем простой вызов команды по имени:

```markup
<Commands>
  <Command Name="ClientEditFormShowCommand" />
</Commands>
```

Для открытия формы в режиме редактирования в команду **ClientEditFormShowCommand** через Input будем передавать нужные значения параметров. И так как открыть форму карточки клиента можно либо по кнопке "Редактировать запись...", либо по двойному клику по строке в таблице, мы создадим [SequentialCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/sequential_command), в которой будем вызывать **ClientEditFormShowCommand**:

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

```markup
<Command Name="ClientEditSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <Command Name="ClientEditFormShowCommand">
      <Input Name="Edit">True</Input>
      <Input Name="ClientId">
        <Object Name="ClientDatabaseTable">
          <Property Name="SelectedRowCellValueByColumnName">
            <Parameters>
              <Parameter Name="ColumnName">ClientId</Parameter>
            </Parameters>
          </Property>
        </Object>
      </Input>
    </Command>
  </Commands>
</Command>
```

{% endcode %}

Укажем команду **ClientEditSequentialCommand** в тэге `<Commands>` кнопки "Редактировать запись...".

Добавьте самостоятельно обработку двойного клика по строке таблицы и вызов команды **ClientEditSequentialCommand**.

Также самостоятельно создайте команду типа [DataConnectionRefreshCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/dc_refresh_command) на **ClientPrimaryGetDataConnection**, которая будет выполняться в Execution по изменению параметра **Updated** у команды **ClientEditFormShowCommand**.

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

Отлично! Теперь можем приступить к наполнению формы карточки клиента объектами. Но для начала реализуем запрос и загружающее соединение с данными для получения информации по клиенту на форме.

### Запросы к базе данных

Перейдем в файл описания работы серверной части приложения (Template.xml). Добавим запрос для получения данных по конкретному клиенту:

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

```markup
<SqlQuery Name="ClientByIdSelectSqlQuery">
  <Text>
    SELECT
      client.title AS "Name",
      client.city_id AS "CityId",
      client.date_of_birth AS "DateOfBirth",
      client.email AS "Email",
      client.phone AS "Phone"
    FROM
      template.client
    WHERE
      client.client_id = {ClientId};
  </Text>
</SqlQuery>
```

{% endcode %}

Добавим его в ранее созданный **ClientViewSqlQueryPermission**.

Давайте сразу сделаем запросы на сохранение изменений в таблицу **template.client**:

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

```markup
<SqlQuery Name="ClientInsertSqlQuery">
  <Text>
    INSERT INTO template.client(
      city_id,
      date_of_birth,
      title,
      email,
      phone
    )
    VALUES (
      {CityId},
      {DateOfBirth},
      {Name},
      {Email},
      {Phone}
    )
    RETURNING client_id;
  </Text>
</SqlQuery>
    
<SqlQuery Name="ClientUpdateSqlQuery">
  <Text>
    UPDATE template.client
    SET
      city_id = {CityId},
      date_of_birth = {DateOfBirth},
      title = {Name},
      email = {Email},
      phone = {Phone}
    WHERE
      client_id = {ClientId};
  </Text>
</SqlQuery>
    
<SqlQuery Name="ClientDeleteSqlQuery">
  <Text>
    DELETE FROM template.client
    WHERE
      client_id = {ClientId};
  </Text>
</SqlQuery>
```

{% endcode %}

Запрос **ClientInsertSqlQuery** возвращает идентификатор **client\_id** новой записи. Этот идентификатор будем использовать на формах, как результат выполнения команды [SaveCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/save_command). Но об этом позже, а пока настроим права доступа к новым запросам.

Соберем новые запросы в **ClientEditSqlQueryPermission**:

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

```markup
<Permission Name="ClientEditSqlQueryPermission" Type="SqlQueryPermission">
  <SqlQueries>
    <SqlQuery Name="ClientInsertSqlQuery" />
    <SqlQuery Name="ClientUpdateSqlQuery" />
    <SqlQuery Name="ClientDeleteSqlQuery" />
  </SqlQueries>
</Permission>
```

{% endcode %}

И сделаем **ClientEditRole:**

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

```markup
<Role Name="ClientEditRole">
  <Permissions>
    <Permission Name="ClientViewSqlQueryPermission" />
    <Permission Name="ClientEditSqlQueryPermission" />
  </Permissions>
</Role>
```

{% endcode %}

Скорректируем права доступа для **GuestGroup**:

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

```markup
<Group Name="GuestGroup">
  <Roles>
    <Role Name="CityEditRole" />
    <Role Name="ClientEditRole" />
  </Roles>
</Group>
```

{% endcode %}

### Создание объектов формы

Перейдем в файл карточки клиента (TemplateClientEdit.xml) и первым делом создадим [PrimaryGetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/primary_dc):

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

```markup
<DataConnection Name="ClientPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="ClientByIdSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="Name" />
      <Field Name="CityId" />
      <Field Name="DateOfBirth" />
      <Field Name="Email" />
      <Field Name="Phone" />
    </Fields>
    <Parameters>
      <Parameter NativeName="ClientId">
        <Value>
          <Parameter Name="ClientId" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>
```

{% endcode %}

Создавать объекты на форме можно по одному: отдельно Label, отдельно связанный с ним TextBox. А можно воспользоваться сниппетом, который вставит пару объектов с корректной привязкой координат относительно друг друга и контейнера, в котором они располагаются.

Вставить сниппет можно из окна автозаполнения. Разместим курсор внутри **ContentPanel** и начнем писать "Lab". Нажмите на сочетание клавиш Ctrl+Space, чтобы открылось окно автозаполнения.

![](/files/-MacVm_OckgieHaf-thH)

Редактор подсказывает нам три сниппета: "Label + ComboBox", "Label + DateTimePicker" и "Label + TextBox". Справа от имени сниппета указывается его короткое имя. Например, lcombox. По этому имени также можно искать конкретный сниппет. Эти сниппеты пригодятся нам при создании объектов на форме.

Выберем сниппет "Label + TextBox" и нажмем Enter. Откроется окно настроек:

<figure><img src="/files/hBMxRggVpPtQX4Xbv3hi" alt=""><figcaption></figcaption></figure>

Последние два поля оставим пустыми, т.к. в **ContentPanel** у нас еще нет никаких объектов, к которым можно привязаться. Жмем Finish.

Редактор на место курсора подставит код наших объектов **NameLabel** и **NameTextBox**. Также откроет блокнот с текстом подсказки необходимых изменений. Сейчас эта подсказка нас не интересует - все необходимые доработки уже существуют.

Давайте смотреть, что добавил редактор. В объекте **NameLabel** редактор подсвечивает ошибки:&#x20;

![](/files/-Macdb4d0Ep7drgDdYkj)

Это из-за того, что последние два поля в настройках сниппета мы оставили пустыми. Давайте заменим значения для тэгов `<Top>` и `<Left>` на **5** и **10** соответственно.

Вторым интересным моментом является то, что редактор для объекта **NameTextBox** в тэг `<Text>` сразу подставил наше соединение с данными **ClientPrimaryGetDataConnection** и указал нужное поле.

```markup
<Text>
  <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
    <Fields>
      <Field Name="Name" />
    </Fields>
  </DataConnection>
</Text>
```

Перейдем в приложение и нажмем кнопку "Добавить запись...". Убедимся, что карточка клиента успешно загружена, и проверим расположение объектов.

<figure><img src="/files/Ip13DXZIoggTr9kPQcQh" alt=""><figcaption></figcaption></figure>

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

Для создания поля "Дата рождения" воспользуемся сниппетом "Label + DateTimePicker":

<figure><img src="/files/QIBIzMHoqQBRU2wdx8Ck" alt=""><figcaption></figcaption></figure>

Теперь заполним два последних поля. И укажем в них ранее созданные поля.

Жмем Finish.

Обратите внимание на значения тэгов `<Top>` у **DateOfBirthLabel** и **DateOfBirthDateTimePicker**. Отступ между объектами **NameTextBox** и **DateOfBirthLabel** равен 5 пикселей. А отступ между **DateOfBirthLabel** и **DateOfBirthDateTimePicker** - нулевой. Такие значения отступов позволяют визуально отделить поля одного свойства от полей другого свойства.

Редактор для объекта **DateOfBirthDateTimePicker** в тэг `<Value>` сразу подставил наше соединение с данными **ClientPrimaryGetDataConnection** и указал нужное поле.

Перейдем в приложение и убедимся, что карточка клиента успешно загружена, и проверим расположение объектов.

<figure><img src="/files/xqzfEKOVfr6db5XRlVcI" alt=""><figcaption></figcaption></figure>

Внесем небольшие правки в описание объекта **DateOfBirthDateTimePicker**. Во-первых, изменим его ширину, прописав в тэг `<Width>` значение **150**, чтобы поле выглядело симпатичнее. Во-вторых, изменим значение атрибута **Show** у тэга `<NullValue>` на **True**. Атрибут **Show** определяет, может ли дата иметь значение **NULL**.

В итоге синтаксис объекта **DateOfBirthDateTimePicker** будет выглядеть так:

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

```markup
<MyObject Name="DateOfBirthDateTimePicker" Type="DateTimePicker" Assembly="BaseControls">
  <Top>
    <Object Name="DateOfBirthLabel">
      <Property Name="Bottom" />
    </Object>
  </Top>
  <Left>
    <Object Name="DateOfBirthLabel">
      <Property Name="Left" />
    </Object>
  </Left>
  <Width>150</Width>
  <Format>dd MMMM yyyy</Format>
  <ShowCalendar>True</ShowCalendar>
  <NullValue Show="True" />
  <Value>
    <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
      <Fields>
        <Field Name="DateOfBirth" />
      </Fields>
    </DataConnection>
  </Value>
</MyObject>
```

{% endcode %}

По заданию поле "Город" должно быть сделано в виде выпадающего списка ([ComboBox](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/combobox)). Поэтому для его создания воспользуемся сниппетом "Label + ComboBox":

<figure><img src="/files/cwHP2jkR3ftBcffSBt81" alt=""><figcaption></figcaption></figure>

Помимо знакомых уже полей настроек, этот сниппет имеет две специальных настройки: **Имя схемы БД** и **Имя таблицы БД**. Они необходимы для автоматического создания SqlQuery, на основе которого будет заполняться выпадающий список.

Жмем Finish.

Как можем видеть, редактор в файле описания работы серверной части приложения (Template.xml) создал запрос **CityShortSelectSqlQuery**. Осталось добавить этот запрос в какой-нибудь [SqlQueryPermission](https://wfsys.gitbook.io/workflow-engine-syntax/workflow_engine/permissions/sql_query_permission). Например, в **CityViewSqlQueryPermission**.

Вы могли заметить, что текст запроса в созданном **CityShortSelectSqlQuery** полностью совпадает с текстом запроса в **CitySelectSqlQuery**. Такое дублирование кода допустимо. Лучше иметь отдельный запрос на заполнение выпадающего списка. Забегая вперед, назову две причины такого разделения. Во-первых, в будущем это упростит настраивание прав доступа. А, во-вторых, текст обоих запросов будет дополняться и изменяться по-разному.

Вернемся в файл карточки клиента (TemplateClientEdit.xml) и посмотрим, что добавилось там.

Редактор создал **CityShortPrimaryGetDataConnection**, который прописал в тэг `<ValueList>` созданного объекта **CityComboBox**. А в тэг `<Value>` того же объекта подставил **ClientPrimaryGetDataConnection** и указал нужное поле.

Перейдем в приложение и убедимся, что карточка клиента успешно загружена, и проверим расположение объектов.

Самостоятельно создайте поля "Контактный телефон" и "E-mail", использую нужные сниппеты. И скорректируйте размеры формы.

В результате у вас должна получиться форма подобного вида:

<figure><img src="/files/zKW31lcnEaspovow7knq" alt=""><figcaption></figcaption></figure>

### Редактирование выпадающего списка

По заданию справа от поля "Город" должна располагаться кнопка, по которой будет открываться форма со списком городов. Давайте создадим ее.

Для начала скачайте архив с изображением для этой кнопки и распакуйте его в папку **\Template\Projects\1. Template\Forms\Images\16x16**.

{% file src="/files/-MalpLlmHKqdDpQvnmfj" %}
Иконка для кнопки
{% endfile %}

Создайте самостоятельно команду на открытие формы TemplateCityList.xml.

Создадим саму кнопку, разместив код, представленный ниже, сразу после описания объекта **CityComboBox**.

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

```markup
<MyObject Name="CityListButton" Type="Button" Assembly="BaseControls">
  <Top>
    <Object Name="CityComboBox">
      <Property Name="Top" />
    </Object>
  </Top>
  <Left>
    <Formula>
      <Plus DataType="IntegerDataType">
        <Item>
          <Object Name="CityComboBox">
            <Property Name="Right" />
          </Object>
        </Item>
        <Item>5</Item>
      </Plus>
    </Formula>
  </Left>
  <Height>22</Height>
  <Width>22</Width>
  <Hint>Список городов...</Hint>
  <BackgroundImage>Images\16x16\list.png</BackgroundImage>
  <BackgroundImageLayout>Center</BackgroundImageLayout>
  <FlatStyle>Flat</FlatStyle>
  <FlatBorderSize>1</FlatBorderSize>
  <FlatBorderColor>ButtonFlatBorderColor</FlatBorderColor>
  <FlatMouseDownBackColor>ButtonFlatMouseDownBackColor</FlatMouseDownBackColor>
  <FlatMouseOverBackColor>ButtonFlatMouseOverBackColor</FlatMouseOverBackColor>
  <Commands>
    <Command Name="CityListFormShowCommand" />
  </Commands>
</MyObject>
```

{% endcode %}

Не забудьте поправить ширину **CityComboBox** с учетом ширины созданной кнопки и отступа в 5 пикселей между объектами.

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

<figure><img src="/files/6g4d9saGXfmwgZjwQ4Y8" alt=""><figcaption></figcaption></figure>

Осталось реализовать обновление **CityShortPrimaryGetDataConnection**, который предоставляет список для **CityComboBox**, если на форме были сохранены изменения.

Для этого самостоятельно выполните следующие пункты:

* Создайте DataConnectionRefreshCommand на CityShortPrimaryGetDataConnection;
* Создайте условие для проверки параметра Updated у команды открытия формы TemplateCityList.xml;
* Создайте Execution, который по условию из предыдущего пункта будет вызывать команду на обновление CityShortPrimaryGetDataConnection;
* На форме TemplateCityList.xml создайте параметр Updated и команду UpdatedTrueValueSetCommand;
* Там же на форме TemplateCityList.xml в тэге `<Commands>` кнопки SaveButton исправьте список выполняемых команд. Удалите вызов команды CityDataConnectionRefreshCommand. Вместо нее добавьте вызовы команд UpdatedTrueValueSetCommand и FormCloseCommand;
* Так же на форме TemplateCityList.xml удалите описание команды CityDataConnectionRefreshCommand. Эта команда нам больше не понадобится.

Перейдем в приложение. Убедимся, что формы успешно загружаются, а при сохранении изменений на форме списка городов, в карточке клиента обновляется список городов в **CityComboBox**.

### Сохранение изменений в таблицу в базе данных

Запросы **ClientInsertSqlQuery**, **ClientUpdateSqlQuery** у нас уже есть. Давайте сделаем для них два  [SetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/set_dc):

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

```markup
<DataConnection Name="ClientInsertSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <Parameters>
    <Parameter NativeName="Name">
      <Value>
        <Object Name="NameTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="DateOfBirth">
      <Value>
        <Object Name="DateOfBirthDateTimePicker"/>
      </Value>
    </Parameter>
    <Parameter NativeName="CityId">
      <Value>
        <Object Name="CityComboBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="Phone">
      <Value>
        <Object Name="PhoneTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="Email">
      <Value>
        <Object Name="EmailTextBox" />
      </Value>
    </Parameter>
  </Parameters>
  <SqlQueries>
    <SqlQuery Name="ClientInsertSqlQuery" Type="Insert" />
  </SqlQueries>
</DataConnection>

<DataConnection Name="ClientUpdateSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <Parameters>
    <Parameter NativeName="ClientId">
      <Value>
        <Parameter Name="ClientId" />
      </Value>
    </Parameter>
    <Parameter NativeName="Name">
      <Value>
        <Object Name="NameTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="DateOfBirth">
      <Value>
        <Object Name="DateOfBirthDateTimePicker"/>
      </Value>
    </Parameter>
    <Parameter NativeName="CityId">
      <Value>
        <Object Name="CityComboBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="Phone">
      <Value>
        <Object Name="PhoneTextBox" />
      </Value>
    </Parameter>
    <Parameter NativeName="Email">
      <Value>
        <Object Name="EmailTextBox" />
      </Value>
    </Parameter>
  </Parameters>
  <SqlQueries>
    <SqlQuery Name="ClientUpdateSqlQuery" Type="Update" />
  </SqlQueries>
</DataConnection>
```

{% endcode %}

Как вы помните, чтобы SetDataConnection выполнился, нужна команда [SaveCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/save_command), которая будет активировать соединение с данными для отправки. Давайте создадим две команды для каждого SetDataConnection:

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

```markup
<Command Name="ClientInsertSaveCommand" Type="SaveCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="ClientInsertSetDataConnection" />
  </DataConnections>
</Command>

<Command Name="ClientUpdateSaveCommand" Type="SaveCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="ClientUpdateSetDataConnection" />
  </DataConnections>
</Command>
```

{% endcode %}

Как вы помните, наш запрос **ClientInsertSqlQuery** возвращает идентификатор **client\_id** новой записи. Команда **ClientInsertSaveCommand** получает этот идентификатор от **ClientInsertSetDataConnection** и записывает его в свой результат выполнения. Давайте создадим команду типа [ValueSetCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/value_set_command), которая запишет этот результат в параметр формы **ClientId**:

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

```markup
<Command Name="ClientIdValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Parameter Name="ClientId">
    <Command Name="ClientInsertSaveCommand" />
  </Parameter>
</Command>
```

{% endcode %}

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

Скорректируем синтаксис команды **SaveSequentialCommand**, которая уже есть в коде:

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

```markup
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands>
    <If>
      <When>
        <Parameter Name="Edit" />
      </When>
      <Then>
        <Command Name="ClientUpdateSaveCommand" />
      </Then>
      <Else>
        <Command Name="ClientInsertSaveCommand" />
        <Command Name="ClientIdValueSetCommand" />
      </Else>
    </If>
    <Command Name="UpdatedTrueValueSetCommand" />
    <Command Name="FormCloseCommand" />
  </Commands>
</Command>
```

{% endcode %}

Вернемся в файл стартовой формы (TemplateStart.xml). Здесь мы получим значение параметра **ClientId** из команды **ClientEditFormShowCommand**, которое будем использовать для выделение строки в таблице **ClientDatabaseTable**. Это позволит пользователю убедиться в том, что новая запись успешно сохранилась. Для выделения строки в таблице создадим следующую команду:

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

```markup
<Command Name="ClientSelectInTableValueSetCommand" Type="ValueSetCommand" Assembly="Commands">
  <Object Name="ClientDatabaseTable">
    <Property Name="SelectRowByFieldValue">
      <Parameters>
        <Parameter Name="ColumnName">ClientId</Parameter>
        <Parameter Name="Value">
          <Command Name="ClientEditFormShowCommand" Parameter="ClientId" />
        </Parameter>
      </Parameters>
    </Property>
  </Object>
</Command>
```

{% endcode %}

Добавьте вызов этой команды в Execution по изменению параметра **Updated** у команды **ClientEditFormShowCommand** после вызова команды типа [DataConnectionRefreshCommand](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/commands/dc_refresh_command).

Перезапустите проект. Убедитесь, что формы успешно загружаются, и происходит выделение строки нового клиента.

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

Вернемся в файл стартовой формы (TemplateStart.xml) и реализуем функционал удаления записей из таблицы клиентов.

У нас уже есть sql-запрос на удаление записи в таблице **template.client** в базе данных. Давайте создадим SetDataConnection для этого запроса:

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

```markup
<DataConnection Name="ClientDeleteSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <Parameters>
    <Parameter NativeName="ClientId">
      <Value>
        <Object Name="ClientDatabaseTable">
          <Property Name="SelectedRowCellValueByColumnName">
            <Parameters>
              <Parameter Name="ColumnName">ClientId</Parameter>
            </Parameters>
          </Property>
        </Object>
      </Value>
    </Parameter>
  </Parameters>
  <SqlQueries>
    <SqlQuery Name="ClientDeleteSqlQuery" Type="Delete" />
  </SqlQueries>
</DataConnection>
```

{% endcode %}

Sql-запрос ожидает только один параметр **ClientId**. В качестве значения в этот параметр передадим значение из одноименной колонки выделенной строки таблицы **ClientDatabaseTable**.

После удаления записи из таблицы в базе данных нам нужно будет обновить данные, получаемые из **ClientPrimaryGetDataConnection**. Это можно сделать используя знакомую нам команду DataConnectionRefreshCommand. А можно использовать более компактный способ обновить DataConnection.

У [SetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/set_dc) и [DatabaseTableSetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/database_table_set_dc) есть необязательный тэг `<Refresh>`, в качестве значения которого указывается список тэгов `<DataConnection>` c именами загружающих соединений с данными, которые будут обновлены после выполнения сохранения.

Давайте добавим такой тэг в наш **ClientDeleteSetDataConnection**. Таким образом, общий синтаксис соединения с данными для отправки будет выглядеть так:

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

```markup
<DataConnection Name="ClientDeleteSetDataConnection" Type="SetDataConnection" Assembly="DataConnections">
  <Workflow Name="Template" />
  <Parameters>
    <Parameter NativeName="ClientId">
      <Value>
        <Object Name="ClientDatabaseTable">
          <Property Name="SelectedRowCellValueByColumnName">
            <Parameters>
              <Parameter Name="ColumnName">ClientId</Parameter>
            </Parameters>
          </Property>
        </Object>
      </Value>
    </Parameter>
  </Parameters>
  <SqlQueries>
    <SqlQuery Name="ClientDeleteSqlQuery" Type="Delete" />
  </SqlQueries>
  <Refresh>
    <DataConnection Name="ClientPrimaryGetDataConnection" />
  </Refresh>
</DataConnection>
```

{% endcode %}

Самостоятельно создайте команду SaveCommand для нового DataConnection и пропишите ее в тэге `<Commands>` кнопки "Удалить запись...".

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

<figure><img src="/files/18wACXofWO1OLpcBKKmD" alt=""><figcaption></figcaption></figure>

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

На уроке мы узнали, что помимо паттернов форм, есть сниппеты, которые позволяют вставлять куски кода в редактируемый файл. Сниппеты ускоряют процесс наполнения формы объектами.

Также рассмотрели использование универсального значения [Input](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/values/input), которое позволяет передавать в команду значения параметров в месте вызова команды, а не при ее описании.

Еще с первого урока у нас есть проблема с отсутствием проверки корректности введенных пользователем данных. На следующем уроке рассмотрим, как это сделать корректно и удобно для пользователя. Также рассмотрим паттерн onClose, суть которого заключается в проверке наличия изменений на форме и уведомлении пользователя о них при попытке закрыть форму без сохранения.

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

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

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

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

s

{% file src="/files/Vnjie62mswX6y7b1hXGx" %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://wfsys.gitbook.io/wt-practice/main/lesson_combo_box.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
