# Урок 6. Главная форма

На прошлом уроке мы рассмотрели разные варианты удаления связанных записей и остановились на варианте с архивированием.

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

В этом уроке мы:

* познакомимся с главным меню [MainMenu](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/menus/main_menu);
* рассмотрим  [SecondaryGetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/secondary_dc).

  <div data-gb-custom-block data-tag="hint" data-style="success" class="hint hint-success"><p>Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье <a href="https://wfsys.gitbook.io/workflow-technology/setting-up-dev-environment/manual-deployment-project">Разворачивание проекта</a>.</p><p>При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела <a href="/pages/-MdvNprVvngfwvsUA7NA#answer">Ответы</a> прошлого урока. Скопируйте папки <em>Forms</em>, <em>Workflow</em> и <em>Patterns</em> в папку с развернутым проектом, например, в папку <em>D:\WT\Projects\Template\Projects\1. Template</em>.</p><p>Инструкция по подключению шаблонов находится по <a href="/pages/-M_eX46yXXalFkHRNC7X#podklyuchenie-shablonov-k-proektu">ссылке</a>.</p></div>

## Список заказов и карточка заказа <a href="#list-of-orders-and-order-card" id="list-of-orders-and-order-card"></a>

По итогам текущего урока у нас должны получиться формы вида:

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

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

### Таблица в базе данных

Начнем с таблицы в базе данных.

Создайте таблицу **template.order** для заказов. В таблице будем хранить идентификатор клиента (client\_id), номер заказа (order\_number), дату и время заказа (order\_date) и комментарий к заказу (description). Все поля, кроме комментария, должны иметь ограничение **not null**.

Для дат со временем используем тип **timestamp without time zone**, который не хранит информацию о временной зоне. В таком случае PostgreSQL предполагает, что все временные данные принадлежат одной временной зоне, и не производит никаких преобразований. О том, как происходит работа с временными зонами в платформе Workflow Technology, мы поговорим далее в уроке.

Для удаления заказов в таблице будем использовать колонку **deleted** типа boolean с ограничением **not null** и значение по умолчанию **false**. Значение в этой колонке будем обновлять через update-запрос при попытке удалить заказ. Таким образом, мы будем хранить в базе данных информацию обо всех когда-либо созданных заказах. А в одном из будущих уроков реализуем форму для просмотра удаленных заказов.

### Главная форма <a href="#main-form" id="main-form"></a>

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

Давайте в последний раз заменим стартовую форму. Для этого файл TemplateStart.xml переименуем в TemplateClientList.xml, создадим форму для списка заказов и назовем ее TemplateStart.xml.

Для создания формы воспользуйтесь паттерном. Самостоятельно создайте все необходимы объект и элементы формы.

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

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

Таблица на форме списка заказов должна содержать колонки OrderId, ClientId, CityId, № (RowNumber), Номер заказа (OrderNumber), Дата и время заказа (OrderDate), Город (CityTitle) и Клиент (ClientTitle).&#x20;

В запрос для получения списка заказов и PrimaryGetDataConnection для таблицы не добавляйте поля **ClientTitle** и **CityTitle**. Позже в уроке мы рассмотрим, как динамически подтягивать имена клиентов и названия городов в таблицу, имея только идентификаторы в колонках **ClientId** и **CityId**.&#x20;

