4.1.6.2. Наполнение секций истории

Added in version 1.28.0.

Секции “Часто используемое” и “Недавно закрытые” Навигационного хаба наполняет прикладной слой: он перехватывает действия пользователя, накапливает историю и уведомляет сервер о её изменении. Подключение самих секций через разметку описано на странице Подключение Хаба к приложению.

4.1.6.2.1. Модель ответственности

Платформа и прикладной слой делят обязанности так:

  • Платформа отображает секции, хранит пользовательские настройки секции “Закрепленное”, читает данные секций истории из прикладных выборок (selectionName контролов <tiles> с ролями popular и latest) и обновляет клиент сессии, в которой опубликовано событие истории.

  • Прикладной слой перехватывает действия пользователя (выполнение операций, закрытие выборок), накапливает и агрегирует историю (например, в таблицах решения), поддерживает актуальность данных выборок истории и публикует событие в топик NavHubHistoryEventsTopic после каждого изменения.

Выбор способа накопления и агрегации истории — зона ответственности прикладного департамента; ориентиром служит существующая прикладная библиотека телеметрии ru.bitec.app.btk.telemetry.Btk_TelemetryLib.

4.1.6.2.2. Перехват закрытия выборок

Факт использования выборки надёжнее регистрировать по событию закрытия (afterClose): к этому моменту выборка гарантированно открывалась без ошибок и пользователь взаимодействовал с ней. Перехват по событию открытия (beforeOpen) реализуется идентично.

