24 августа 2021

Работа с любыми структурами данных через DB-Aware контролы

    DB-Aware контролы в Delphi значительно упрощают жизнь разработчикам GUI-программ работающих с базами данных. Они многое делают сами без написания кода – отображают данные, позволяют пользователям их модифицировать и сохраняют изменения в базу данных. Но, что делать, если данные хранятся не в базе данных, а в массиве, списке, объекте или какой-нибудь другой структуре? Можно воспользоваться "memory table" – потомком TDataSet, который хранит данные в памяти. Скопировать в него данные, отобразить, обработать и скопировать обратно. Вариантов таких "memory table" много: TClientDataSet, TFDMemTable из FireDAC, TkbmMemTable, TVirtualTable из UniDAC, TMemTableEh из EhLib... Но есть способ решить этот вопрос проще, без копирования данных туда-сюда.
    Поможет нам с этой задачей TVirtualDataSet из UniDAC. TVirtualDataSet – это потомок TDataSet, который не хранит данные, а является посредником между ними и работающими с TDataSet контролами или функциями. Он позволяет представить любую структуру данных (массив, список, объект и т. д.) в качестве TDataSet.
    TVirtualDataSet взаимодействует с данными с помощью обработчиков событий. Для минимальной работы необходимо написать два обработчика:
  • OnGetRecordCount – вызывается, когда TVirtualDataSet запрашивает количество записей;
  • OnGetFieldValue – вызывается, когда TVirtualDataSet запрашивает значение поля.
    Например, в программе данные имеют тип TPerson record:
type
  TPerson = record
    ID : Integer;
    FIO: String;
    AGE: Integer;
    constructor Create(const AID: Integer; const AFIO: String; const AAGE: Integer);
  end;

implementation

{ TPerson }

constructor TPerson.Create(const AID: Integer; const AFIO: String; const AAGE: Integer);
begin
  ID  := AID;
  FIO := AFIO;
  AGE := AAGE;
end;
Создадим новый VCL-проект, в котором для хранения данных объявим динамический массив FPersons с этементами типа TPerson. На главную форму проекта поместим TDBGrid для отображения данных и TVirtualDataSet с полями, соответствующими типу TPerson (ID: TIntegerField, FIO: TWideStringField и AGE: TIntegerField), который свяжем с массивом FPersons с помощью обработчиков OnGetRecordCount и OnGetFieldValue.
unit Main;

interface

uses
  System.Classes, System.SysUtils,
  Vcl.Forms, Vcl.Controls, Vcl.Grids, Vcl.DBGrids,
  Data.DB, MemDS, VirtualDataSet, DBAccess, Uni;

type
  TMainForm = class(TForm)
    dgPersons: TDBGrid;
    tPersons: TVirtualDataSet;
    tPersonsID: TIntegerField;
    tPersonsFIO: TWideStringField;
    tPersonsAGE: TIntegerField;
    dsPersons: TUniDataSource;
    procedure FormCreate(Sender: TObject);
    procedure tPersonsGetRecordCount(Sender: TObject; out Count: Integer);
    procedure tPersonsGetFieldValue(Sender: TObject; Field: TField; RecNo: Integer; out Value: Variant);
  private
    FPersons: TArray<TPerson>;
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  SetLength(FPersons, 3);

  FPersons[0] := TPerson.Create(123, 'Иванов Иван Иванович', 25);
  FPersons[1] := TPerson.Create(124, 'Петров Петр Петрович', 30);
  FPersons[2] := TPerson.Create(125, 'Сидоров Сидор Сидорович', 27);

  tPersons.Open;
end;

procedure TMainForm.tPersonsGetRecordCount(Sender: TObject; out Count: Integer);
begin
  Count := Length(FPersons);
end;

procedure TMainForm.tPersonsGetFieldValue(Sender: TObject; Field: TField; RecNo: Integer; out Value: Variant);
begin
  case Field.FieldNo of
    1: Value := FPersons[RecNo - 1].ID;
    2: Value := FPersons[RecNo - 1].FIO;
    3: Value := FPersons[RecNo - 1].AGE;
  end;
end;

end.
Обработчик OnGetFieldValue (tPersonsGetFieldValue) вызывается каждый раз, когда TVirtualDataSet необходимо для строки данных (параметр RecNo: Integer) получить значение поля (параметр Field: TField). Обратите внимание, что нумерация свойства TField.FieldNo, в отличие от индекса TDataSet.Fields начинается с 1. Запускаем программу и в TDBGrid видим содержимое массива FPerson:
    Простым примером применения этого механизма может быть использование массива в качестве LookupDataSet для lookup-поля. Но с помощью TVirtualDataSet мы можем не только отображать данные произвольной структуры, но и их модифицировать. Для этого необходимо реализовать еще три обработчика:
  • OnInsertRecord – вызывается, когда в TVirtualDataSet добавляется новая запись;
  • OnModifyRecord – вызывается, когда в TVirtualDataSet модифицируется запись;
  • OnDeleteRecord – вызывается, когда в TVirtualDataSet удаляется запись.
    В программе изменим тип поля FPersons с массива на список и дополним TVirtualDataSet обработчиками OnInsertRecord, OnModifyRecord и OnDeleteRecord. А на форму добавим TDBNavigator, который свяжем с TVirtualDataSet.
unit Main;

interface

uses
  System.Classes, System.SysUtils, System.Generics.Defaults, System.Generics.Collections,
  Vcl.Forms, Vcl.Controls, Vcl.Grids, Vcl.DBGrids,
  Data.DB, MemDS, VirtualDataSet, DBAccess, Uni, Vcl.ExtCtrls, Vcl.DBCtrls;

type
  TMainForm = class(TForm)
    dgPersons: TDBGrid;
    tPersons: TVirtualDataSet;
    tPersonsID: TIntegerField;
    tPersonsFIO: TWideStringField;
    tPersonsAGE: TIntegerField;
    dsPersons: TUniDataSource;
    DBNavigator: TDBNavigator;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure tPersonsGetRecordCount(Sender: TObject; out Count: Integer);
    procedure tPersonsGetFieldValue(Sender: TObject; Field: TField; RecNo: Integer; out Value: Variant);
    procedure tPersonsInsertRecord(Sender: TObject; var RecNo: Integer);
    procedure tPersonsModifyRecord(Sender: TObject; var RecNo: Integer);
    procedure tPersonsDeleteRecord(Sender: TObject; RecNo: Integer);
  private
    FPersons: TList<TPerson>;
  end;

var
  MainForm: TMainForm;

implementation

{$R *.dfm}

procedure TMainForm.FormCreate(Sender: TObject);
begin
  FPersons := TList<TPerson>.Create(TComparer<TPerson>.Construct(function(const Left, Right: TPerson): Integer
                                                                  begin
                                                                    Result := CompareStr(Left.FIO, Right.FIO);
                                                                  end));
  FPersons.Add(TPerson.Create(125, 'Сидоров Сидор Сидорович', 27));
  FPersons.Add(TPerson.Create(123, 'Иванов Иван Иванович', 25));
  FPersons.Add(TPerson.Create(124, 'Петров Петр Петрович', 30));
  FPersons.Sort;

  tPersons.Open;
end;

procedure TMainForm.FormDestroy(Sender: TObject);
begin
  tPersons.Close;
  FPersons.Free;
end;

procedure TMainForm.tPersonsGetRecordCount(Sender: TObject; out Count: Integer);
begin
  Count := FPersons.Count;
end;

procedure TMainForm.tPersonsGetFieldValue(Sender: TObject; Field: TField; RecNo: Integer; out Value: Variant);
begin
  case Field.FieldNo of
    1: Value := FPersons[RecNo - 1].ID;
    2: Value := FPersons[RecNo - 1].FIO;
    3: Value := FPersons[RecNo - 1].AGE;
  end;
end;

procedure TMainForm.tPersonsInsertRecord(Sender: TObject; var RecNo: Integer);
var
  NewPerson: TPerson;
begin
  NewPerson := TPerson.Create(tPersonsID.Value, tPersonsFIO.Value, tPersonsAGE.Value);
  FPersons.Add(NewPerson);
  FPersons.Sort;
  RecNo := FPersons.IndexOf(NewPerson) + 1;
end;

procedure TMainForm.tPersonsModifyRecord(Sender: TObject; var RecNo: Integer);
var
  TempPerson: TPerson;
begin
  TempPerson := FPersons[RecNo - 1];
  TempPerson.ID  := tPersonsID.Value;
  TempPerson.FIO := tPersonsFIO.Value;
  TempPerson.AGE := tPersonsAGE.Value;
  FPersons[RecNo - 1] := TempPerson;
  FPersons.Sort;
  RecNo := FPersons.IndexOf(TempPerson) + 1;
end;

procedure TMainForm.tPersonsDeleteRecord(Sender: TObject; RecNo: Integer);
begin
  FPersons.Delete(RecNo - 1);
end;

end.
В FormCreate при создании списка я добавил ему компаратор, который будет сортировать список по полю "FIO". Поэтому для наглядности я заполняю FPersons в произвольном порядке. Посмотрим, как работает эта версия программы.
    Для начала в конец таблицы добавим еще некоего Билла Гейтса.
Нажатие на кнопку DBNavigator "Сохранить" вызвало у TVirtualDataSet метод Post и обработчик OnInsertRecord (tPersonsInsertRecord), который дополнил наш список этим почтенным ИТ-пенсионером, пересортировал список и изменил позицию текущей записи TVirtualDataSet на новую запись вернув ее новый номер через параметр RecNo.
    Теперь переименуем Петрова в Гарри Гаррисона. Нажатие на кнопку DBNavigator "Сохранить" вызвало у TVirtualDataSet метод Post и обработчик OnModifyRecord (tPersonsModifyRecord), который изменил значение элемента списка, пересортировал список и изменил позицию текущей записи TVirtualDataSet если после сортировки она переместилась в списке. Обратите внимание на параметр RecNo. В этом обработчике он имеет двойное назначение – сначала указывает на номер модифицированной записи, а потом на ее новый номер после сортировки.
    Напоследок удалим из нашего списка Иванова. Обработчик OnDeleteRecord (tPersonsDeleteRecord) удалил из списка запись по указанному в параметре RecNo индексу.
    Как видите, TVirtualDataSet и несколько простых обработчиков могут позволить программисту работать с любой структурой данных как с TDataSet.

Комментариев нет:

Отправить комментарий