Практика Avi#

Про selectStatement и onRefreshExt#

Изначально результат выборки формируется операцией onRefresh или onRefreshItem одним из двух способов: реляционный или объектный запрос.

Реляционный запрос#

В этом случае в теле onRefresh вызывается метод selectStatement или prepareSelectStatement (вызывает тот же selectStatement, но с добавлением фильтров, описанных в avm). Иначе говоря, результатом операции onRefresh является текстовое значение, внутри которого реляционный запрос.

Примечание

onRefreshExt в случае формирования выборки реляционным запросом не вызывается!

Так формируется по умолчанию отображение List (кроме коллекций). Не учитываются значения хранящиеся в кэше, учитываются значения только из БД.

Пример selectStatement для отображения List:

override protected def selectStatement: String = {
  s"""SELECT
    t.id  
    ,t.idClass  
    ,t.idState  
    ,t1.sHeadLine_dz as idStateHL  
    ,t.idStateMC  
    ,t.idObjectType  
    ,t2.sHeadLine_dz as idObjectTypeHL  
    ,t.idDepOwner  
    ,t3.sHeadLine_dz as idDepOwnerHL  
    ,t.gidSrc  
    ,t.idResourceHolder  
    ,t4.sHeadLine_dz as idResourceHolderHL  
    ,t.sRegNum  
    ,t.dReg  
    ,t.idPeriod  
    ,t5.sHeadLine_dz as idPeriodHL  
    ,t.sDescription  
    ,t.gid  
    ,t.sRegNum_dz  
    ,t.sRegNumBMs_dz  
    ,t.sRegNumidVer_dz  
    FROM Oil_DemandMov t
      LEFT JOIN  Btk_ClassState t1 on t.idState = t1.id
      LEFT JOIN  Btk_ObjectType t2 on t.idObjectType = t2.id
      LEFT JOIN  Bs_DepOwner t3 on t.idDepOwner = t3.id
      LEFT JOIN  Bs_Contras t4 on t.idResourceHolder = t4.id
      LEFT JOIN  Bs_Period t5 on t.idPeriod = t5.id
  """
}

Коммит в БД при выполнении реляционного запроса и @FlushBefore#

Важно понимать, что при реляционном запросе сервер приложения делает коммит в БД (особенность работы сервера приложения). Поэтому на выборках, где вводятся значения в поля и поддерживается возможность пользователю отменить изменения, нельзя использовать формирование выборки на реляционном запросе. Иначе данные запишутся в БД без разрешения пользователя (без нажатия пользователем на операцию сохранения (дискетка)).

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

В таком случае перед onRefresh необходимо добавить аннотоцию @FlushBefore(mode = FlushBeforeMode.Disabled):

trait Lookup extends Default with super.Lookup {

  @FlushBefore(mode = FlushBeforeMode.Disabled)
  override protected def onRefresh: Recs = {
    s"""SELECT
        t.id
        ,t.sHeadLine_dz as sHeadLine
        ,t.sMnemoCode_dz as sMnemoCode
        ,coalesce(t.sMnemoCode_dz, '') || ' ' || coalesce(t.sHeadLine_dz, '') as sMnemoCodeHeadLine
      from AsfEqp_IntoOperExt t
      order by upper(t.sHeadLine_dz)
    """
  }
}

Иначе при открытии выпадающего списка данные будут записываться в БД.

Объектный запрос#

В этом случае onRefresh формируется из экземпляров объектов SRop (инструменты refreshByParent, load, TxIndex, OQuery) или case class’ов.

В этом случае учитываются значения, хранящиеся в кэше.

Примеры:

По умолчанию отображение List у коллекций формируется с помощью refreshByParent:

trait List_idPlanFactoryShip extends Default with super.List_Master {

  override protected def onRefresh: Recs = {
    Oil_PlanFactShipSegrGroupApi().refreshByParent(getIdMaster)
  }
}

