Практика Avi
Contents
Практика 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
определит, является ли выборка главным меню.