или Множественное значение

В некоторых случаях свойство объекта представляет собой множество ссылочных значений. Например, у книги может быть несколько авторов, компьютерная игра может относиться сразу к нескольким жанрам. В этом случае нас выручит подтип значения под названием “множество”.

На стороне базы данных уже все готово для хранения таких значений. Рассмотрим ещё раз таблицу oproperty.

Каждая запись в этой таблице – одно значение конкретного свойства объекта, в том числе:

  • value_s – текстовое представление значения
  • id_object1 – ссылка на объект

Стоит заметить, что поле value_s хранит не только само значение свойства (текст, число или дату), но и удобную форму представления ссылочного свойства – название объекта. В случае множественного значения в этом поле хранится список названий объектов, входящих в множество выбранных значений, а каждая ссылка хранится в отдельной записи с теми же атрибутами id_object и id_cproperty, что и запись с визуализацией множества. Чтобы отличить значение свойства от значений множественных ссылок, служит поле orderNum:

  • NULL – запись служит для хранения отображаемого свойства
  • <порядковый номер> – запись содержит элемент множественного значения.

Сейчас поле orderNum по сути используется как флаг, но в перспективе можно добавить функционал, связанный с порядком следования элементов множества.

Доработки приложения

Так как Data Keeper создается на My Visual Database, то мне всё чаще приходится сталкиваться с ограничениями и ошибками в работе данной платформы, которые не позволяют реализовать все задумки. В частности, компонент TdbComboBox задуман как компонент, который имеет возможность отображать множественные значения. Но не имеет желания корректно работать под ОС Windows 10, в результате чего мне пришлось отказаться от его использования в режиме редактирования множественных значений. Также окончилась неудачей попытка создания суррогатного компонента из-за ограничений скриптового языка MVDB. А так как я всё равно собирался создавать возможность альтернативного выбора ссылочных значений через модальную форму, то с неё и начнем, сразу добавив в неё функционал по работе с множественными значениями.

frmObject_Sel

Форма выбора значений оказалась похожей на форму со списком экземпляров классов frmObject. По крайней мере алгоритм формирования табличного представления у них одинаковый, что заставило меня оформить его как отдельную процедуру btnPrepareOject(), в которую передается кнопка для отображения результата SQL-запроса в таблице, ID класса объекта, а также флаг необходимости добавления поля с чекерами – элемента управления множественным выбором.

procedure btnPrepareOject( AButton: TdbButton; AIDClass: integer; ACheckBox:Boolean = False);
// генерация табличного представления класса
var
  tmpParentList: string;
  tmpChildList: string;
  tmpDataSet: TDataSet;
  tmpSQL: string;
  tmpFields : string;
  tmpJoins: string;
  tmpCount: integer;
  tmpCaptions: string;
  tmpTableAlias: string;
begin
  GetChildAndParent( AIDClass, tmpParentList, tmpChildList); // получить список ID родителей и детей
  tmpSQL := 'SELECT * FROM cproperty WHERE id_class in ('+tmpParentList+') ORDER BY orderNum ';
  SQLQuery(tmpSQL,tmpDataSet);
  tmpFields := 'object.id';
  if ACheckBox then
    tmpFields := tmpFields + ',"$checkbox"';
  tmpJoins := '';
  tmpCaptions := 'delete_col'; // не отображать
  if ACheckBox then
    tmpCaptions := tmpCaptions + ',#';
  tmpCount := 0;
  //
  while not tmpDataSet.EOF do
  begin
    if tmpDataSet.FieldByName('visible').asInteger = 1 then
    begin
      tmpTableAlias := 'OP_'+IntToStr(tmpCount);
      //
      if tmpFields <> '' then
        tmpFields := tmpFields + ', ';
      tmpFields := tmpFields + tmpTableAlias+'.value_s ';
      //
      tmpJoins := tmpJoins + 'LEFT JOIN oproperty '+tmpTableAlias+' ON '+tmpTableAlias+'.id_object = object.id AND '+tmpTableAlias+'.id_cproperty = '+tmpDataSet.FieldByName('id').asString+' AND '+tmpTableAlias+'.orderNum is NULL '+CR ;
      //
      if tmpCaptions <> '' then
        tmpCaptions := tmpCaptions + ', ';
      tmpCaptions := tmpCaptions + tmpDataSet.FieldByName('name').asString;
      if tmpDataSet.FieldByName('is_name').asInteger = 1 then
        AButton.Tag := tmpCount;
      //
      inc(tmpCount);
    end;
    tmpDataSet.Next;
  end;
  tmpDataSet.Free;
  tmpSQL := 'SELECT '+tmpFields+CR+' FROM object '+tmpJoins+CR+' WHERE object.id_class in ('+tmpChildList+')';
  AButton.dbSQL := tmpSQL;
  AButton.dbListFieldsNames := tmpCaptions;
end;Code language: JavaScript (javascript)

Как видно из первых строчек кода, формирование списка ID предков и потомков тоже реализовано как отдельная процедура с параметрами GetChildAndParent(). В дальнейшем она упразднится, так как интервальные деревья, на которые я планирую перевести таблицу object, позволяют формировать списки родителей и детей достаточно простым SQL-запросом.

procedure GetChildAndParent(AID:integer; var AParentList:string; var AChildList:string);
// формирование списка родителей и детей
var
  tmpTree: TdbTreeView;
  s: string;
  tmpIndex: integer;
  tmpForm: TForm;
  tmpLevel: integer;
begin
  AParentList := '-1';
  AChildList := '-1';
  tmpForm := GetFormByName('dtfClass_Tree');
  if tmpForm <> nil then
  begin
    tmpTree := TdbTreeView( Form_GetDataViewer( tmpForm ) );
    // собираем ID c узла и всех родительских узлов
    tmpIndex := Grid_IdToIndex( tmpTree, AID);
    if tmpIndex <> -1 then
    begin
      s := '';
      repeat
        if s <> '' then
          s := s+',';
        s := s + IntToStr( tmpTree.dbIndexToID( tmpIndex ) );
        tmpIndex := tmpTree.GetParent( tmpIndex );
      until tmpIndex < 0;
      AParentList := s;
      // собираем дочерние ID
      tmpIndex := Grid_IdToIndex( tmpTree, AID);
      tmpLevel := tmpTree.GetLevel( tmpIndex );
      s := '';
      repeat
        if s <> '' then
          s := s+',';
        s := s + IntToStr( tmpTree.dbIndexToID( tmpIndex ) );
        inc(tmpIndex);
      until (tmpIndex = tmpTree.RowCount) or ( tmpTree.GetLevel( tmpIndex ) <=  tmpLevel );
      AChildList := s;
    end;
  end;
end;
Code language: JavaScript (javascript)

Процедуры frmObject_tgrMain_OnChange и frmObject_tgrMain_OnColumnResize были переписаны таким образом, чтобы их можно было использовать на разных формах, с чекерами и без них. Для упрощения алгоритма используется соглашение, что колонка с чекерами имеет заголовок “#”.

procedure frmObject_tgrMain_OnChange (Sender: TObject);
// обновление данных в таблице
var
  tmpIDClass: integer;
  tmpSQL: string;
  tmpColumn: integer;
  s: string;
  tmpColWidth: array of string;

  tmpForm:TForm;
  tmpLabel:TdbLabel;
  tmpGrid: TdbStringGridEx;
  tmpFirstColumn: integer;
  tmpIDs: array of string;
  i: integer;
  tmpIndex: integer;
begin
  tmpGrid := TdbStringGridEx(Sender);
  CForm(Sender,tmpForm);
  FindC(tmpForm,'labIDClass',tmpLabel);
  // получаем ID
  tmpIDClass := tmpLabel.Tag;
  // считать ширины колонок из базы
  tmpSQL := 'SELECT COALESCE(col_widths,"") FROM class WHERE id = '+IntToStr(tmpIDClass);
  s := SQLExecute(tmpSQL);
  tmpColWidth := SplitString(s,',');

  tmpFirstColumn := 0;
  if (tmpGrid.Columns.Count > 0) and (tmpGrid.Columns[tmpFirstColumn].Header.Caption = '#')  then
  begin
    tmpGrid.Columns[tmpFirstColumn].Width := 20;
    tmpFirstColumn := 1;
  end;
  for tmpColumn := tmpFirstColumn to tmpGrid.Columns.Count - 1 do
  begin
    if tmpColumn < length(tmpColWidth)+tmpFirstColumn then
      tmpGrid.Columns[tmpColumn].Width := StrToInt(tmpColWidth[tmpColumn-tmpFirstColumn] )
    else
      tmpGrid.Columns[tmpColumn].Width := 200;
  end;
  tmpLabel.Caption := ''; // разблокируем

  // позиционируем
  FindC(tmpForm,'labID',tmpLabel,False);
  if (tmpLabel<>nil) and (tmpLabel.Caption <> '') then
  begin
    if tmpGrid.Columns[0].Header.Caption = '#' then
    begin
      tmpIDs := SplitString(tmpLabel.Caption,',');
      for i:=0 to length(tmpIDs) - 1 do
      begin
        tmpIndex := Grid_IdToIndex(tmpGrid, StrToInt( tmpIDs[i] ) );
        tmpGrid.Cell[0,tmpIndex].asBoolean := True;
      end;
    end
    else
      tmpGrid.dbItemID := StrToInt(tmpLabel.Caption);
  end;
end;

procedure frmObject_tgrMain_OnColumnResize (Sender: TObject; ACol: Integer);
// изменение ширины колонок
var
  tmpIDClass: integer;
  tmpSQL : string;
  i: integer;
  s: string;
  tmpForm:TForm;
  tmpLabel:TdbLabel;
  tmpGrid: TdbStringGridEx;
  tmpFirstColumn: integer;
