Практика avm, примеры интерфейсов#
Раздел описывает практику разметки в AVM и рассматривает примеры реализации интерфейсов.
Присоединение нескольких отображений#

Для этого в разметке нужно выбрать tabDynamicComposer, который позволяет присоединять отображения с помощью dynamicItems и tabItems.
Примечание
Напоминание:
dynamicItems — обычное присоединение отображения с возможностью выбора выравнивания;
tabItems — присоединение отображения как вкладки.
<representation name="Card" editMode="edit" stdFilter.isAvailable="false">
<layout>
<tabDynamicComposer>
<frame filter.isVisible="false">
<card/>
</frame>
<dynamicItems>
<dynamicItem selection="gtk-ru.bitec.app.oilOil_RecievTaskAvi" representation="List_Statistics" align="right"/>
</dynamicItems>
<tabItems isVisible="true"
selection="gtk-ru.bitec.app.btk.Btk_ObjectTypeTabAvi"
representation="List_Tab"
selection.selectionAttr="SSELECTIONNAME"
selection.representationAttr="SREPRESENTATIONNAME"
selection.captionAttr="SCAPTION"
selection.imageIndexAttr="NIMAGE"
selection.paramsAttr="JSONPARAMS"/>
</tabDynamicComposer>
</layout>
</representation>
Примечание
В примере разметки в tabItems присоединены вкладки, настроенные на типе объекта. Это часто встречающаяся практика, данный пример полезно понимать.
Отключение сворачивания отображения#
Чтобы убрать кнопку сворачивания отображения, присоединенного dynamicItems,
необходимо выключить header у frame:
<representation name="Card_Body" editMode="edit" stdFilter.isAvailable="false">
<layout>
<simpleComposer>
<frame header.isVisible="false">
<card/>
</frame>
</simpleComposer>
</layout>
</representation>
Этот способ убирает заголовок с кнопкой.
Заголовок можно вернуть с помощью конструкции vGroup.
<representation name="Card_Body" editMode="edit" stdFilter.isAvailable="false">
<layout>
<tabComposer>
<frame filter.isVisible="false" toolBar.isVisible="false" header.isVisible="false">
<card>
<layout>
<vBox>
<vGroup caption="Задание на приём нефтепродуктов">
<hBox>
<vBox>
<attr name="sRegNum"/>
<attr name="dReg"/>
<attr name="idStateHL"/>
</vBox>
<vBox>
<attr name="idObjectTypeHL"/>
<attr name="gidSrcHL"/>
<attr name="gidSrc"/>
</vBox>
</hBox>
<attr name="sDescription"/>
</vGroup>
</vBox>
</layout>
</card>
</frame>
<tabItems isVisible="true"
selection="gtk-ru.bitec.app.btk.Btk_ObjectTypeTabAvi"
representation="List_Tab"
selection.selectionAttr="SSELECTIONNAME"
selection.representationAttr="SREPRESENTATIONNAME"
selection.captionAttr="SCAPTION"
selection.imageIndexAttr="NIMAGE"
selection.paramsAttr="JSONPARAMS"/>
</tabComposer>
</layout>
</representation>
Отображение с кнопкой сворачивания:

Отображение с выключенным header:

