I continue to develop the concept of a custom data warehouse, which can form the basis of the My Visual Multibase project. Below is a description of the implementation of three more possibilities:

  • Inheritance of class properties
  • Displaying child objects along with their parents
  • Extension of basic data types: adding the “Date” type.

Property inheritance

Let me remind you that the principle of inheritance is one of the principles of the object-oriented approach in programming (you can read more about the development of this concept in the article “The Evolution of Abstractions”). And this mechanism really helps save time when creating child classes. Let’s look at this with an example.

It is quite obvious that the ancestor class must have at least two common properties that will be useful to all descendants:

  • Unique identificator
  • Name

The unique identifier is implemented as an object.id field, which is not editable. But the name is stored in the cproperty table, along with the other properties of the object. This is necessary so as not to further complicate the methods for generating table views, editing forms and filling out drop-down lists for reference values.

It should be noted that only this property has the “Object Display Name” checkbox checked. The probability that the root object can have other properties is non-zero, so let it be for now.

When describing a geographical object, the “Name” property is already present in the list of properties (1) and is highlighted in italics, which indicates the inheritance of this property from an ancestor. All that remains is to add a new property – “Area”.

The “Country” class inherits two properties: “Name” and “Area”, and has its own property “Capital”, which is a reference to an object of the “City” class.

In order for this mechanism to work, changes were required in the procedures responsible for displaying the list of properties: in tabular form on the “Properties” tab; list of columns on the “Objects” tab; list of editing components on the efmObject form. Now, instead of filtering properties by a specific class, you filter by a list of IDs, which contains the ID of the displayed class and the IDs of all its ancestors.

We also had to replace the dynamic form dtfCProperty with the static frmCProperty, in which data is displayed using an SQL query

select
  cproperty.id,
  cproperty.id_class,
  cproperty.orderNum,
  cproperty.name,
  class.name as  className,
  ptype.name  as ptypeName,
  cproperty.description
from cproperty
left join class on class.id = cproperty.id_class1
left join ptype on ptype.id = cproperty.id_ptype
where id_class in ( {edtIDMaster} )
order by orderNum  Code language: SQL (Structured Query Language) (sql)

The frmCProperty.edtIDMaster component contains a list of IDs. The current implementation of the class tree does not allow generating this list using an SQL query (which suggests switching to an interval tree), so you have to create it using a script using the TdbTreeView.GetParent() method:

procedure frmCProperty_btnUpdate_OnClick (Sender: TObject; var Cancel: boolean);
var
  tmpTree: TdbTreeView;
  s: string;
  tmpIndex: integer;
  tmpForm: TForm;
  tmpLevel: integer;
begin
  tmpForm := GetFormByName('dtfClass_Tree');
  if tmpForm <> nil then
  begin
    tmpTree := TdbTreeView( Form_GetDataViewer( tmpForm ) );
    // collect IDs from the node and all parent nodes
    tmpIndex := tmpTree.SelectedRow;
    if tmpIndex = -1 then
      frmCProperty.edtIDMaster.Text := '-1'
    else
    begin
      s := '';
      repeat
        if s <> '' then
          s := s+','
        else
          frmCProperty.edtIDClass.Value := tmpTree.dbIndexToID( tmpIndex );
        s := s + IntToStr( tmpTree.dbIndexToID( tmpIndex ) );
        tmpIndex := tmpTree.GetParent( tmpIndex );
      until tmpIndex < 0;
      frmCProperty.edtIDMaster.Text := s;
    end;
    // collecting child IDs
    tmpIndex := tmpTree.SelectedRow;
    if tmpIndex = -1 then
      frmCProperty.edtIDChilds.Text := '-1'
    else
    begin
      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 );
      frmCProperty.edtIDChilds.Text := s;
    end;
  end;
end;
Code language: JavaScript (javascript)

In the same script, a list of child nodes is generated, which uses the TdbTreeView.GetLevel() method. This list will be useful to us for displaying objects.

Displaying Child Objects

We modify frmObject_btnUpdate_OnClick() so that the list of objects displays objects of both the selected class itself and all its descendants:

procedure frmObject_btnUpdate_OnClick (Sender: TObject; var Cancel: boolean);
// update table view
var
  tmpIDClass: integer;
  tmpDataSet: TDataSet;
  tmpSQL: string;
  tmpFields : string;
  tmpJoins: string;
  tmpCount: integer;
  tmpCaptions: string;
  tmpTableAlias: string;
  tmpButton: TdbButton;
  tmpIDClassList: string;
  tmpIDChildList: string;