Закрытие перехватывается переопределением метода afterClose (ru.bitec.app.gtk.gl.DefaultRep#afterClose) в выборке. Чтобы не дублировать переопределение в каждой выборке, создаётся промежуточный класс:

abstract class NavigationHubEntityAvi extends EntityAvi {

  trait NavigationHubEntityAviDefault extends EntityAviDefault {
    override protected def afterClose(): Unit = {
      super.afterClose()
      // Зарегистрировать закрытие выборки в истории текущего пользователя.
      // ...
    }
  }

}

AVI-классы выборок, участвующих в истории, наследуются от промежуточного класса:

@AvmFile(name = "Module_Document.avm.xml")
class Module_DocumentAvi extends Module_DocumentDvi with NavigationHubEntityAvi {

  trait Default extends DviDefault with NavigationHubEntityAviDefault

  trait List extends Default with DviList

  trait Card extends Default with DviCard

}

Такой подход заодно определяет область охвата: выборки, не подмешавшие NavigationHubEntityAviDefault, в истории не регистрируются.

4.1.6.2.3. Параметры открытия в истории

Чтобы история различала объекты одного класса — например, карточки разных договоров, — вместе с именем выборки при закрытии сохраняются параметры, с которыми открыта форма. В выборке тайлов истории они отдаются колонкой параметров открытия (атрибут actionParamsAttr фрейма <tiles>, см. Подключение Хаба к приложению), и клик по тайлу воспроизводит открытие.

  • Источник. selection.form.params (ru.bitec.app.gtk.gl.Form#params) — параметры, переданные форме при открытии: из URI-ссылки на выборку, из openFormEx прикладного кода или из тайла Хаба. Значения в карте не обязательно строки; в JSON они записываются строкой, числом или true/false. Ключи со значением null при сериализации отбрасываются, вложенные объекты и массивы не записываются: тайл с такими значениями не выводится.

  • Что в карту не попадает. Фильтры, выставленные пользователем в открытой форме, и переход к другой записи внутри карточки. Если для истории важна запись, на которой пользователь закончил работу, её ключ берётся из текущей строки выборки и записывается под именем параметра ключа карточки (в прикладных библиотеках обычно idItem#). Для списков, открытых без параметров, карта пуста — колонка параметров остаётся пустой.

  • Служебные ключи. Ключи, не влияющие на то, какой объект откроется (CREATINGMODE, DEFAULTEDITOR, размеры окна), в историю не записываются, иначе одна карточка даст несколько записей. EDITINGTYPE со значением Edit сохраняется. Карточки, открытые на создание или копирование, в историю не записываются: повторное открытие создало бы ещё один объект.

  • Подпись тайла. Для тайла записи подпись формируется из объекта (номер, наименование), а не из заголовка выборки, и пересчитывается при чтении выборки истории.

  • Ключ агрегации. Сервер сравнивает параметры тайлов без учёта порядка ключей. Чтобы одна карточка не расходилась в истории на несколько записей, ключ агрегации строится из имени выборки и параметров одним методом для обеих секций — например, с сортировкой ключей перед сериализацией.

  • Размер колонки. Колонка параметров должна вмещать сериализованную карту целиком: обрезанный JSON не разбирается, и тайл не выводится. Если в параметрах бывают фильтры или макросы, колонку заводят текстового типа без ограничения длины.

4.1.6.2.4. Перехват выполнения операций

Выполнение операций перехватывается обработчиком событий уровня приложения. В классе главной выборки приложения (какой класс является главным — определяется по src/main/resources/META-INF/applications.xml прикладного решения) переопределяется метод onRestoreState (ru.bitec.app.btk.gl.ApplicationAvi.ApplicationAviDefault#onRestoreState) и регистрируется обработчик на все операции:

override def onRestoreState(bundle: KeyBundle): Unit = {
  super.onRestoreState(bundle)

  val thisRep = selection.baseRep.unwrap(classOf[InternalRep])
  val idUser = Btk_UserApi().getCurrentUserID()
  val sSelectionName = selection.name + "#" + selection.representation

  val isOperProcessing = (sOperName: String) => {
    (sOperName.toUpperCase === "closeFormOk".toUpperCase).isFalse &&
      (sOperName.toUpperCase === "closeFormCancel".toUpperCase).isFalse &&
      !session.isClosed &&
      selection.coreRep != null
    // Допустимы дополнительные проверки.
  }

  thisRep.handlers.addBeforeEvent(RepEvent.allOperations, spOperName => {
    val sOperName = spOperName.toLowerCase
    if (isOperProcessing(sOperName)) {
      // Сохранить параметры выполненной операции в историю.
      // ...
    }
  })
}

Служебные операции (в примере — операции закрытия форм) исключаются из регистрации фильтром isOperProcessing.

4.1.6.2.5. Событие изменения истории

После внесения изменений в историю (или пересчёта агрегатов) прикладной код публикует событие в шину сообщений. Контракт события задаёт интерфейс ru.bitec.gtk.core.gl.workcenter.navhub.NavHubHistoryEventsTopic (модуль gtk-core — доступен и прикладному слою):

Константа

Значение

Назначение

TOPIC_NAME

WorkCenter.NavHub.History.AppEvents

Имя выделенного топика событий истории

LASTCLOSED_EVENT_NAME

EVENT_LASTCLOSED

Изменился список недавно закрытых — обновить секцию “Недавно закрытые”

POPULAR_EVENT_NAME

EVENT_POPULAR

Изменилась статистика использования — обновить секцию “Часто используемое”

Публикация выполняется асинхронно, ответ не предусмотрен:

import ru.bitec.gtk.core.gl.workcenter.navhub.NavHubHistoryEventsTopic

application.eventBus.post(
  NavHubHistoryEventsTopic.TOPIC_NAME,
  NavHubHistoryEventsTopic.LASTCLOSED_EVENT_NAME,
  Array.empty
)

Событие — только сигнал: получив его, сервер заново читает прикладные выборки истории соответствующей секции (см. Подключение Хаба к приложению) и передаёт обновлённое состояние Хаба на открытые клиенты. Аргументы события сервер не разбирает, поэтому к моменту публикации данные выборок истории уже должны отражать новое состояние. События с другими именами в этом топике сервер игнорирует.