Отображение с
vGroup:
Динамическая смена отображения#
Эта тема отвечает на вопрос, как при изменении значения поля менять присоединяемое отображение.
Примечание
Так работает tabItems на типе объекта: при смене типа объекта набор вкладок в tabItems меняется на те, которые настроены на выбранном типе объекта.
Смена отображений при изменении значения фильтра#
Пример реализации:
ru.bitec.app.oil.Oil_TaskMonitor.avm.xml— разметка отображенияCard;ru.bitec.app.oil.Oil_TaskMonitorAvi.Card— результат выборки отображенияCard.
AVM:
<representation name="Card">
<filter name="Oil_TaskMonitorFilter">
<macros name="DefFltReferenceMacro">
<condition logicalOperator="and" id="attrs" isExpression="false">
<filterAttr name="flt_idObjectType" attribute-type="Long" caption="Тип задания"
isLastInLine="false" order="10" editorType="edit" isVisible="false"/>
<filterAttr name="flt_idObjectTypeHL" attribute-type="Varchar" caption="Тип задания"
isLastInLine="false" order="10.2" editorType="lookup">
<editor>
<lookup lookupQuery="gtk-Btk_ObjectTypeAvi#MainLookup" lookupKeyAttr="id"
lookupListAttr="sHeadLine" changeableAttr="flt_idObjectType"
isLookupLazyLoad="true"/>
</editor>
</filterAttr>
<filterAttr name="flt_dBegin" attribute-type="Date" caption="Дата, время с" isLastInLine="false"
order="20" editorType="dateTimePick">
<card controlWidth="60" isControlWidthFixed="true"/>
</filterAttr>
<filterAttr name="flt_dEnd" attribute-type="Date" caption="по" isLastInLine="true"
order="30" editorType="dateTimePick">
<card controlWidth="60" isControlWidthFixed="true"/>
</filterAttr>
</condition>
</macros>
</filter>
<layout>
<tabDynDetComposer>
<frame filter.isVisible="true">
<card/>
</frame>
<tabItems selection="gtk-Oil_TaskMonitorAvi" representation="List_RecievTask" isVisible="false"/>
<dynDetail masterAlign="top" detailAlign="client" selectionAttr="sSelectionAttr"
representationAttr="sRepresentationAttr" paramsAttr="jsonParams" isVisible="true"/>
</tabDynDetComposer>
</layout>
<attributes>
<attr name="sSelectionAttr" isVisible="false"/>
<attr name="sRepresentationAttr" isVisible="false"/>
<attr name="sCaptionAttr" isVisible="false"/>
<attr name="nImageIndexAttr" isVisible="false"/>
<attr name="jsonParams" isVisible="false"/>
</attributes>
</representation>
Это отображение Card.
В теге filter определены фильтры. Панель фильтров включена свойством тега <frame filter.isVisible="true">.
Присоединение отображений выполнено инструментом tabDynDetComposer, который позволяет присоединять tabItems закладками, а dynDetail — аналогично dynamicItem.
Закладка tabItems присоединена как пустышка, чтобы заработал tabDynDetComposer (проблема была актуальна на 05.07.2023, обещали исправить).
В закладке dynDetail определены свойства, значения которых являются атрибутами отображения (см. перечень атрибутов отображения). Тем самым отображение Card как результат выборки должно иметь настройку для dynDetail.
Для формирования такой выборки необходимо рассмотреть Avi:
trait List_CommonResource extends Default with List_CoreResource {
override protected val sNameFieldForGST = "Brigade"
override protected val sNameTableForGST = "Bs_Brigade"
}
trait Card extends Default with super.Card {
//Типы объекта класса Oil_Task и соответствующие отображения к ним
lazy val mapRepByOT: Map[NString, NString] =
Map("Oil_TaskReceive".ns -> "List_RecievTask".ns
, "Oil_TaskShip".ns -> "List_ShipTask".ns
, "Oil_TaskCommon".ns -> "List_CommonTask".ns
)
@FlushBefore(mode = FlushBeforeMode.Disabled)
override protected def onRefresh: Recs = {
val idvOT = getVar("flt_idObjectType").asNLong
if (idvOT.isNotNull) {
val sRep = mapRepByOT.getOrElse(Btk_ObjectTypeApi().getMnemoCode(idvOT), throw AppException("Не найдено отображение."))
s"""
select
'gtk-Oil_TaskMonitorAvi' as sSelectionAttr
,'$sRep' as sRepresentationAttr
,'${Btk_ObjectTypeApi().getShortCaption(idvOT)}' as sCaptionAttr
,cast(null as numeric) as nImageIndexAttr
,'{}' as jsonParams
"""
} else {
s"""
select cast(null as int8) as id
,cast(null as numeric) as nOrder
,cast(null as varchar) as sCaptionAttr
,cast(null as numeric) as nImageIndexAttr
,cast(null as varchar) as sRepresentationAttr
,cast(null as varchar) as sSelectionAttr
,'{}' as jsonParams
where false
"""
}
}
override protected def selectStatement: JClob = s""
}
У отображения есть фильтр по типу объекта flt_idObjectType с редактором lookup. При выборе различных типов объекта к Card должны присоединяться различные отображения. Их соответствие описано в mapRepByOT.
Алгоритм метода onRefresh:
Проверить, заполнено ли значение фильтра типа объекта:
val idvOT = getVar("flt_idObjectType").asNLong if (idvOT.isNotNull) {}
Получить соответствующее отображение для выбранного типа объекта из
mapRepByOT:val sRep = mapRepByOT.getOrElse(Btk_ObjectTypeApi().getMnemoCode(idvOT), throw AppException("..."))
Сформировать выборку:
sSelectionAttr— выборка, отображение которой присоединяется. В рассматриваемом случае все отображения принадлежат одной и той же выборке;sRepresentationAttr— соответствующее отображение;sCaptionAttr— описание отображения;nImageIndexAttr— картинка отображения;jsonParams— JSON-параметры.
Смена отображения по значению поля#
Если нужно менять отображения в зависимости от значения полей отображения Card, сделайте промежуточное отображение с механикой, описанной выше, например List_Tab, и присоедините его к Card.
Фиксация ширины столбца или поля#
В карточке.
Задайте в теге card свойства для атрибута:
<attr name="sRegNum" caption="Рег.№" editorType="edit" order="10" isReadOnly="true">
<card controlWidth="30" isControlWidthFixed="true"/>
</attr>
В списке.
Задайте свойства атрибута в теге grid:
<attr name="sRegNum" caption="Рег.№" editorType="edit" order="10">
<grid controlWidth="30" isControlWidthFixed="true"/>
</attr>
controlWidth— ширина поля;isControlWidthFixed:true— ширина будет зафиксирована;false— ширина доступна для редактирования в интерфейсе.
Сортировка данных в гридах#
Общие сведения#
Сортировка в гриде определяет порядок отображения записей в списке. Разработчик может задать сортировку в коде, разрешить или запретить пользователю менять порядок строк, а также изменить способ сравнения значений для отдельных столбцов.
Сортировка может выполняться:
по возрастанию — от меньшего к большему, например от
AдоZили от1до100;по убыванию — от большего к меньшему, например от
ZдоAили от100до1.
В этом разделе описано:
как задать сортировку по умолчанию;
как пользователь управляет сортировкой в интерфейсе;
как работает многоуровневая сортировка;
как снять сортировку со столбца;
как сортировка отображается визуально;
как ограничить сортировку по столбцам;
как изменить поведение сортировки для строковых полей с числовыми значениями.
Настройка сортировки по умолчанию#
Разработчик может задать порядок сортировки в зависимости от того, как формируется список: реляционным SQL-запросом или объектным запросом.
Сортировка в реляционном запросе#
Если список формируется SQL-запросом, порядок строк задавайте через ORDER BY.
override protected def onRefresh: Recs = {
s"""
SELECT *
FROM Pm_ExpectedPay t
ORDER BY t.sNumDoc
"""
}
Сортировка в объектном запросе#
Если список формируется объектным запросом, отсортируйте коллекцию после получения данных.
override protected def onRefresh: Recs = {
val rops = new OQuery(Pm_ExpPayRegAta.Type) {
where(t.id in idavExpPayRegToLoad)
}.toSeq
rops.sortBy(_.get(_.dPlanPay))
}
Управление сортировкой в интерфейсе#
Пользователь может изменять порядок сортировки прямо в гриде. Сортировка включается кликом по заголовку колонки.
Если сортировка по колонке еще не установлена, обычный клик включает сортировку по возрастанию. Повторный клик по отсортированной колонке меняет направление сортировки.
Действие |
Результат |
|---|---|
|
Включает одиночную сортировку по возрастанию. Если ранее была настроена сортировка по другим колонкам, она сбрасывается |
|
Меняет направление сортировки |
|
Включает многоуровневую сортировку или добавляет колонку в текущую многоуровневую сортировку |
|
Меняет направление сортировки по этой колонке, но не меняет ее приоритет |
|
Отключает сортировку по этой колонке |
|
Не изменяет сортировку |
|
Отключает сортировку по колонке. Действие аналогично |
После настройки значение сохраняется в свойстве selection.sortOrder.
selection.sortOrder = "DPLANPAY Asc,IDSTATEHL Desc"
Подробнее о формате значения смотрите в документации сервера приложений: CoreSelection.sortOrder.
Многоуровневая сортировка#
Многоуровневая сортировка позволяет упорядочивать данные сразу по нескольким колонкам.
Чтобы добавить колонку в многоуровневую сортировку, используйте Shift + ЛКМ по заголовку колонки. Колонка, добавленная раньше, имеет более высокий приоритет сортировки, чем колонки, добавленные после нее.
Если по нескольким колонкам включена сортировка, в заголовках колонок отображаются иконки сортировки с порядковыми номерами. Номер показывает приоритет сортировки:
меньший номер означает более высокий приоритет;
колонка, добавленная первой, имеет больший приоритет;
колонка, добавленная позже, применяется после колонок с более высоким приоритетом.
При Shift + ЛКМ по колонке, которая уже участвует в многоуровневой сортировке, меняется только направление сортировки. Порядковый номер и приоритет этой колонки не изменяются.
Снятие сортировки#
Чтобы отключить сортировку по конкретной колонке, используйте Ctrl + ЛКМ по ее заголовку.
Если колонка участвует в многоуровневой сортировке, сортировка снимается только с этой колонки. Остальные отсортированные колонки сохраняются, а их порядковые номера смещаются.
Если после снятия сортировки остается только одна отсортированная колонка, ее порядковый номер не отображается.
Если выполнен обычный клик по неотсортированной колонке, вся предыдущая сортировка сбрасывается, и включается одиночная сортировка по выбранной колонке.
Визуальное отображение сортировки#
Состояние сортировки отображается в заголовке колонки.
Визуальные признаки сортировки:
иконка направления показывает, что колонка отсортирована;
направление иконки показывает порядок сортировки: по возрастанию или по убыванию;
при многоуровневой сортировке рядом с иконкой отображается порядковый номер;
порядковый номер показывает приоритет сортировки между колонками.
Сортировка в области группировки#
Область группировки поддерживает сортировку аналогично основному гриду, включая многоуровневую сортировку.
В области группировки доступны следующие действия:
Действие |
Результат |
|---|---|
|
Включает сортировку |
Повторный |
Меняет направление сортировки |
|
Отключает сортировку |
|
Используется для многоуровневой сортировки |
Приоритет сортировки в области группировки определяется иерархией группировки: колонка, расположенная левее, имеет более высокий приоритет, чем колонка, расположенная правее.
Сортировка на уровне группировки является более приоритетной. Сортировка на уровне грида применяется после сортировки, заданной в области группировки.
Программная установка порядка сортировки#
Если список должен открываться уже отсортированным, задайте значение selection.sortOrder до выполнения onRefresh().
override def beforeFirstOpen(): Unit = {
super.beforeFirstOpen()
selection.sortOrder = "sNumDoc ASC, dPlanPay DESC"
}
Система может автоматически применять сортировку из selection.sortOrder и для частично прогружаемых выборок. Для этого используется макрос SortOrder#.
Получить значение макроса можно так:
selection.getMacro("SortOrder#")
Например, для значения:
selection.sortOrder = "sNumDoc DESC"
макрос будет содержать:
ORDER BY SNUMDOC DESC NULLS LAST
Чтобы заполнение макроса и его автоматическая подстановка в запрос работали, для отображения в AVM необходимо включить свойство applySrvSortOrderMacros.
<representation name="List" editMode="notEdit" caption="Заявки на платеж" applySrvSortOrderMacros="true">
<!-- ... -->
</representation>
По умолчанию в DVM значение этого свойства равно false.
В реляционных запросах конструкция с использованием макроса автоматически добавляется к запросу из selectStatement(). В объектных запросах, которые используют SQL при формировании объектов, значение макроса можно подставить вручную.
Подробнее смотрите в описании свойства Representation.applySrvSortOrderMacros.
Сброс сортировки#
Чтобы программно сбросить текущую сортировку, установите null.
selection.sortOrder = null
Ограничение сортировки по столбцам#
Для каждого атрибута можно отдельно определить, разрешена ли сортировка. Настройка выполняется через свойство sortMode в метаданных.
<attr name="sNumDoc" caption="Номер" isVisible="true" order="10">
<sort sortMode="yes"/>
</attr>
<attr name="dPlanPay" caption="План.дата" isVisible="true" order="20">
<sort sortMode="no"/>
</attr>
<attr name="idStateHL" caption="Состояние" isVisible="true" order="30">
<sort sortMode="askUser"/>
</attr>
Свойство sortMode поддерживает следующие значения:
Значение |
Поведение |
|---|---|
|
Сортировка разрешена |
|
Сортировка по столбцу запрещена |
|
При попытке сортировки система запрашивает подтверждение |
Поведение каждого значения:
При
sortMode="yes"пользователь может отсортировать данные по столбцу без дополнительных ограничений.
При
sortMode="no"система запрещает сортировку по столбцу и выводит предупреждение.
При
sortMode="askUser"система перед сортировкой запрашивает подтверждение.
Отключить сортировку сразу для всей таблицы нельзя. Ограничение настраивается отдельно для каждого столбца.
Во время работы формы значение sortMode можно переопределить через setMetaProp():
override def beforeOpen(): Unit = {
super.beforeOpen()
setMetaProp("sNumDoc", "View.Representation.Attributes.Attribute.Sort.sortMode", "asYes")
setMetaProp("dPlanPay", "View.Representation.Attributes.Attribute.Sort.sortMode", "asNo")
setMetaProp("idStateHL", "View.Representation.Attributes.Attribute.Sort.sortMode", "asAskUser")
}
Подробнее смотрите в справке по свойствам Sort и перечислению SortModes.
Переопределение способа сортировки#
В AVM можно изменить не только доступность сортировки, но и сам способ сравнения значений. Для этого используйте атрибут applySortAsNumber в теге grid.
Если атрибут имеет строковый тип, но хранит числовые значения, стандартная сортировка будет лексикографической. Например, значения будут упорядочены так: 1, 10, 11, 2, 20. Чтобы сортировать такие значения как числа, включите applySortAsNumber="true".
<attr name="sRegNum" caption="№" editorType="edit" order="10">
<grid applySortAsNumber="true"/>
</attr>
Свойство applySortAsNumber поддерживает следующие значения:
Значение |
Поведение |
|---|---|
|
Система сравнивает значения как числа |
|
Система сравнивает значения как строки |
applySortAsNumber работает только в представлениях списка и применяется только при полной загрузке записей. В карточке сортировка не используется.
Для преобразования используются строковые представления чисел, соответствующие лексической грамматике Java для чисел с плавающей запятой. Поддерживаются, например, такие значения:
123;123.45;0.123;.123;1e-6;+5.0;-2.5E10;Infinity;NaN.
Не поддерживаются, например, такие значения:
пустая строка;
12,3;1 2 3;abc;1.2.3;10 euros.
При включенном applySortAsNumber сортировка выполняется на сервере приложений: система загружает данные из базы данных, преобразует строковые значения в число, после чего сравнивает их и формирует итоговый порядок строк. При этом в SQL-запрос не добавляется ORDER BY. Направление сортировки ASC или DESC определяется по действиям пользователя или по исходным параметрам выборки.
Используйте этот режим с учетом ограничений:
он корректно работает только для полностью загруженных списков;
при частичной загрузке преобразование в число не выполняется;
при частичной загрузке ошибки не возникает;
если список загружен не полностью, поведение
applySortAsNumberне применяется, и специального уведомления система не выводит;на больших объемах данных сортировка может увеличить время отклика и потребление памяти сервера приложений.
Рекомендации по выбору способа сортировки#
В зависимости от задачи рекомендуется использовать следующие способы настройки сортировки:
Сценарий |
Рекомендуемый способ |
|---|---|
Нужно открыть список в заранее заданном порядке |
|
Список формируется SQL-запросом |
|
Список формируется объектным запросом |
Сортировка коллекции в коде |
Нужно запретить сортировку по столбцу |
|
Нужно запрашивать подтверждение перед сортировкой |
|
Строковое поле содержит числа |
|
Скрытие описания поля#
Чтобы убрать заголовок у поля, писать <attr caption=""> неправильно.
Необходимо задать свойство labelPosition:
<attr name="idTankerHL" caption="Танкер" order="120.1">
<editor labelPosition="none"/>
</attr>
Помимо значения none свойство labelPosition имеет значения:
right— справа;left— слева;above— сверху;under— снизу.
Стилизация ячейки в списке#
Для этого необходимо иметь дополнительный атрибут, в который будет записываться системное имя стиля. Обычно такой атрибут называют sStyle.
Далее необходимо указать, что столбец такого-то атрибута имеет стиль, описанный в атрибуте sStyle:
<attr name="nRating" order="20" caption="Разряд">
<style attr="sStyle"/>
</attr>
Стили можно посмотреть и создать в Настройках систем:

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

