Хранение данных происходит с помощью типа TLinesData, являющегося record.
В TLinesData самое важное - это NodeColList, список класса TVariantList, хранит в себе значения всех колонок для конкретной строки данных.
Здесь нам важно ознакомиться и понять принцип заполнения следующих полей класса UniLines:
Для этого используется процедура Open. Она принимает в себя следующие параметры:
Учитывая все параметры по умолчанию, то для самой простой загрузки данных, без необходимости их редактирования, будет достаточно и такого:
ulf.Open(2820, null, '', RdTr);
Краткая последовательность загрузки данных такова:
MetaObject:=DMForm.MetaObjectList.MainRoot.FindObjectByTag(AMetaObject_Tag); //находится нужный метаобъект по тагу;
qQuery.SelectSQL.Text:=MetaQuery.GetFullSQL(sAddWhere, CheckManyToOneLinks,
True, False, CheckColumns, False, iCastlingTables); //формируется запрос;
qQuery.Open; //открывается датасет
FillColumns; //заполнение колонок
LoadData(null); //загрузка данных
Самое интересное происходит как раз в двух последних процедурах.
FillColumns выглядит следующим образом:
//цикл по всем метаколонкам метазапроса
for i:=0 to MetaQuery.MetaColumnsCount-1 do
begin
cl:=MetaQuery.MetaColumns[i]; //получение конкретной метаколонки
if cl.Is_LineNumber or cl.OnlyForFilters then //условия, что колонке не нужно имя
fn := ''
else
begin
//определение названия колонки
if MetaQuery.SQL='' then
fn := qQuery.Fields[j].FieldName
else
fn := cl.SQL_Select;
inc(j);
end;
//добавление колонки
if FillColumn(cl,fn) then
inc(cnt);
end;
В FillColumn из интересного происходит следующее:
col:=AddColumn; //вызывает TVirtualTreeColumnFooter(VST.Header.Columns.Add) - добавление колонки в заголовок VST
{дальше много кода по выставлению различных свойств у этой колонки}
Fields.Add(AFieldName); //добавляет название колонки в Fields
fr:=ClsAdd(Cl, col); //добавляет в список Cls "фрейм-колонку" - которая объединяет в себе ссылки на объекты колонок VST и мета-колонки
{выставление оставшихся свойств}
Таким образом после FillColumns у нас заполняется список названий колонок Fields, список колонок Cls (элементы которого являются ссылками на колонки VST и на метаколонки).
Затем вызывается LoadData, у которого из основного:
qQuery.First;
while (not qQuery.Eof) do
begin
//addNewLine по умолчанию True
if (AddNewLine) then
Dat := AddData(nil) //здесь формируется NodeColList об этом будет ниже
else
//... несущественные сейчас операции
if (Dat <> nil) then
begin
//проходимся по списку наших колонок
for i:=0 to Cls.Count-1 do
begin
//вытаскиваем ссылку на метаколонку
fr := TFrameColumn(Cls.Items[i]);
cl := fr.cl;
//...
//заполняем значение в NodeColList для текущей колонки в массиве
tempVariant := qQuery.FieldByName(Fields[i]).Value;
if VarType(tempVariant)=8209 then
begin
if VarIsNull(tempVariant) then
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value
else
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).asString;
end
else
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value;
end
end
end
И теперь ещё посмотрим, что происходит внутри AddData:
function TUniLinesFrame.AddData(ANode: PVirtualNode = nil): PLinesData;
var Node: PVirtualNode;
i: integer;
begin
//если мы передали ANode = nil, то будем создавать новую ноду, иначе работаем с переданной
if ANode=nil then
begin
Node:=VST.AddChild(nil);
if FIsMultiline then
Node.States:=Node.States+[vsMultiline];
end
else
Node:=ANode;
//...
Result:=VST.GetNodeData(Node); //получаем данные текущей ноды (запись типа PLinesData)
Result.NodeColList:=TVariantList.Create; //создаём объект NodeColList
for i:=0 to Cls.Count-1 do
Result.NodeColList.Add(Null); //заполняем null-ами элементы по числу наших колонок в Cls
//инициализация остальных параметров, указание опций по умолчанию
end;
Подытожим последовательность происходящих действий:
Рассмотрим модуль DocInWay.pas (карточка накладной).
Например, в модуле , есть UniLines, который отображает данные по услугам из этой накладной. Сами накладные у нас хранятся в таблице DOCS_IN_WAY, а услуги в ней - в таблице DOCS_IN_WAY_SERVS.
Когда мы создаём новую накладную, в ней никаких услуг быть не может, но для инициализации колонок нам всё равно нужно выполнить процедуру Open:
ULF.Open(1581, -1, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
Но когда открывается существующая накладная, мы делаем так:
ULF.Open(1581, ID, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
В качестве ID используется ID текущей накладной, которую мы открываем.
При формировании запроса будет сформировано условие dis.doc_in_way_id = :MAIN_ID, которое оставит только те строки, которые относятся к накладной. В этот :MAIN_ID будет подставляться переданный ID. Это делается за счёт наличия колонки со следующими опциями:
Другой пример, журнал заказов - модуль ZakazPrepHim.pas.
ULF.Open(66, Null, ' ', RdTr, False, WhereSQL, True, ' ', True, False, cbDateKind.ItemIndex);
Обновление данных происходит с помощью процедуры procedure TUniLinesFrame.Refresh. Она принимает следующие параметры:
Основная логика Refresh такова:
//...предварительные проверки и инициализации
//если у нас не поменялось условие для фильтрации, нам не нужен :MAIN_ID и у нас неполная перезагрузка данных, то:
if (sAddWhere=sLastAddWhere) and (Object_ID=Null) and (not FullReload) and (not IsVisiblePage) then
begin
//получаем список конкретных записей, которые были изменены (добавлены, изменены или удалены)
ids:=GiveMeResultSET('SELECT DISTINCT ITEM_ID FROM META_OBJECTS_CHANGES MOC '+
'WHERE ID>:p1', [FLastLoadId], RdTr);
try
//по каждой записи получаем все произошедшие изменения
while not ids.Eof do
begin
Ch:=GiveMeResultSET('SELECT * FROM META_OBJECTS_CHANGES MOC '+
'WHERE ID>:p1 AND MOC.ITEM_ID=:p2 '+
'ORDER by MOC.change_date,MOC.item_id',
[FLastLoadId, FldAsInt64(ids.FieldByName('ITEM_ID'))], RdTr);
try
WasUpdate:=False;
WasInsert:=False;
WasDels:=False;
while not Ch.Eof do
begin
case Ch.FieldByName('CHANGE_TYPE').AsInteger of
0: WasInsert:=True;
1: WasUpdate:=True;
2: WasDels:=True;
end;
Ch.Next;
end;
//через флаги определяем конечное состояние записи, были ли она в конечном счёте добавлена, обновлена или удалена
if WasInsert and WasUpdate then
WasUpdate:=false;
if WasDels and WasInsert then
begin
WasDels:=False;
WasInsert:=False;
end;
//если запись добавлена или обновлена, то выполнить LoadLine
if WasInsert then
begin
if LoadNewFromMetachanges then
begin
Located:=LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //определяем, есть ли у нас эта запись
LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), not Located); //<----------
WasChanges;
end;
end
else
begin
if LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), True) then
begin
if WasUpdate then
begin
// update line
if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then
begin
LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //<----------------------
WasChanges;
end;
end
else
//если запись удалена, то удаляем соответствующую ноду из VST
if WasDels then
begin
if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then
begin
Self.VST.DeleteNode(Self.VST.FocusedNode);
WasChanges;
end;
end;
end;
end;
finally
ch.Free;
end;
ids.Next;
end;
finally
ids.Free;
end;
FLastLoadId:=FastSQLInt('SELECT * FROM SP_GEN_META_OBJECTS_CHANGES_ID', [])-1;
end
Основная загрузка данных происходит в процедуре LoadLine. В качестве входящих параметров принимает PrimaryKey нужной записи и AddNewLine - флаг, что нам надо добавить новую строку. PrimaryKey нужен, чтобы в запрос метаобъекта поставить условие по конкретному ID. Поле для условия берётся по флагу Primary Field у колонки метаобъекта.
Происходит это так:
procedure TUniLinesFrame.LoadLine(PrimaryKey: Variant; AddNewLine: Boolean = False);
var
vOldCur: TCursor;
vWhere: String;
begin
//...предварительные обработки
//если запрос ещё не сформирован, то формируем его, добавляя в условие параметр PRIM
if (MetaQuery.SQL = '') then
qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryField +'=:PRIM'+ vWhere, CheckManyToOneLinks)
else
qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryFieldOrigin +'=:PRIM'+ vWhere, False);
//передаём параметр
qQuery.ParamByName('PRIM').Value := PrimaryKey;
qQuery.Open; //формируем датасет
LoadData(PrimaryKey, AddNewLine); //используем уже рассмотренную ранее процедуру по загрузке данных
end;
Т.е. LoadLine, по своей сути, формирует запрос данных, но с таким условием, чтобы получить только одну запись. Разумеется, для нормальной работы соответствующий метаобъект должен быть корректно настроен.
Если же нам требуется полная перезагрузка данных, то тут процесс схож с процедурой Open и только дополнен очищением текущих данных и ссылок, после чего также формируется запрос, формируется датасет и вызывается LoadData.
Рассмотрим модуль DocsInWay.pas (журнал накладных).
На журнале расположены кнопки, выступающие в роли быстрой фильтрации журнала:
На нажатие каждой кнопки повешен обработчик, в процессе работы которого формируется некоторая строка sWhere, содержащая в себе необходимое условие для запроса, после чего вызывается
ULF.Refresh(null, sWhere, True);
UniLines позволяет легко сохранять имеющиеся данные в базу. Для этого используется процедура Save. Она не принимает в себя никаких параметров. Разберём, как именно она работает:
procedure TUniLinesFrame.Save;
var Node: PVirtualNode;
Dat: PLinesData;
oldCur: TCursor;
prevCurLine: PLinesData;
begin
{... предварительные обработки}
try
//начинаем работать непосредственно с нодами VST, начиная с первой и до последней по очереди
Node:=VST.GetFirst;
while Node<>nil do
begin
//получаем данные, хранящиеся в этой ноде
Dat:=VST.GetNodeData(Node);
curLine:=Dat;
//если состояние LineState имеет значения lnInsert, lsUpdate или lsDelete, то вызываем процедуру OnSaveLine, если она имеется;
if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then
begin
if Assigned(FOnSaveLine) then
FOnSaveLine(Dat^.LineState);
end;
//Отработка OnSaveLine может поменять состояние текущей строки (например, там может быть запрет на дальнейшие операции путём выставления lsNone).
if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then
begin
//если состояние не поменялось, то запускается SaveLine
SaveLine;
//после него запускается обработчик OnAfterSaveLine, если он имеется.
if Assigned(FOnAfterSaveLine) then
FOnAfterSaveLine(Dat^.LineState);
end;
Node:=VST.GetNext(Node);
end;
// Помечаем все изменные строки как неизмененные
Node:=VST.GetFirst;
while Node<>nil do
begin
Dat:=VST.GetNodeData(Node);
curLine:=Dat;
if (Dat^.LineState<>lsNone) and not ((Dat^.LineState = lsDelete) or (Dat^.LineState = lsEmpty)) then
begin
Dat^.LineState:=lsNone;
end;
Node:=VST.GetNext(Node);
end;
finally
Screen.Cursor:=oldCur;
curLine:=prevCurLine;
end;
end;
Здесь появляется ещё один важный параметр - LineState, который указывается для данных ноды. В этой статье подробно мы не будем останавливаться, отметим про него, что это - условно "состояние" строки, была ли она добавлена, обновлена или удалена (значение LineState будет принимать соответственно lsInsert, lsUpdate, lnDelete). В остальных случаях она будет приниимать значение lsNone (стандартное значение при загрузке данных) или lsEmpty.
OnSaveLine и OnAfterSaveLine - базово этих обработчиков нет, но они могут быть заданы в рантайме для тех или иных целей. Примеры рассмотрим позже, а пока обратим внимание на самую интересную часть, процедуру SaveLine:
procedure TUniLinesFrame.SaveLine;
var i: integer;
PrimTbl, PrimFld: string;
PrimVal, CurVal: variant;
MetaCol: TMetaColumn;
begin
//если lineState стал lsNone, то ничего не делается;
if LineState=lsNone then
Exit;
{...прочие очистки}
//перебираем список колонок
for i:=0 to Cls.Count-1 do
begin
MetaCol:=GetMetaColumn(i);
//выбираем только те колонки, у которых указана опция Is_Field и не указана Is_LineNumber
if (MetaCol.Is_Field) and (not MetaCol.Is_LineNumber) then
begin
//получаем значение, содержащееся в определенной колонке текущей записи
curVal:=curLine.NodeColList.Items[i];
//если колонка с опцией Is_Primary, текущее значение Null, то генерируем значение ID на основе той процедуры, которую мы передали при Open у текущего UniLines;
if MetaCol.Is_Primary then
begin
if (CurVal=null) then
begin
if (FProcGenID<>'') then
begin
CurVal:=FastSQLVal('select * from '+FProcGenID, []);
curLine.NodeColList.Items[i]:=CurVal;
if curLine.LineState=lsUpdate then
curLine.LineState:=lsInsert;
end;
end;
end;
{...доп. обработки в зависимости от типа значения колонки}
//заполняем массив полей и их значений
sqlFlds.Add(Fields[i]);
sqlVals.Add(CurVal);
end;
end;
{...}
//получаем название таблицы, в которую будем сохранять данные, и название поля с первичным ключом
MetaCol:=MetaQuery.PrimaryColumn;
if MetaCol<>nil then
begin
PrimTbl:=MetaCol.TableName;
PrimFld:=MetaCol.FieldName;
i:=GetMetaColumnIndex(MetaCol);
PrimVal:=curLine.NodeColList.Items[i];
end;
//в зависимости от текущего состояния строки, делаем нужную операцию с базой данных
if (PrimTbl<>'') and (PrimFld<>'') then
case curLine.LineState of
lsInsert:
begin
InsertFromControls(PrimTbl, sqlFlds, sqlVals, RdTr);
end;
lsUpdate:
begin
UpdateFromControls(PrimTbl, PrimFld, PrimVal, sqlFlds, sqlVals, RdTr);
end;
lsDelete:
begin
DeleteFromControls(PrimTbl, PrimFld, PrimVal, RdTr);
end;
end
else
raise Exception.Create('Primary key for '+MetaObject.ObjName+' not found');
end;
Обратим внимание, что коммита здесь не происходит, его надо выполнять отдельно. Также, если не нашли Primary-колонку, будет выдан эксэпшн.
Рассмотрим простой пример сохранения, в котором опишем полную последовательность шагов, что за чем происходит. Обратим внимание на модуль PlanMobileItem.pas - карточка выезда в журнале выездов. В качестве сохраняемого UniLines здесь используется таблица с перечнем заказов, по которому должен быть выезд.
Открытие Unilines:
ULF.Open(5758, -1, 'SP_GEN_MOBILE_PLAN_ORDERS_ID', UpTr, False);
Здесь есть колонка "ВнНомер", с опциями Primary, Is_Field. В качестве поля используется ID из таблицы MOBILE_PLAN_ORDERS.
Заполнение этой таблицы данными делается в процессе работы пользователя, механизмы заполнения рассмотрим в другой статье.
При закрытии карточки выезда вызывается ULF.Save, и далее по шагам: