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

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

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

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

  • познакомимся с главным меню MainMenu;

  • рассмотрим SecondaryGetDataConnection.

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

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

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

Список заказов и карточка заказа

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

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

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

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

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

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

Главная форма

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

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

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

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

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

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

Для колонки OrderDate используйте вложенный необязательный тэг <DataType>, задающий формат строки, в которую будет преобразовано значение из источника данных при заполнении таблицы:

<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-запросе на получение списка заказов исключить удаленные заказы, чтобы не отображать их на главной форме.

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

Команда INSERT позволяет получать данные из добавленной записи без использования дополнительного запроса к базе данных. Для этого используется предложение RETURNING.

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

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

Карточка заказа

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

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

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

Так как пользователь не может редактировать значение объектов CityTextBox и PhoneTextBox, то исключим их из проверки свойства формы FormChanged, прописав в тэге <MyObject> атрибут ChangeForm со значением False.

Для того чтобы перевести текстовое поле "Комментарий к заказу" (DescriptionTextBox) в многострочный режим, добавьте в описание поля тэг <Multiline> со значением True и настройте высоту поля.

Не забудьте на форме реализовать паттерн onClose.

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

Временные зоны

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

Правила передачи времени между сервером и клиентом

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

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

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

Временная зона сервера

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

appsettings.json
"TimeZone": {
  "TimeZone": "Europe/Moscow",
}

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

Временная зона клиента

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

Карточка заказа

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

Для получения данных о выбранном клиенте будем использовать вторично загружающее соединение с данными SecondaryGetDataConnection, для которого в качестве источника данных укажем ClientPrimaryGetDataConnection и добавим фильтр по полю ClientId. В качестве значения фильтра будем брать значение из выпадающего списка ClientComboBox.

TemplateOrderEdit.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>

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

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

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

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

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

Главная форма

Подстановка значений в колонки таблицы

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

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

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

Для подстановки значения в колонку используется вложенный тэг <Substitution> у тэга <Column>. Добавим в описание колонки ClientTitle таблицы OrderDatabaseTable тэг <Substitution>:

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

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

Сейчас у нас нет возможности просматривать списки клиентов и городов. Давайте добавим главное меню <MainMenu>, чтобы иметь доступ к нашим спискам. Но для начала самостоятельно создайте команды ClientListFormShowCommand и CityListFormShowCommand для открытия форм списка клиентов и списка городов соответственно. А затем скопируйте представленный ниже код и добавьте его в файл стартовой формы перед описанием <MyObjects>.

TemplateStart.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>

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

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

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

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

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

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

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

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

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

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

Затем на главной форме для CityPrimaryGetDataConnection и ClientPrimaryGetDataConnection создайте отдельные команды DataConnectionRefreshCommand. Для каждой команды создайте отдельный Execution, который будет проверять параметр Updated у соответствующей команды FormShowCommand.

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

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

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

Итоги

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

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

Рассмотрели механизм подстановки данных в колонку таблицы DatabaseTable, который в коде реализуется через вложенный тэг <Substitution> у тэга <Column>.

Ответы

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

Last updated