По умолчанию отображение Card у коллекций формируется с помощью load:

trait Card extends Default with super.Card {

  override protected def onRefreshItem: Recs = {
    Oil_PlanFactShipSegrGroupApi().load(getVar(getPKFieldName).asNLong)
  }
}

onRefreshExt#

В случае объектного запроса сервер сам вызывает метод onRefreshExt, в который можно дописать получение нехранимых полей на синтаксисе SQL, НЕ учитывая значения из кэша:

override protected def onRefreshExt: String = {
  s"""with t as ( select 
   :id as id
  ,:idPlanFactoryShip as idPlanFactoryShip
  ,:idSegregationGroup as idSegregationGroup
)
SELECT
   t.id
  ,t1.sHeadLine_dz as idPlanFactoryShipHL
  ,t2.sHeadLine_dz as idSegregationGroupHL
  ,t2.sMnemoCode_dz as idSegregationGroupMC
FROM t
  LEFT JOIN  Oil_PlanFactoryShip t1 on t.idPlanFactoryShip = t1.id
  LEFT JOIN  Oil_SegregationGroup t2 on t.idSegregationGroup = t2.id
"""
}

Примечание

Также существует другой способ реализовать нехранимые поля (через case class AdditionalInfo), который будет описан дальше. Данный способ формирует значения с учётом кэша.

Добавление нехранимых полей#

Здесь будет рассмотрено 2 случая, как реализовать нехранимые поля в выборке, реализованные реляционным запросом и объектным.

Для формирования нехранимого поля нужно:

  • получить поле в запросе под заданным псевдонимом

  • описать поле в разметке avm под тем же псевдонимом

Формировать реляционно или объектно#

Если нехранимый атрибут вычисляется динамически от изменений на выборке, это значит, что работа идёт с значениями из кэша, т.е. с значениями, которые ещё не закоммичены в БД. Это означает, что нехранимое поле реализовывать нужно на объектном запросе.

Например: в карточке документа нужно посчитать сумму по всем записям в коллекции. Если поле суммы реализовать реляционно, то оно будет формироваться только из согласованных в БД значений. Это значит, для того чтобы учитывались новые записи в коллекции, их нужно закоммитить (сохранить) в БД. Если же поле реализовать объектно, то в нём будут учитываться данные из кэша, т.е. те самые созданные новые записи в коллекции, которые ещё не закоммичены в БД.

Нехранимые поля в реляционном запросе#

Примечание

Не учитывает значения из кэша, которые не закоммичены в БД.

Чтобы добавить не хранимый атрибут реляционно, нужно иметь значение атрибута в выборке запроса selectStatement.

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

Dvi:

    override protected def selectStatement: String = {
      s"""SELECT
    t.id  
    ,t.idClass  
    ,t.idObjectType  
    ,t1.sHeadLine_dz as idObjectTypeHL   --нехранимое поле
    ,t.dReg  
    ,t.sRegNum  
    ,t.idState  
    ,t2.sHeadLine_dz as idStateHL    --нехранимое поле 
    FROM Rzd_Train t
      LEFT JOIN  Btk_ObjectType t1 on t.idObjectType = t1.id
      LEFT JOIN  Btk_ClassState t2 on t.idState = t2.id
    """

dvm:

<representation name="Default">
    <attributes>
        <attr name="idObjectType" caption="Тип документа" isVisible="false" editorType="edit" order="10" isRequired="true"/>
        <attr name="idObjectTypeHL" caption="Тип документа" order="10.2" isRequired="true">
            <editor>
                <buttonsEdit canEditText="true" changeableAttr="idObjectType">
                    <buttons lookup="true" openCard="true" reset="true"/>
                </buttonsEdit>
            </editor>
            <ref class="Btk_ObjectType"/>
        </attr>
        <attr name="idState" caption="Состояние" isVisible="false" editorType="edit" order="40" isRequired="true"/>
        <attr name="idStateHL" caption="Состояние" order="40.2" isRequired="true">
            <editor>
                <lookup changeableAttr="idState" isLookupLazyLoad="true" lookupKeyAttr="id" lookupListAttr="sHeadLine" lookupQuery="gtk-ru.bitec.app.btk.Btk_ClassStateAvi#Lookup_Class"/>
            </editor>
            <ref class="Btk_ClassState"/>
            <grid columnWidth="12"/>
        </attr>
    </attributes>
</representation>

Примечание

Напомню, что Dvi и dvm - это результат работы кодогенератора по данным описанным в разметке odm. Разработчик работает в соответствующих файлах Avi и avm, переопределяя содержимое Dvi и dvm.

ВНИМАНИЕ! Изменять Dvi и dvm нельзя.

А точнее бесполезно, потому что изменения будут перетёрты при следующем запуске кодогенератора.

Нехранимые поля в объектном запросе#

Примечание

Учитывает значения из кэша, которые не закоммичены в БД.

onRefreshExt#

Данный метод вызывается, если результатом onRefresh является набор объектов, а не текст, представляющий реляционный запрос.

С помощью onRefreshExt в отображении Card получены хэдлайны в Dvi:

    override protected def onRefreshExt: String = {
      s"""with t as ( select 
       :id as id
      ,:idObjectType as idObjectType
      ,:idState as idState
      ,:gidSrc/*@NString*/ as gidSrc
    )
    SELECT
       t.id
      ,t1.sHeadLine_dz as idObjectTypeHL
      ,t2.sHeadLine_dz as idStateHL
      ,t3.sHeadLine as gidSrcHL
    FROM t
      LEFT JOIN  Btk_ObjectType t1 on t.idObjectType = t1.id
      LEFT JOIN  Btk_ClassState t2 on t.idState = t2.id
      LEFT JOIN  Btk_Object t3 on t.gidSrc = t3.gidRef
    """
    }

Здесь используется синтаксис postgresql.

Применяется конструкция with (ссылка).

Через ":" подставляются текущие значения атрибутов с выборки (в том числе из кэша). Так, например, ":id as id" означает, что с выборки будет получено текущее значение атрибута id и подставлено в таблицу t под псевдонимом поля id. Теперь обращение в основном запросе t.id будет возвращать текущее значеине id.

Примечание

Под фразой «текущее значение» понимается значение на момент обновления выборки и выполнения метода onRefreshExt.

Примечание

Описание в dvm не меняется в зависимости от метода получения значения нехранимого поля (реляционного или объектного) и останется таким же, как в случае формирования нехранимого поля в selectStatement.

case class AdditionalInfo#

Нехранимое поле можно сформировать, переопределив onRefresh и onRefreshItem.

В этом случае объект представляют как кортеж (SRop, экземпляр case class’а). Тем самым объект имеет поля записи, описанных в БД (хранимые поля), и поля case class’а (нехранимые).

В этом случае принято называть case class AdditionalInfo, а заполнение описывать в методе getAdditionalInfo.

Нехранимое поле в отображении Card:

case class AdditionalInfo(
                           var nQtyLoadRecievActs: NNumber,
                           var nQtyLoadTransferCertificate: NNumber,
                           var nQtyLoadTransferCertificateAccounted: NNumber,
                           var nQtyRemains: NNumber,
                           var nQtyReserv: NNumber
                         )

protected def getAdditionalInfo(rop: Oil_ExternalMovApi#ApiRop): AdditionalInfo = {
  AdditionalInfo(
    nQtyLoadRecievActs = None.nn
    , nQtyLoadTransferCertificate = None.nn
    , nQtyLoadTransferCertificateAccounted = None.nn
    , nQtyRemains = None.nn
    , nQtyReserv = None.nn
  )
}

override protected def onRefresh: Recs = {
  val rop = thisApi().load(getVar(CardRep.IdItemSharp).asNLong)
  (rop, getAdditionalInfo(rop))
}

override protected def onRefreshItem: Recs = {
  val rop = thisApi().load(getVar(getPKFieldName).asNLong)
  (rop, getAdditionalInfo(rop))
}

Примечание

Переменные внутри case class необходимо указывать var для Avi

Нехранимое поле в отображении List_idBrigade коллекции:

trait List_idBrigade extends Default with super.List_idBrigade {

  case class AdditionalInfo(var sEmpId: NString = None.ns,
                            var sPosition: NString = None.ns)

  def getAdditionalInfo(rop: Bs_BrigadeStaffApi#ApiRop): AdditionalInfo = {
    if (rop.get(_.idEmployee).isNotNull) {
      val avEmploee = Bs_EmployeeApi().load(rop.get(_.idEmployee)).copyAro()
      AdditionalInfo(
        sEmpId = avEmploee.sEmpId,
        sPosition = avEmploee.sPosition)
    } else AdditionalInfo()
  }

  override protected def onRefresh: Recs = {
    Bs_BrigadeStaffApi().refreshByParent(getVarWithDep("super$id").asNLong)
      .map(rop => {
        (rop, getAdditionalInfo(rop))
      })
  }
}

Такой способ самый универсальный.

Например, с помощью onRefreshExt не получится посчитать сумму по всем записям коллекции с учётом кэша (для этого необходимо присоединить таблицу коллекции по условию idparent = t.id, что обеспечит учёт только тех записей и их значений, которые закоммичены в БД, т.е. без учёта кэша).

В рассматриваемой реализации, можно получить все записи коллекции с учётом кэша с помощью метода refreshByParent(rop.get(_.id)) и далее в обходчике сложить необходимые значения полей.

Нехранимые строки#

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

Пример в проекте pgDev: ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay.

case class Row#

По сколку поля строки редактируемые, то не нужно использовать реляционный запрос для формирования выборки. Выборка будет формироваться экземплярами case class’а. Таким образом onRefresh вернёт список экземпляров case class’а.

В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.

Case class должен иметь такие же поля, как поля класса (таблицы), описанные в odm и нехранимые, формируемые не реляционно.

При добавлении нового хранимого поля, необходимо это поле добавить в case class.

/**
 * Представление строки, поля соответствуют хранимым полям строки
 *
 * Полем id выступает idFlyOverPos, т.к. для работы некоторых инструментов (например, onRefreshExt) необходимо
 * наличие уникального индификатора
 */
case class Row(
                var id: NLong //Oil_RecievTaskDet.idFlyOverPos
                , var gid: NGid = None.ng
                , var idRecievTask: NLong
                , var nRow: NNumber
                , var idWagon: NLong = None.nl
                , var idRailwayInvoice: NLong = None.nl
                , var bUnloaded: NNumber = None.nn
                , var bReFeed: NNumber = None.nn
                , var idReFeedType: NLong = None.nl
                , var bWagonRemoved: NNumber = None.nn
                , var bRequiredMeasure: NNumber = None.nn
                , var nQtyMeasure: NNumber = None.nn
                , var idStorageTank: NLong = None.nl
                , var sCertificateList: NString = None.ns
                , var idRecievAct: NLong = None.nl
                , var bRepeatedFeed: NNumber = None.nn
                , var bCommAct: NNumber = None.nn
                , var bRecievActCreated: NNumber = None.nn
                , var idFlyOverPos: NLong = None.nl
                , var idLockDeviceOut: NLong = None.nl
                , var idRecievTaskDet: NLong = None.nl //Oil_RecievTaskDet.id
                , var idTrain: NLong = None.nl
                , var bRecievByMeasure: NNumber = None.nn
              )

onRefresh#

ru.bitec.app.oil.Oil_RecievTaskDetAvi.List_idRecievTaskByFlyOverWay#onRefresh.

Описание алгоритма:

  • Получаем все записи данной коллекции по мастеру (т.е. те, что хранятся в БД или в кэше).

    RecievTaskDetApi().refreshByParent(getIdMaster)
    
  • Получаем все вагоны выбранной у мастера «Пути эстакады».

    val ropParent = Oil_RecievTaskApi().load(selection.master.getSelfVar("id").asNLong)
    Oil_FlyOverPosApi().txidFlyOverWay.refreshByKey(ropParent.get(_.idFlyOverWay))
    

    Примечание

    В примере в коллекции «Подача» имеет столько строк, сколько вагонов указано в записи «Путь эстакады», на который ссылается мастер-документ.

  • Реализуем обходчик по каждому вагону и заполняем экземпляры case class’а

    • Если на данный вагон есть запись из таблицы БД, то заполняем данными из БД

    • Иначе устанавливаем предзаполненные значения необходимых полей или None

    ropaFOP.map(ropFOP => {
      //поиск ропы в БД
      val ropOpt = ropa.find(_.get(_.idFlyOverPos) === ropFOP.get(_.id)) 
      getRowByRop(ropOpt, ropFOP, nvRow)
    })
    
    
    protected def getRowByRop(ropOpt: Option[SRop[_ <: JLong, _ <: Oil_RecievTaskDetAro]]
                              , ropFOP: Oil_FlyOverPosApi#ApiRop
                              , npRow: NNumber = None.nn
                            ): Row = {
      Row(
    idPlacement = ropOpt.map(_.get(_.idPlacement)).nl
      .nvl(thisApi().getidPlacementByFlyOverPos(ropFOP.get(_.id)))
    ...)
    }
    

    При добавлении нового хранимого атрибута, необходимо в этом методе заполнения описать правило заполнения нового поля.

    На данном этапе предположим, что id заполняется от ropOpt (реальной записи), в случае отсутствия None.nl. Т.е. пустая строка имеет id = None.nl, что дальше будет использоваться, как признак пустой строки.

    Примечание

    Внимание: такая реализация не совсем корректна. В таком случае не будет работать ряд системных операций, таких, как onRefreshExt, которые требуют уникального значения id. Но для понимания будет рассмотрена такая реализация, а вопрос уникальности id будет рассмотрен далее.

    Row case class, представление строки, поля соответствуют хранимым полям записи.

  • Возвращаем полученный список экземпляров case class’а.

insert при вводе значения в нехранимую строку#

Необходимо переопределить операцию beforeEdit().

override def beforeEdit(): Unit = {
  		if (getSelfVar("id").isNull) {
    			regRow()
  		}
}

beforeEdit() вызывается при каждой попытке редактировать поле. Здесь необходимо инициализировать, что заполнение введётся в пустой нехранимой строке или в хранимой. Это можно сделать, проверив заполнен ли id (на данном этапе предполагается, что у пустой строки id = None.nl).

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

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

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

  • Реализовать обходчик по всем имеющимся полям case class’a, отсечь поля, которые не имеют сеттеров (gid, например) и значение которых null, по оставшимся полям вызывать сеттеры и проставлять соответствующие значения:

/**
 * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
 *
 * @param ropParent
 * @param row
 * @return
 */
def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
  insertByParent(ropParent) :/ { rop =>
    //setter'ы для переноса умолчательных значений из Row
    row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to)
      .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
      .foreach(field => {
          setAttrValue(rop, field._1, field._2)
      })
    rop
  }
}