begin
  frmObject.labIDClass.Caption := 'Change'; // block the event processing of frmObject_tgrMain_OnColumnResize
  // a query is built to select data - all objects of the specified class
  tmpIDClass := Form_GetDataViewer( GetFormByName('dtfClass_Tree') ).dbItemId;
  frmObject.labIDClass.Tag := tmpIDClass; // запоминаем класс
  tmpIDClassList := frmCProperty.edtIDMaster.Text; // список классов
  tmpIDChildList := frmCProperty.edtIDChilds.Text; // список классов
  tmpSQL := 'SELECT * FROM cproperty WHERE id_class in ('+tmpIDClassList+') ORDER BY orderNum ';
  SQLQuery(tmpSQL,tmpDataSet);
  tmpFields := 'object.id';
  tmpJoins := '';
  tmpCaptions := 'delete_col'; // не отображать
  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+CR ;
      //
      if tmpCaptions <> '' then
        tmpCaptions := tmpCaptions + ', ';
      tmpCaptions := tmpCaptions + tmpDataSet.FieldByName('name').asString;
      //
      inc(tmpCount);
    end;
    tmpDataSet.Next;
  end;
  tmpDataSet.Free;
  tmpSQL := 'SELECT '+tmpFields+CR+' FROM object '+tmpJoins+CR+' WHERE object.id_class in ('+tmpIDChildList+')';
  tmpButton := TdbButton(Sender);
  tmpButton.dbSQL := tmpSQL;
  tmpButton.dbListFieldsNames := tmpCaptions;
end;Code language: Delphi (delphi)

Now in the list of objects you can see how objects of the selected class…

…as well as all objects of child classes:

Date data type

Add the type to the visual components directory.

Then we add a new class to store date data.

Now we add the procedure for generating components on the object editing form, adding the creation of a component for entering the date.

By the way, we had to change the concept of building the form, namely, to abandon the removal of unnecessary components in favor of their reuse. This is due to a bug within MVDB that causes the TdbComboBox.Free method to throw an Access Violation if you ever open the combobox’s dropdown list. However, there is also a plus in this approach – the form opens faster and memory fragmentation is reduced.

procedure efmObject_OnShow (Sender: TObject; Action: string);
// display the editing form
var
  i: integer;
  tmpSQL: string;
  tmpIDClass: integer;
  tmpCount: integer;
  tmpLabel: TdbLabel;
  tmpEdit: TdbEdit;
  tmpComboBox: TdbComboBox;
  tmpForm: TForm;
  tmpParent: TdbPanel;
  tmpDataSet: TDataSet;
  tmpID: string;
  tmpControlID: integer;
  tmpIDClassList: string;
  tmpDataEdit : TdbDateTimePicker;
begin
  tmpIDClassList := frmCProperty.edtIDMaster.Text; // список классов
  tmpForm := TForm(Sender);
  tmpParent := efmObject.panEdit;
  if Action = 'NewRecord' then
  begin
    efmObject.cmbClass.dbItemID := Form_GetDataViewer( GetFormByName('dtfClass_Tree') ).dbItemId;
  end;
  // hide all components
  for i := tmpParent.ControlCount - 1 downto 0 do
  begin
    tmpParent.Controls[i].Visible := False;
  end;
  tmpIDClass := efmObject.cmbClass.dbItemID;
  tmpCount := 0;
  // create the components that are needed to edit the properties of the current object
  tmpSQL := 'SELECT cproperty.id, cproperty.name, cproperty.is_name, class.name as cname, class.id_uicontrol, class.id as ClassID '+
    'FROM cproperty LEFT JOIN class ON class.id = cproperty.id_class1 WHERE id_class in ('+tmpIDClassList+') ORDER BY orderNum ';
  SQLQuery(tmpSQL,tmpDataSet);
  while not tmpDataSet.EOF do
  begin
    tmpControlID := tmpDataSet.FieldByName('id_uicontrol').asInteger;
    // метка
    FindC(tmpForm,'labData_'+intToStr(tmpCount),tmpLabel,False);
    if tmpLabel = nil then
      tmpLabel := TdbLabel.Create( tmpForm );
    with tmpLabel do
    begin
      visible := True;
      parent := tmpParent;
      Font.Size := 11;
      top := tmpCount * 50;
      left := 8;
      name := 'labData_'+intToStr(tmpCount);
      Caption := tmpDataSet.FieldByName('name').asString;
      if tmpDataSet.FieldByName('is_name').asInteger = 1 then
        Font.Style := fsBold
      else
        Font.Style :=0;
    end;
    // Edit
    FindC(tmpForm,'edtData_'+intToStr(tmpCount),tmpEdit,False);
    if tmpEdit = nil then
      tmpEdit := TdbEdit.Create( tmpForm );
    with tmpEdit do
    begin
      visible := True;
      name := 'edtData_'+intToStr(tmpCount);
      parent := tmpParent;
      Font.Size := 11;
      top := tmpCount * 50 + tmpLabel.Height;
      left := 8;
      width := 300;
      tag := tmpDataSet.FieldByName('id').asInteger; //
      tagString := VarToStr( SQLExecute('SELECT id FROM oproperty WHERE id_object = '+IntToStr( efmObject.btnSave.dbGeneralTableId  )+' AND id_cproperty = '+tmpDataSet.FieldByName('id').asString ) );
      text := VarToStr( SQLExecute('SELECT value_s FROM oproperty WHERE id_object = '+IntToStr( efmObject.btnSave.dbGeneralTableId  )+' AND id_cproperty = '+tmpDataSet.FieldByName('id').asString ) );
      dbCurrency := False;
      NumbersOnly := False;
      if tmpControlID = 3 then // Integer number
      begin
        dbCurrency := True;
        dbAccuracy := 0;
        Alignment := taLeftJustify;
      end;
      if tmpControlID = 4 then // Real Number
      begin
        NumbersOnly := True;
      end;
      onChange := 'efmObject_edtEdit_OnChange';
      Font.Style := 0;
    end;
    // ComboBox
    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;
        if tagString <> '' then // если есть ссылочное значение, то синхронизировать выпадающий список
          dbItemID := StrToInt(tagString);
        onChange := 'efmObject_cmbEdit_OnChange';
        Font.Style :=0;
      end;
    end;
    // Date
    if tmpControlID = 5 then 
    begin
      FindC(tmpForm,'dtpData_'+intToStr(tmpCount),tmpDataEdit,False);
      if tmpDataEdit = nil then
      begin
        tmpDataEdit := TdbDateTimePicker.Create( tmpForm );
        AssignEvents(tmpDataEdit);
      end;
      with tmpDataEdit do
      begin
        visible := True;
        name := 'dtpData_'+intToStr(tmpCount);
        parent := tmpParent;
        Font.Size := 11;
        top := tmpCount * 50 + tmpLabel.Height;
        left := 8;
        width := 300;
        ShowCheckBox := True;
        if ValidDate( tmpEdit.Text ) then
        begin
          DateTime := StrToDate( tmpEdit.Text );
          Checked := True;
        end
        else
          Checked := False;
        //
        dbOnChange := 'efmObject_dtpEdit_OnChange';
        Font.Style :=0;
      end;
    end;
    inc(tmpCount);
    tmpDataSet.Next;
  end;
  tmpDataSet.Free;
  tmpForm.ClientHeight := tmpCount * 50 + 48; // adjust the height of the form
