Типовой задачей представления данных является создание отношений Master-Detail, то есть когда один объект является детализацией другого. По другому можно сказать, что один объект состоит из нескольких других. При этом классы объектов различаются. Реализовать это в Data Keeper не составило труда, однако некоторые изменения и доработки сделать всё же пришлось.

Структура данных

Детализация строится на основании свойства, которое ссылается на объект-мастер. Но далеко не всегда требуется эта самая детализация. Поэтому в таблицу cproperty необходимо добавить логическое поле is_detail, которое и будет отвечать за визуализацию детализации – превращать связь “один-ко-многим” в пару таблиц на форме.

Для нового поля добавляем элемент на форме редактирования efmCProperty.

Как видно из примера, свойство “Страна” класса “Город” используется для детализации типа. В данном случае типом является Страна. Звучит немного странно, но пока не придумал более простого названия. Но, надеюсь, вполне очевидно, что после установки этого флага при просмотре списка стран должна появиться вкладка с детализацией по городам.

Формы

На форме отображения объекта произошли изменения: таблица для отображения данных tgrMain перенесена на новую панель panMain, которая расположена в верней части формы. В нижней части формы добавлена вторая панель – panDetails, которая служит для отображения кнопочного переключателя детализации pgcDetails, созданного из компонента TdbPageControl. Чтобы все сработало, с помощью скриптов устанавливается выравнивание (свойство Align) и добавляется разделитель (компонент TSplitter). Обратите внимание, что pgcDetails имеет небольшую высоту, так как остальная часть panDetails используется для размещения на ней формы детализации – frmObject_Detail.

Краткая справка по сервисным функциям и процедурам, которые были для этого использованы:

  • Splitter_Create() – создание разделителя
  • Form_ShowOnWinControl() – размещение формы на компоненте другой формы

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

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

SELECT 
  class.id, class.name 
FROM cproperty 
LEFT JOIN class ON class.id = cproperty.id_class 
WHERE 
  cproperty.is_detail = 1 
AND cproperty.id_class1 = <ID_Мастер-объекта>Code language: SQL (Structured Query Language) (sql)

Полный текст процедуры frmObject_btnUpdate_OnClick приводится ниже.

procedure frmObject_btnUpdate_OnClick (Sender: TObject; var Cancel: boolean);
// обновить отображение
var
  tmpIDClass: integer;
  tmpButton: TdbButton;
  tmpDataSet: TDataSet;
  tmpSQL: string;
  i: integer;
  tmpTabSheet: TdbTabSheet;
  Splitter:TSplitter;
begin
  frmObject.labIDClass.Caption := 'Change'; // блокируем срабатывание frmObject_tgrMain_OnColumnResize
  // строится запрос на выборку данных - все объекты указанного класса
  // получаем ID класса из дерева класса
  tmpIDClass := Form_GetDataViewer( GetFormByName('dtfClass_Tree') ).dbItemId;
  frmObject.labIDClass.Tag := tmpIDClass; // запоминаем класс
  tmpButton := TdbButton(Sender);
  btnPrepareOject( tmpButton, tmpIDClass);
  // убираем детализацию
  for i:= frmObject.pgcDetails.PageCount - 1 downto 0 do
  begin
    frmObject.pgcDetails.Pages[i].Free;
  end;
  FindC(frmObject,frmObject.panDetails.TagString,Splitter);
  Splitter.Visible := False;
  frmObject.panDetails.Visible := False;
  // определить, есть ли детализация, и создать вкладки-кнопки
  tmpSQL := 'SELECT class.id, class.name FROM cproperty LEFT JOIN class ON class.id = cproperty.id_class WHERE cproperty.is_detail = 1 AND cproperty.id_class1 = '+IntToStr( tmpIDClass );
  SQLQuery(tmpSQL,tmpDataSet);
  while not tmpDataSet.EOF do
  begin
    tmpTabSheet := TdbTabSheet.Create( frmObject );
    with tmpTabSheet do
    begin
      PageControl := frmObject.pgcDetails;
//      Name := '';
      Caption := tmpDataSet.FieldByName('name').asString;
      Tag := tmpDataSet.FieldByName('id').asInteger;
//      TagString := '';
    end;
    tmpDataSet.Next;
  end;
  if frmObject.pgcDetails.PageCount > 0 then
  begin
    frmObject.panDetails.Visible := True;
    Splitter.Visible := True;
    frmObject_Detail.btnUpdate.Click;
  end;