begin
  tmpGrid := TdbStringGridEx(Sender);
  if (tmpGrid.Columns.Count > 0) then
  begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'labIDClass',tmpLabel);
  //
  if tmpLabel.Caption = '' then
  begin
    s := '';
    tmpFirstColumn := 0;
    if  tmpGrid.Columns[tmpFirstColumn].Header.Caption = '#' then
      tmpFirstColumn := 1;
    for i:=tmpFirstColumn to tmpGrid.Columns.Count - 1 do
      s := s + IntToStr(tmpGrid.Columns[i].Width) + ',';
    delete(s,length(s),1);
    // запоминаем ширины всех колонок в БД
    tmpIDClass := tmpLabel.Tag; //  класс
    tmpSQL := 'UPDATE class SET col_widths = "'+s+'" WHERE id = '+IntToStr(tmpIDClass);
    SQLExecute(tmpSQL);
  end;
  end;
end;Code language: PHP (php)

На форме также расположена кнопка подтверждения выбора (1) и дополнительный элемент-метка, который используется для передачи параметров на форму: значение ссылочного параметра и его визуальное отображение. На кнопке находится обработчик, который может формировать из чекеров строку с ID выбранных значений и строку визуального отображения:

procedure frmObject_Sel_btnSelect_OnClick (Sender: TObject; var Cancel: boolean);
var
  i: integer;
  tmpCaption: string;
  tmpIDs: string;
begin
  if  frmObject_Sel.tgrMain.Columns[0].Header.Caption = '#' then
  begin
    tmpCaption := '';
    tmpIDs := '';
    for i := 0 to frmObject_Sel.tgrMain.RowCount - 1 do
    begin
      if frmObject_Sel.tgrMain.Cell[0,i].AsBoolean then
      begin
        if tmpCaption <> '' then
          tmpCaption := tmpCaption + ', ';
        tmpCaption := tmpCaption + frmObject_Sel.tgrMain.cells[ frmObject_Sel.btnUpdate.Tag + 1, i  ];
        if tmpIDs <> '' then
          tmpIDs := tmpIDs + ',';
        tmpIDs := tmpIDs + IntToStr(frmObject_Sel.tgrMain.dbIndexToID( i ));
      end;
    end;
    frmObject_Sel.labID.Caption := tmpIDs;
    frmObject_Sel.labID.TagString := tmpCaption;
  end
  else
  begin
    frmObject_Sel.labID.Caption := IntToStr(frmObject_Sel.tgrMain.dbItemID);
    frmObject_Sel.labID.TagString := frmObject_Sel.tgrMain.cells[ frmObject_Sel.btnUpdate.Tag, frmObject_Sel.tgrMain.SelectedRow  ];
  end;
  frmObject_Sel.ModalResult := mrOK;
end;Code language: JavaScript (javascript)

Доработке также подверглись другие процедуры и функции.

Компоненты UI

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

Чтобы всё сработало, настраиваем тип компонента в дереве классов:

Затем устанавливаем тип значения “Автор” как “Множество”.

В результате при редактировании книги можно выбрать несколько авторов, что соответствует реальной картине мира.

Результат

Появилась возможность редактирования множественных значений, которые отображаются как список (1), а редактируются чекерами (2) на отдельной форме.

Не следует путать множественное значение со списком объектов, входящих в состав другого объекта, например, рецепт блюда или детализация накладной. Главное отличие состоит в том, что элемент такого списка не является простым объектом, а содержит несколько уникальных свойств (ингредиенты – информацию о количестве, строка в накладной – количество и стоимость товара). Вот только удобней отображать детализацию накладной вместе с самой накладной. Задача отображения зависимых объектов стоит в списке приоритетных доработок:

  • Интервальное дерево классов
  • Поиск объектов по заданному значению свойства (нескольких свойств)
  • Отображение зависимых объектов (объектов, у которых данный объект является значением какого-либо свойства)
  • Создание произвольных представлений (использования аналога SQL для ООП).
  • Фильтрация (автоматическое построение панели фильтрации объектов заданного класса на основании списка свойств)
  • Флаг обязательности заполнения свойства объекта (аналог обязательного поля)
  • Флаг уникальности свойства объекта класса (аналог уникальности значения поля)
  • Флаг глобальной уникальности свойства объекта (или свойство с генерацией GUID)
  • Расширение базовых типов данных (время, да/нет, изображения/файлы и др.)
  • Добавление контроля для типов данных (аналог доменов) – диапазон значений для чисел и дат, длина текста для строк и т.д.
  • Ручная настройка положения и размера компонентов на форме редактирования классов.
  • Добавление новых ссылочных значений непосредственно из формы редактирования объекта.

Заглядывая на перспективу реализации данной системы для web, размышляю над тем, что web-интерфейс не предполагает наличия модальных форм, создаваемых сервером. Придется много мудрить с клиентскими скриптами, чтобы реализовать такую простую и нужную концепцию, как модальность и вложенность отображения форм.

Ссылки

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *