Практика avm, примеры интерфейсов

Содержание

Практика 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:

  1. Проверить, заполнено ли значение фильтра типа объекта:

    val idvOT = getVar("flt_idObjectType").asNLong
    if (idvOT.isNotNull) {}
    
  2. Получить соответствующее отображение для выбранного типа объекта из mapRepByOT:

    val sRep = mapRepByOT.getOrElse(Btk_ObjectTypeApi().getMnemoCode(idvOT), throw AppException("..."))
    
  3. Сформировать выборку:

    • 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))
}

Управление сортировкой в интерфейсе#

Пользователь может изменять порядок сортировки прямо в гриде. Сортировка включается кликом по заголовку колонки.

Если сортировка по колонке еще не установлена, обычный клик включает сортировку по возрастанию. Повторный клик по отсортированной колонке меняет направление сортировки.

Действие

Результат

ЛКМ по неотсортированной колонке

Включает одиночную сортировку по возрастанию. Если ранее была настроена сортировка по другим колонкам, она сбрасывается

ЛКМ по отсортированной колонке

Меняет направление сортировки

Shift + ЛКМ по колонке

Включает многоуровневую сортировку или добавляет колонку в текущую многоуровневую сортировку

Shift + ЛКМ по колонке, участвующей в многоуровневой сортировке

Меняет направление сортировки по этой колонке, но не меняет ее приоритет

Ctrl + ЛКМ по отсортированной колонке

Отключает сортировку по этой колонке

Ctrl + ЛКМ по неотсортированной колонке

Не изменяет сортировку

Ctrl + Shift + ЛКМ по колонке

Отключает сортировку по колонке. Действие аналогично Ctrl + ЛКМ

После настройки значение сохраняется в свойстве selection.sortOrder.

selection.sortOrder = "DPLANPAY Asc,IDSTATEHL Desc"

Подробнее о формате значения смотрите в документации сервера приложений: CoreSelection.sortOrder.

Многоуровневая сортировка#

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

Чтобы добавить колонку в многоуровневую сортировку, используйте Shift + ЛКМ по заголовку колонки. Колонка, добавленная раньше, имеет более высокий приоритет сортировки, чем колонки, добавленные после нее.

Если по нескольким колонкам включена сортировка, в заголовках колонок отображаются иконки сортировки с порядковыми номерами. Номер показывает приоритет сортировки:

  • меньший номер означает более высокий приоритет;

  • колонка, добавленная первой, имеет больший приоритет;

  • колонка, добавленная позже, применяется после колонок с более высоким приоритетом.

При Shift + ЛКМ по колонке, которая уже участвует в многоуровневой сортировке, меняется только направление сортировки. Порядковый номер и приоритет этой колонки не изменяются.

Снятие сортировки#

Чтобы отключить сортировку по конкретной колонке, используйте Ctrl + ЛКМ по ее заголовку.

  • Если колонка участвует в многоуровневой сортировке, сортировка снимается только с этой колонки. Остальные отсортированные колонки сохраняются, а их порядковые номера смещаются.

  • Если после снятия сортировки остается только одна отсортированная колонка, ее порядковый номер не отображается.

  • Если выполнен обычный клик по неотсортированной колонке, вся предыдущая сортировка сбрасывается, и включается одиночная сортировка по выбранной колонке.

Визуальное отображение сортировки#

Состояние сортировки отображается в заголовке колонки.

Визуальные признаки сортировки:

  • иконка направления показывает, что колонка отсортирована;

  • направление иконки показывает порядок сортировки: по возрастанию или по убыванию;

  • при многоуровневой сортировке рядом с иконкой отображается порядковый номер;

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

Сортировка в области группировки#

Область группировки поддерживает сортировку аналогично основному гриду, включая многоуровневую сортировку.

В области группировки доступны следующие действия:

Действие

Результат

ЛКМ по заголовку группы

Включает сортировку

Повторный ЛКМ по отсортированному заголовку группы

Меняет направление сортировки

Ctrl + ЛКМ по заголовку группы

Отключает сортировку

Shift + ЛКМ по заголовку группы

Используется для многоуровневой сортировки

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

Сортировка на уровне группировки является более приоритетной. Сортировка на уровне грида применяется после сортировки, заданной в области группировки.

Программная установка порядка сортировки#

Если список должен открываться уже отсортированным, задайте значение 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 поддерживает следующие значения:

Значение

Поведение

yes

Сортировка разрешена

no

Сортировка по столбцу запрещена

askUser

При попытке сортировки система запрашивает подтверждение

Поведение каждого значения:

  • При 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 поддерживает следующие значения:

Значение

Поведение

true

Система сравнивает значения как числа

false

Система сравнивает значения как строки

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 не применяется, и специального уведомления система не выводит;

  • на больших объемах данных сортировка может увеличить время отклика и потребление памяти сервера приложений.

Рекомендации по выбору способа сортировки#

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

Сценарий

Рекомендуемый способ

Нужно открыть список в заранее заданном порядке

selection.sortOrder или ORDER BY

Список формируется SQL-запросом

ORDER BY

Список формируется объектным запросом

Сортировка коллекции в коде

Нужно запретить сортировку по столбцу

sortMode="no"

Нужно запрашивать подтверждение перед сортировкой

sortMode="askUser"

Строковое поле содержит числа

applySortAsNumber="true"

Скрытие описания поля#

Чтобы убрать заголовок у поля, писать <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