end;Code language: PHP (php)

Для редактирования детальной части также используется форма efmObject, а сам механизм вызова формы редактирования остался прежним, так как возможность вызова формы редактирования по имени формы табличного представления была реализована через вызов функции Grid_GetTableName().

function Grid_GetTableName(AGrid: TdbStringGridEx): string;
// возвращает имя таблицы
var
  tmpForm: TAForm;
begin
  // используем соглашение по наименованию формы:
  // <frm><имя таблицы>[_<суффикс>]
  // то есть может быть несколько форм с табличным представлением для одной таблицы,
  // они должны различаться суффиксом
  CForm(AGrid, tmpForm);
  Result := DeleteSuffix(DeleteClassName(tmpForm.name));
end;
Code language: PHP (php)

Важной деталью является функция автоматического заполнения поля связи детализации с мастером при добавлении новой записи. Для этого в процедуру efmObject_OnShow() были добавлены всего несколько строчек (19-29):

    if tmpControlID = 2 then // выпадающий список
    begin
      //
      FindC(tmpForm,'cmbData_'+intToStr(tmpCount),tmpComboBox,False);
      if tmpComboBox = nil then
        tmpComboBox := TdbComboBox.Create( tmpForm );
      with tmpComboBox do
      begin
        visible := True;
        name := 'cmbData_'+intToStr(tmpCount);
        parent := tmpParent;
        Font.Size := 11;
        top := tmpCount * 50 + tmpLabel.Height;
        left := 8;
        width := 300;
        tagString := VarToStr( SQLExecute('SELECT id_object1 FROM oproperty WHERE id_object = '+IntToStr( efmObject.btnSave.dbGeneralTableId  )+' AND id_cproperty = '+tmpDataSet.FieldByName('id').asString ) );
        dbSQL := 'SELECT object.id, oproperty.value_s FROM object LEFT JOIN oproperty ON oproperty.id_object = object.id WHERE oproperty.id_cproperty = ( SELECT id FROM cproperty WHERE cproperty.is_name = 1 ) AND object.id_class = '+tmpDataSet.FieldByName('ClassID').asString+' ORDER BY 2';
        dbUpdate;
        enabled := True;
        // автозаполнение связи с мастером
        // если редактируемая таблица - таблица детализации и поле является полем связи с мастером, то
        if (ActiveGrid = frmObject_Detail.tgrMain) and (tmpDataSet.FieldByName('is_detail').asInteger = 1) then
        begin
          enabled := False; // делаем поле недоступным для ручного изменения
          if (Action = 'NewRecord') then
          begin // а для новой записи добавляем ID из главной таблицы
            tagString := IntToStr( frmObject.tgrMain.dbItemID );
          end;
        end;
        if tagString <> '' then // если есть ссылочное значение, то синхронизировать выпадающий список
          dbItemID := StrToInt(tagString);
        onChange := 'efmObject_cmbEdit_OnChange';
        Font.Style :=0;
      end;
    end;

Code language: PHP (php)

Также потребуется скрипт для удаления детализации при удалении мастера, так как каскадное удаление, которое обычно применяют в таких случаях, не сработает – объекты хранятся в одной таблице и связываются косвенно, через таблицу свойств. Но это я сделаю в следующей версии, а пока придется вручную удалять все объекты детализации, и только потом – объект-мастер.

Результат

Теперь из любого ссылочного свойства можно создать детализированное представление данных.

Получилось неплохо, выглядит как реальный счет, однако теперь ярче стали видны и недостатки.

У каждого объекта маячит свойство “Название”. Для счета оно не актуально, для его идентификации достаточно даты и номера. С другой стороны, если счёт понадобится как свойство в другом объекте, то либо надо допиливать систему настойки для отображения названия объекта (чтобы отображать сразу два поля: дату и номер), либо нужен какой-то механизм автоматического заполнения названия на основании данных из других полей на форме редактирования. Это касается и поля “Сумма”, значение которого является произведением значений полей “Цена” и “Количество”. С другой стороны, такие поля обычно являются вычисляемыми – они не хранятся в базе, но отображаются в табличном представлении, а в My Visual Database могут отображаться и на форме редактирования.

Что ещё нужно сделать:

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

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

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