Для колонки **OrderDate** используйте вложенный необязательный тэг [`<DataType>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable/column#column_data_type), задающий формат строки, в которую будет преобразовано значение из источника данных при заполнении таблицы:

```xml
<Column Name="OrderDate" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
  <Title>Дата и время заказа</Title>
  <Width>130</Width>
  <AutoSizeMode Value="None" />
  <Alignment Value="MiddleCenter" />
  <DataType Type="DateTimeDataType" Format="dd.MM.yyyy HH:mm" />
</Column>
```

Для удаления заказов реализуйте update-запрос на обновление значения в колонках **deleted** и **date\_deleted**. Не забудьте в select-запросе на получение списка заказов исключить удаленные заказы, чтобы не отображать их на главной форме.

Реализуйте выделение нужной строки в таблице заказов после создания и редактирования заказа.

{% hint style="info" %}
Команда `INSERT` позволяет получать данные из добавленной записи без использования дополнительного запроса к базе данных. Для этого используется предложение `RETURNING`.

С его помощью мы можем получить идентификатор добавленного заказа и вернуть его на форму. Результат запроса будет записан в результат выполнения команды SaveCommand, откуда его можно сохранить в параметр формы.
{% endhint %}

На кнопках используйте режим **DisabledMode.**

### Карточка заказа <a href="#order-card" id="order-card"></a>

Создайте форму карточки заказа (TemplateOrderEdit.xml) с полями: Номер заказа (OrderNumberTextBox), Дата и время заказа (OrderDateDateTimePicker), Клиент (ClientComboBox), Город (CityTextBox), Телефон клиента (PhoneTextBox) и Комментарий к заказу (DescriptionTextBox).

Для выпадающего списка клиентов **ClientComboBox** создайте отдельный запрос на получения списка клиентов и назовите его, например, ClientShortSelectSqlQuery. Этот запрос должен возвращать поля ClientId, ClientTitle, CityTitle и Phone. Для запроса ClientShortSelectSqlQuery создайте отдельный SqlQueryPermission (например, ClientOrderViewSqlQueryPermission), который добавьте в роли **OrderViewRole** и **OrderEditRole**.&#x20;

Поля "Город" (CityTextBox) и "Телефон клиента" (PhoneTextBox) оставьте без указания источника данных в тэге `<Text>` и сделайте их нередактируемыми, указав признак `<ReadOnly>` со значением **True**. Эти поля будут информативными и будут заполнятся данными из **ClientShortSelectSqlQuery** для клиента, выбранного в выпадающем списке **ClientComboBox**. Позже в уроке рассмотрим инструмент, который позволяет это сделать.

Так как пользователь не может редактировать значение объектов CityTextBox и PhoneTextBox, то исключим их из проверки свойства формы [FormChanged](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/form#get_form_changed), прописав в тэге `<MyObject>` атрибут `ChangeForm` со значением **False**.

Для того чтобы перевести текстовое поле "Комментарий к заказу" (DescriptionTextBox) в многострочный режим, добавьте в описание поля тэг [`<Multiline>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/textbox#multiline) со значением True и настройте высоту поля.

Не забудьте на форме реализовать [паттерн **onClose**](https://wfsys.gitbook.io/wt-practice/main/lesson_4#pattern-onclose)**.**

По завершении у вас будет форма вида:

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

## Временные зоны <a href="#time-zone" id="time-zone"></a>

Так как в карточке заказа мы используем [DateTimePicker](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/date_time_picker) для даты со временем, необходимо сделать отступление и поговорить о том, как происходит работа со временем и временными зонами в платформе. Подробно по этому вопросу можно почитать по [ссылке](https://wfsys.gitbook.io/wt-knowledge-base/platforma-wt/time_zone), а сейчас затронем только те моменты, которые важны для текущей работы.

#### Правила передачи времени между сервером и клиентом <a href="#rules_timezone" id="rules_timezone"></a>

Дата со временем (в формате даты) между клиентом и сервером всегда передается в UTC. Когда дату со временем передаем с формы на сервер, платформа переводит время из временной зоны клиента в UTC, а сервер полученную дату переводит из UTC в свою временную зону.

Если в DateTimePicker используется только дата (без времени) или только время, то на сервер значение DateTimePicker отправится без перевода в UTC. Пример такой отправки мы наблюдаем в карточке клиента с полем "Дата рождения". А если в DateTimePicker используется дата и время, то они будут приведены к UTC. Эта логика будет отрабатываться в карточке заказа.

Аналогичное преобразование происходит и в обратную сторону. Сервер получает дату со временем из база данных, переводит ее в UTC и передает клиенту. Клиент, получив дату со временем, переводит ее из UTC в свою временную зону.

#### Временная зона сервера <a href="#server_timezone" id="server_timezone"></a>

Даты со временем хранятся в базе данных во временной зоне сервера, по умолчанию это **Etc/GMT**. Чтобы задать свою временную зону, необходимо в серверный файл настроек (appsettings.json) внести строки:

{% code title="appsettings.json" %}

```json
"TimeZone": {
  "TimeZone": "Europe/Moscow",
}
```

{% endcode %}

В качестве значения вложенного поля TimeZone ожидается имя временной зоны в формате IANA. В базе данных в таблице **public.time\_zone\_info** в поле **name** указаны все доступные имена временных зон, поддерживаемые платформой.

#### Временная зона клиента <a href="#client_timezone" id="client_timezone"></a>

Для хранения временной зоны клиента используется колонка **time\_zone\_info\_id** в таблице **public."user"**. По умолчанию там стоит идентификатор временной зоны **Europe/Moscow**. На временную зону клиента не влияют настройки часового пояса в системе Windows.

## Карточка заказа <a href="#order-card-detail" id="order-card-detail"></a>

Давайте вернемся в файл карточки заказа (TemplateOrderEdit.xml) и доделаем поля "Город" (CityTextBox) и "Телефон клиента" (PhoneTextBox),&#x20;

Для получения данных о выбранном клиенте будем использовать вторично загружающее соединение с данными [SecondaryGetDataConnection](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/dataconnections/secondary_dc), для которого в качестве источника данных укажем **ClientPrimaryGetDataConnection** и добавим фильтр по полю **ClientId**. В качестве значения фильтра будем брать значение из выпадающего списка **ClientComboBox**.

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

```xml
<DataConnection Name="ClientSecondaryGetDataConnection" Type="SecondaryGetDataConnection" Assembly="DataConnections">
  <SourceDataConnection Name="ClientPrimaryGetDataConnection" />
  <Filter>
    <Field NativeName="ClientId" />
    <Value>
      <Object Name="ClientComboBox" />
    </Value>
    <DataType Type="IntegerDataType" />
  </Filter>
</DataConnection>
```

{% endcode %}

SecondaryGetDataConnection можно использовать на форме так же, как и PrimaryGetDataConnection, обращаясь к его полям, которые наследуются из источника. Например, для задания значения тэга `<Text>` у объекта **CityTextBox** будем использовать запись вида:

```xml
<DataConnection SourceDataConnection="ClientSecondaryGetDataConnection">
  <Fields>
    <Field Name="CityTitle"/>
  </Fields>
</DataConnection>
```

Аналогично заполните тэг `<Text>` у объекта **PhoneTextBox**.

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

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

Отлично! Теперь можем вернуться в файл главной формы (TemplateStart.xml). Оставшуюся часть урока полностью посвятим ей.

## Главная форма <a href="#main-form-detail" id="main-form-detail"></a>

### Подстановка значений в колонки таблицы <a href="#substitution-values-in-table-columns" id="substitution-values-in-table-columns"></a>

В таблице заказов остались незаполненными колонки "Город" (CityTitle) и "Клиент" (ClientTitle). Давайте рассмотрим механизм, который позволяет подтягивать в колонку таблицы значение из стороннего источника данных на основе значения из другой колонки таблицы.

Создайте **ClientSimpleSelectSqlQuery** с двумя полями ClientId и ClientTitle, и поместите его в разрешение **ClientOrderViewSqlQueryPermission**, так как он связан с просмотром заказов.

В файле главной формы создайте **ClientPrimaryGetDataConnection** на этот запрос. Этот DataConnection будем использовать в качестве источника данных для заполнения колонки **ClientTitle** таблицы **OrderDatabaseTable**.&#x20;

Для подстановки значения в колонку используется вложенный тэг [`<Substitution>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable/column#column_substitution) у тэга `<Column>`.  Добавим в описание колонки ClientTitle таблицы OrderDatabaseTable тэг `<Substitution>`:

```xml
<Column Name="ClientTitle" Type="DatabaseTableColumnTextBox" Assembly="DatabaseTableColumnControls">
  <Title>Клиент</Title>
  <Width>300</Width>
  <AutoSizeMode Value="Fill" />
  <Substitution SourceColumn="ClientId">
    <DataConnection SourceDataConnection="ClientPrimaryGetDataConnection">
      <Fields>
        <Field Name="ClientId" />
        <Field Name="ClientTitle" />
      </Fields>
    </DataConnection>
  </Substitution>
</Column>
```

Атрибут `SourceColumn` тэга `<Substitution>` ожидает имя колонки, на основе значения которой будут подставляться данные для отображения. В качестве значения тэга `<Substitution>` ожидается матрица из двух колонок: первая должна содержать ключ, по которому будет происходить выборка значения, а вторая - самое значение.

Аналогично сделайте для колонки **CityTitle**. Создайте для этого новый запрос **CitySimpleSelectSqlQuery**, который будет возвращать список всех городов (архивных и неархивных), и создайте для него отдельное разрешение **CityOrderViewSqlQueryPermission**.

Запустите проект и проверьте корректность заполнения таблицы.

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

### Главное меню <a href="#main-menu" id="main-menu"></a>

Сейчас у нас нет возможности просматривать списки клиентов и городов. Давайте добавим главное меню [`<MainMenu>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/menus/main_menu), чтобы иметь доступ к нашим спискам. Но для начала самостоятельно создайте команды **ClientListFormShowCommand** и **CityListFormShowCommand** для открытия форм списка клиентов и списка городов соответственно. А затем скопируйте представленный ниже код и добавьте его в файл стартовой формы перед описанием `<MyObjects>`.

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

```xml
<MainMenu>
  <MenuItem Name="FileMenuItem" Type="MenuItem">
    <Title>Файл</Title>
    <Items>
      <MenuItem Name="FileRefreshMenuItem" Type="MenuItem">
        <Title>Обновить</Title>
        <Command Name="" />
      </MenuItem>

      <MenuItem Name="FileSeparator" Type="Separator" />

      <MenuItem Name="FileExitMenuItem" Type="MenuItem">
        <Title>Выход</Title>
        <Command Name="FormCloseCommand" />
      </MenuItem>
    </Items>
  </MenuItem>

  <MenuItem Name="ListMenuItem" Type="MenuItem">
    <Title>Списки</Title>
    <Items>
      <MenuItem Name="CityMenuItem" Type="MenuItem">
        <Title>Города...</Title>
        <Command Name="CityListFormShowCommand" />
      </MenuItem>

      <MenuItem Name="ClientMenuItem" Type="MenuItem">
        <Title>Клиенты...</Title>
        <Command Name="ClientListFormShowCommand" />
      </MenuItem>
    </Items>
  </MenuItem>
</MainMenu>
```

{% endcode %}

В пункте меню **FileRefreshMenuItem** (Обновить) в качестве значения атрибута `Name` тэга `<Command>` укажите имя команды на обновление PrimaryGetDataConnection, который используете в таблице заказов.

Если сейчас запустим проект, то на стартовой форме не увидим главное меню. Это связано с тем, что меню размещается в верхней части формы, там, где расположена панель **HeadPanel**, а точнее, под этой панелью. Давайте немного сдвинем панель вниз, задав координате `<Top>` значение **24**:

```xml
<MyObject Name="HeadPanel" Type="Panel" Assembly="BaseControls">
  <Top>24</Top>
  <Left>0</Left>
  <Height>50</Height>
  <Width>
    <Form>
      <Property Name="Width" />
    </Form>
  </Width>
  <BackColor>HeadBackgroundColor</BackColor>
  
  <!-- ... -->
  
</MyObject>
```

Запустим проект и проверим отображение главного меню.

<figure><img src="/files/4MWlrkrkGmb91OVR1V42" alt=""><figcaption></figcaption></figure>

Отлично! Теперь мы имеем доступ к нашим спискам и можем их редактировать.

В карточке клиента у нас есть кнопка **CityListButton** для доступа к списку городов. Ее можно удалить - она нам больше не пригодится. Удалите ее и все элементы, связанные с логикой обработки результата с формы списка городов.

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

#### Обновление списков городов и клиентов на главной форме

Сейчас при изменении города или клиента главная форма не узнает об этом,  и в таблице будет отображаться старая информация. Чтобы этот момент поправить, мы должны отлавливать наличие изменений на форме списка, передавать признак изменений на главную форму и обновлять соответствующий DataConnection.

На форме списка городов уже есть параметр Updated, которому присваиваем значение True, когда редактируем список городов. Добавьте такой же параметр на форму списка клиентов и изменяйте его значение, когда происходит редактирование списка клиентов.

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

#### Архивирование записей клиентов

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

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

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

В этом уроке мы окончательно определились с главной формой в нашем проекте. До конца практики ее внешний вид не будет сильно меняться. В следующем уроке мы добавим фильтры для упрощения работы со списком заказов. Рассмотрим различные виды фильтрации данных на форме и особенности их применения.

Также на уроке мы познакомились с объектом [MainMenu](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/menus/main_menu), который применяется для организации классического доступа к различным функциям приложения.

Рассмотрели механизм подстановки данных в колонку таблицы [DatabaseTable](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable), который в коде реализуется через вложенный тэг [`<Substitution>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable/column#column_substitution) у тэга [`<Column>`](https://wfsys.gitbook.io/workflow-forms-syntax/workflow_forms/objects/databasetable/column).

## Ответы <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>lesson6-answer.zip</td><td><a href="https://wfsys.ru/download/wt_practice_desktop_answers/lesson6-answer.zip">https://wfsys.ru/download/wt_practice_desktop_answers/lesson6-answer.zip</a></td></tr></tbody></table>


---

# 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_main_form.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.