Чтобы введенное пользователем значение относилось к только что созданной записи, необходимо принудительно после регистрации задать id на выборке:

val rop = thisApi().registerByRow(thisRow())
setVar("id", rop.get(_.id))

afterEdit()#

Также необходимо добавить проверку, является ли строка пустой, в afterEdit().

Иначе load() внутри afterEdit() будет вызван по некорректного id пустой строки и будет вызвана ошибка.

Поддержание уникального id в нехранимых полях.#

Без уникального id ряд системных операций не будет работать или будет работать некорректно, в том числе для работы onRefreshExt.

Необходимо понять, что на выборке является уникальным, в случае примера это ссылка соответствующая вагону: idFlyOverPos .

Теперь параметр id на выборке будет иметь не значение null в случае пустой строки и значение записи коллекции из БД, а значение idFlyOverPos.

А сам id записи коллекции будет храниться в поле case class’a idRecievTaskDet Это нужно учесть в:

  • Структуре case class’а

  • В переносе значений из записи из БД в case class для формирования onRefresh

  • В условии по признаку пустая ли строка (теперь строка пустая, если idRecievTaskDet === None.nl):

override def beforeEdit(): Unit = {
  if (getSelfVar("idRecievTaskDet").isNull) {
    regRow()
  }
}
  • При сеттере в поле пустой строки в методе переноса умолчательных значений:

    /**
    * insertByParent(ropParent) + сеттеры заполненных полей case class'а Row
    *
    * @param ropParent
    * @param row
    * @return
    */
    def insertByParentAndRow(ropParent: Oil_RecievTaskApi#ApiRop, row: Row): ApiRop = {
      insertByParent(ropParent) :/ { rop =>
        //setter'ы для переноса умолчательных значений из Row
        row.getClass.getDeclaredFields.map(_.getName.ns).zip(row.productIterator.to(scala.collection.immutable.IndexedSeq))
          .filterNot(field => saFieldsNotSetter.contains(field._1) || field._2.asInstanceOf[Nullable[_ <: Any, _]].isNull)
          .foreach(field => {
            if (field._1 == sid.ns) {
              setAttrValue(rop, sidFlyOverPos, field._2)
            } else {
              setAttrValue(rop, field._1, field._2)
            }
          })
        rop
      }
    }
    
  • При сеттере в поле пустой строки после создания записи проставить параметр выборки idRecievTaskDet, заместо id, в id только что созданной записи:

    val rop = thisApi().registerByRow(thisRow())
    setVar("idRecievTaskDet", rop.get(_.id))
    
  • При взятии параметра у дочерних выборок по super$id -> super$idRecievTaskDet

  • В CWA управление свойством isEnabled у операций:

    selection.opers().setEnabled("Delete",selection.canDelete && selection.getSelfVar("idRecievTaskDet").notNull())
    
  • Удаление:

    thisApi().delete(thisApi().load(getSelfVar("idRecievTaskDet").asNLong))
    
  • thisRop()

    thisApi().load(getSelfVar("idRecievTaskDet").asJLong)
    
  • onInvalidateItem():

    override protected def onInvalidateItem(): Unit = {
      if (!getSelfVar(thisApi().sidRecievTaskDet).isNull && thisRop() != null) {
        session.invalidateObject(thisRop())
      }
    }
    
  • учесть в onRefreshExt() в тексте запроса