Операции на панели инструментов описываются в коде с помощью аннотации @Oper (Параметры аннотации). В аннотации задаются основные параметры кнопки: заголовок, описание и горячая клавиша.
Пример:
@Oper(caption = "-", description = "Сделать что-то", hotKey = "Enter")
Настройка подсказок в полях#

Для отображения всплывающих подсказок используется механизм парных атрибутов. Он настраивается в метаданных фрейма и заполняется в SQL-запросе, который формирует набор данных для отображения.
Используются два свойства:
commentAttribute— атрибут, который определяет наличие маркера;commentTextAttribute— атрибут с текстом подсказки.
Подробнее о работе атрибутов см. в описании свойств фрейма.
Пример:
<frame header.isCaptionVisible="true" toolBar.isCaptionsVisible="true">
<grid isMultilineRow="true"
commentAttribute="sAttrNote"
commentTextAttribute="sAttrNoteText"/>
</frame>
Пример формирования текста подсказки:
CASE
WHEN t.idParent IS NOT NULL THEN CAST(NULL AS VARCHAR)
WHEN t.jObjAttrs_dz ->> '${Zsngpro_DataInstallPkg().sFunctionalityAttrName}' IS NULL
THEN 'nImage|функциональность не сопоставлена'
WHEN EXISTS (${Zsngpro_FunctionalityApi().getBHasActionGst("t.id")})
THEN 'nImage|функциональность сопоставлена'
ELSE 'nImage|новая функциональность'
END AS sAttrNoteText
Пример формирования подсказок для нескольких атрибутов:
'nImageType|' || COALESCE(
(SELECT tt.sHeadLine_dz FROM Bpm_TaskType tt WHERE t.idTaskType = tt.id), ''
) || ';' ||
'nImportanceImage|' || COALESCE(
(SELECT tc.sHeadLine_dz FROM Bpm_TaskCategories tc WHERE t.idTaskCategory = tc.id), ''
) AS sAttrNoteText_dz