end;
Code language: Delphi (delphi)

The date can be entered or not entered – the TdbDateTimePicker component checker is responsible for this

Column width

The column width of a class table view is no longer tied to any specific class property, since different classes can now have the same (inherited) properties. This circumstance led to a change in the data structure and the algorithm for saving data on setting column widths, which are now stored in the class.col_widths field.

The data is stored as a string with integer values separated by commas. Modified procedures for loading and saving the setting are given below.

procedure frmObject_tgrMain_OnChange (Sender: TObject);
// updating data in a table
var
  tmpIDClass: integer;
  tmpSQL: string;
  tmpColumn: integer;
  s: string;
  tmpColWidth: array of string;
begin
  // get ID
  tmpIDClass := frmObject.labIDClass.Tag;
  // read column widths from base
  tmpSQL := 'SELECT COALESCE(col_widths,"") FROM class WHERE id = '+IntToStr(tmpIDClass);
  s := SQLExecute(tmpSQL);
  tmpColWidth := SplitString(s,',');
  for tmpColumn := 0 to frmObject.tgrMain.Columns.Count - 1 do
  begin
    if tmpColumn < length(tmpColWidth) then
      frmObject.tgrMain.Columns[tmpColumn].Width := StrToInt(tmpColWidth[tmpColumn] )
    else
      frmObject.tgrMain.Columns[tmpColumn].Width := 200;
  end;
  frmObject.labIDClass.Caption := ''; // unlock frmObject_tgrMain_OnColumnResize
end;

procedure frmObject_tgrMain_OnColumnResize (Sender: TObject; ACol: Integer);
// column resize
var
  tmpIDClass: integer;
  tmpSQL : string;
  i: integer;
  s: string;
begin
  if frmObject.labIDClass.Caption = '' then
  begin
    s := '';
    for i:=0 to frmObject.tgrMain.Columns.Count - 1 do
      s := s + IntToStr(frmObject.tgrMain.Columns[i].Width) + ',';
    delete(s,length(s),1);
    // remember the widths of all columns in the database
    tmpIDClass := frmObject.labIDClass.Tag; //  класс
    tmpSQL := 'UPDATE class SET col_widths = "'+s+'" WHERE id = '+IntToStr(tmpIDClass);
    SQLExecute(tmpSQL);
  end;
end;
Code language: Delphi (delphi)

Results

The Data Keeper project is developing, algorithms for generating and retrieving data are being refined.

What should be done:

  • Set property support
  • Interval class tree
  • Searching for objects by a given property value (several properties)
  • Displaying dependent objects (objects for which this object is the value of a property)
  • Creation of arbitrary views (using an analogue of SQL for OOP).
  • Filtering (automatic construction of a filter panel for objects of a given class based on a list of properties)
  • Flag for mandatory completion of an object property (analogous to a required field)
  • Flag of the uniqueness of a property of a class object (analogous to the uniqueness of a field value)
  • Global uniqueness flag of an object property (or property with GUID generation)
  • Extension of basic data types (time, yes/no, images/files, etc.)
  • Adding control for data types (analogous to domains) – range of values for numbers and dates, text length for strings, etc.
  • Manually adjusting the position and size of components on the class editing form.
  • Adding new reference values directly from the object editing form.

Links

Leave a Reply

Your email address will not be published. Required fields are marked *