Динамическое присоединение столбцов#

Если такое отображение только для чтения и результат не использует значения, которые могут быть в кэше, тогда можно реализовать на реляционном запросе, иначе необходимо использовать инструменты DynMetaBuilder и DynRecBuilder.

Реляционная реализация#

В этом случае необходимо собрать текст запроса selectStatement.

Ниже пример формирования одного из столбца:

s"""
,(select string_agg(cast(t.id as varchar), ', ')
  from Oil_Task t
  where t.idResource  = ${rv.idResource()}
    and (t.dBegin < $sMainFromTab.dEndTime and t.dExec > $sMainFromTab.dBegTime)
    and t.idObjectType = :${Oil_TaskMonitorPkg().sfltidObjectType}
    and t.idStateMC >= 200
    and t.idDepOwner = :super$$idGlobalDepOwner
  group by t.idResource
  ) as \"$sNameFieldForGST[${rv.id()}]\"
"""

Пример текста после подстановки значения:

(select string_agg(cast(t.id as varchar), ', ')
  from Oil_Task t
  where t.idResource  = 115
    and (t.dBegin < ‘2023-08-25 12:00:00’ and t.dExec > ‘2023-08-25 14:00:00’)
    and t.idObjectType = 225
    and t.idStateMC >= 200
    and t.idDepOwner = 336
  group by t.idResource
  ) as “idFlyOverWay[13]"

Мы получим столбец с названием idFlyOverWay[13], результатом будет значение подзапроса.

DynMetaBuilder и DynRecBuilder#

Пример: ru.bitec.app.oil.Oil_TaskMonitorAvi.List_CoreResource.

Структура формирования:

Recs(<объекты, представляющие строку (список rop или case class)>)
 	.extend(<DynMetaBuilder>.build())
       .foreach((row, builder) =>  (row, <DynRecBuilder>.build))
  • Пример использования Recs:

    Recs(SomeApi().refreshByParent(ropMaster))
    

    ,где SomeApi() - какая-либо Api класса.

  • Пример использования .extend()

    Здесь формируются методанные о добавляемых столбцах: название столбца, тип данных, caption.

    val dynMetaBuilder = DynMetaBuilder()          
    dynMetaBuilder.add("idFlyOverWay[1]", classOf[String], "ПЭ-1")
    dynMetaBuilder.add("idFlyOverWay[20]", classOf[String], "ПЭ-2")
    dynMetaBuilder.add("idFlyOverWay[360]", classOf[String], "ПЭ-3")
    
    Recs(SomeApi().refreshByParent(ropMaster))
    .extend(dynMetaBuilder.build())
    

    ,где SomeApi() - какая-либо Api класса.

  • Пример использования .foreach()

    Стоит понимать, что это не привычный обходчик по коллекции, который ничего не возвращает, а отдельный метод для динамического формирования столбцов, который должен ВЕРНУТЬ результат выборки!

    В данном методе столбец получает построчно значения.

    Recs(SomeApi().refreshByParent(ropMaster))
      .extend(dynMetaBuilder.build())
      .foreach((row, builder) =>  { 
        builder.set(("idFlyOverWay[1]", row.get(_.sDiscription)))
        builder.set(("idFlyOverWay[20]", "hello, world"))
        builder.set(("idFlyOverWay[20]", None.ns))
    
        //метод должен вернуть кортеж 
        (row, builder.build)
    })
    

    ,где SomeApi() - какая-либо Api класса.

    Тут заданы различные значения для столбцов. На каждую строку row будет формироваться 3 значения для 3ёх столбцов. Столбец idFlyOverWay[1] будет в каждой строке разный, остальные же иметь одинаковое значение.

Описать столбец в разметке avm можно по имени поля без квадратных скобок.

Динамическое изменение свойств avm#

Для этого необходимо использовать следующий метод:

ru.bitec.app.gtk.gl.Rep#setMetaProp

Описание

Пример использования:

setMetaProp(attr.sSystemName.get //для какого поля меняется свойство
//какое свойство 
//в данном случае свойство задает системное имя атрибута в выборке, 
//которое хранит настройки типа редактора
, s"View.Representation.Attributes.Attribute.Editor.editorTypeAttr"меняется
, thisPkg.getSEditorAttrName(attr) //значения свойства
)

OnrefreshExt и значение даты с выборки#

В onRefreshExt нет приведения /*NDate*/, нужно писать через /*NString*/.

В onRefreshExt в конструкции with объявляются поля, которые нужно получить с выборки (из кэша). Иногда требуется указать тип данных. Тип данных указывается в /*...*/, но парсер на знает NDate, поэтому необходимо указать, как NString и далее использовать в запросе cast(... as timestamp) или as date.

override protected def onRefreshExt: String = {
  s"""with t as ( select
       :id as id
      ,:idState as idState
      ,:gidSrc/*@NString*/ as gidSrc
      ,:idObjectType as idObjectType
      ,:idDepOwner as idDepOwner
      ,:idDischargePlace as idDischargePlace
      ,:idSegregationGroup as idSegregationGroup
      ,:idService as idService
      ,:idFlyOverWay as idFlyOverWay
      ,:idStateMC as idStateMC
      ,:dPlanEnd /*@NString*/ as dPlanEnd
      ,:dPlanBegin /*@NString*/ as dPlanBegin
    )
    SELECT
       t.id
      ,to_char(cast(t.dPlanEnd as timestamp) - cast(t.dPlanBegin as timestamp), 'dd д. hh24 ч.') as nPlanBusy
      ,t1.sHeadLine_dz as idStateHL
      ,t2.sHeadLine_dz as idObjectTypeHL
      ,t3.sHeadLine_dz as idDepOwnerHL
      ,t4.sHeadLine_dz as idDischargePlaceHL
      ,t5.sMnemoCode_dz as idSegregationGroupHL
      ,t6.sHeadLine_dz as idServiceHL
      ,t7.sHeadLine as gidSrcHL
      ,t8.sHeadLine_dz as idFlyOverWayHL
    FROM t
      LEFT JOIN  Btk_ClassState t1 on t.idState = t1.id
      LEFT JOIN  Btk_ObjectType t2 on t.idObjectType = t2.id
      LEFT JOIN  Bs_DepOwner t3 on t.idDepOwner = t3.id
      LEFT JOIN  Bs_Placement t4 on t.idDischargePlace = t4.id
      LEFT JOIN  Oil_SegregationGroup t5 on t.idSegregationGroup = t5.id
      LEFT JOIN  Gds_Service t6 on t.idService = t6.id
      LEFT JOIN  Btk_Object t7 on t.gidSrc = t7.gidRef
      LEFT JOIN  Oil_FlyOverWay t8 on t.idFlyOverWay = t8.id
  """
}

Поиск отображения на выборке#

selection.form.findSelection(...)

Ссылка

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

Пример использования:

val sel = selection.form.findSelection(Bdg_ForecastAvi.card())

Пользовательская блокировка#

Пользовательская блокировка включается при взаимодействии пользователя с интерфейсом в методе Dvi#beforeEdit, который является результатом кодогенератора.

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

  • Btk_FormSessionApi().lockObject(gid)

  • Btk_FormSessionApi().lockObjectMulti(gida)

Больше про пользовательскую блокировку можно узнать здесь.

Объект класса в процессе создания и другие состояния объекта rop#

Иногда, есть необходимость в Avi знать, что запись находится в процессе создания, т.е. не имеет реализации, как запись в таблице БД.

Это можно узнать по объекту записи rop:

  • используйте метод Avi thisRop() чтобы получить объект rop, чья карточка открыта или на котором стоит фокус, если отображение list.

  • если rop.ropMode == InsertRopMode, то объект находится в процессе создания, т.е. есть в кэше приложения, но не имеет реализации в таблице БД.

Где поле объекта rop.ropMode хранит в себе информацию состояния объекта. Есть и иные состояния объекта:

  • ReadRopMode

  • UpdateRopMode

  • DeleteRopMode

  • InsertRopMode

Пример кода из проекта:

    //Документ должен быть закоммичен, чтобы были пройдены все соответсвующие проверки,
    // т.к. по закрытию card_ReadFromFile будет flush()
    if (rop.ropMode == InsertRopMode) {
      throw AppException("Для вызова операции необходимо, чтобы документ был заполенен и сохранён.")
    }

Как узнать, что выборка является главным меню или главной выборкой формы#

В системе есть 3 типа форм:

  • главная

  • модальная

  • MDI (отображается в качестве закладки на главной форме)

У каждой формы есть главная выборка. Флаг selection.isMainOnForm и указывает, что выборка - главная на форме.

Условие application.mainSelection == selection определит, является ли выборка главным меню.