Это наиболее простая операция на пути создания нового модуля. Для создания нового класса необходимо использовать Component Expert из Delphi. Введите имя нового класса, а в качестве родительского класса введите с клавиатуры название уже существующего класса объекта БД. Иерархический список существующих в системе классов приведен в Приложении 2.
Например, класс TDBOMyObject на основе базового класса TDBObject можно создать при помощи эксперта Delphi, выбрав в меню Component->NewComponent. Имя суперкласса для создаваемого класса следует ввести вручную в поле “Ancestor Type”. В поле "Unit file name" введите путь и имя файла. Сохранять файл нового класса, как и файлы его будущих форм, следует только в подкаталоге SYSTEM\CLASSES\. После этого нажмите кнопку "Create Unit".
Рис.3.1. Пример создания нового класса
Далее, в файл CLASSES\RegDBOC.pas необходимо внести название нового модуля Delphi в разделе interface uses, а в процедуры RegisterDBOClasses и UnRegisterDBOClasses необходимо добавить новые строки в соответствии с названием нового класса, например так:
uses
DBObject,
. . .
DBOMyObject,
. . .
procedure RegisterDBOClasses;
begin
RegisterClass( TDBObject );
. . .
RegisterClass( TDBOMyObject );
end;
Все методы можно условно разбить на три группы:
Отдельно рассматриваются методы создания и удаления объекта, поскольку они не отображаются интерфейсной программой в контекстном меню доступных методов, а вызываются непосредственно ядром системы в ответ на соответствующее инициирующее сообщение (вызов создания или удаления пользователем или передача ядру сообщения в режиме CM_CREATE, CM_DELETE – см. модуль DBOConst.pas).
Как можно увидеть из описания API абстрактного класса, он обладает методами всех вышеназванных групп: View – метод просмотра, Edit – метод редактирования, Send – метод пересылки (перемещения) объекта в другую папку. Данные методы являются “публикуемыми” в интерфейсе приложения ONTARIO и доступны из контекстного меню объекта (аналог декларации published Delphi в применении к методам класса). Определены также методы создания и удаления класса – конструктор и деструктор, соответственно. Любой класс, являющийся подклассом базового класса TDBObject, будет обладать перечисленными стандартными свойствами без каких–либо дополнительных усилий со стороны программиста, необходимо просто создать новый класс на базе абстрактного.
Как было сказано ранее, базовый класс имеет набор свойств (атрибутов), которые хранятся в таблице Objs. Для прикладного использования в таблице Objs доступны следующие атрибуты, которые отображаются в окне Проводника:
Табл.3.1.
Свойство объекта |
Соответствующее поле таблицы Objs |
Тип данных |
Наименование | Name | varchar( 128 ) |
Расширение | Ext | varchar( 128 ) |
Дата (создания) | CreationDate | datetime |
Статус (состояние) | State | varchar( 16 ) |
Сумма | Summ | money |
Флаг | Attrib | int |
При этом, сам базовый класс использует только атрибут "Наименование", в чем можно убедиться, создав из клиентского приложения в базе данных новый объект класса "Абстрактный объект" (TDBObject).
При создании нового класса следует определить, достаточно ли для хранения свойств нового класса стандартных свойств базового класса или необходимо хранить еще какие–либо другие данные. Например, для класса “Пользователь”, очевидно, хватает и стандартных свойств: в “Наименование” будем хранить ФИО, а в “Расширении” – имя регистрации на SQL–сервере. Однако, для класса “Товарная позиция” или "Сотрудник" стандартного набора очевидно не хватит.
В общем случае, для хранения свойств нового класса необходимо создать новую таблицу-расширение, которая будет связана с Objs по идентификатору объекта OID. То есть, создаваемая таблица обязательно должна иметь атрибут (поле) целого типа (int) для связи с главной таблицей Objs по ключу OID. Название такого поля не имеет особого значения, но в соответствии с принятыми соглашениями это может быть тот же OID или <имя класса>ID и т.п.
Например, для хранения свойств класса "Сотрудник" в базе данных присутствует таблица-расширение с названием Staff, где перечислены дата и место рождения, паспортные данные, должность и т.п.
Для занесения новых данных при создании объекта следует создать реализацию метода создания в виде процедуры <имя класса>_Create (реализация серверной части метода), которая будет вставлять новую запись в таблицу со значениями свойств создаваемого объекта. Например, серверная часть метода создания класса "Прайс-лист" (TDBOPrList) выглядит следующим образом:
CREATE PROCEDURE TDBOPrList_Create
@OID int,
@CurrencyID int
as
INSERT INTO PrLists( PrListID, CurrencyID )
VALUES ( @OID, @CurrencyID )
GO
Для удаления свойств объекта из БД необходимо создать реализацию серверной части метода удаления объекта в виде процедуры <имя класса>_Delete. Процедура должна проверять, удалены ли уже атрибуты объекта из таблицы Objs и в этом случае также удалить информацию из таблицы расширения. Такая проверка необходима, так как удаление может быть и логическим, при котором информация не удаляется, а объект только помечается удаленным (Objs.Deleted = 1) и перемещается в папку "Удаленные". Пример для класса "Прайс лист":
CREATE PROCEDURE TDBOPrList_Delete
@OID int
as
if NOT EXISTS( SELECT OID FROM Objs WHERE OID = @OID )
DELETE FROM PrLists WHERE PrListID = @OID
GO
Методы создания и удаления всегда вызываются ядром системы внутри транзакции. В случае ошибки вся операция отменяется целиком.
Необходимость переопределения методов очевидна, ведь с каждым потомком класс объекта БД усложняется, дополняется новыми свойствами (атрибутами) и процедурами их обработки. Например класс “Пользователь” в дополнении к абстрактному классу использует атрибут Ext таблицы Objs для хранения имени регистрации на SQL Server, вследствие чего требует при редактировании отображать это поле и записывать в него изменения. Кроме того, при удалении объекта “Пользователь” необходимо дополнительно удалять его из всех рабочих групп и удалять его личную папку.
Для реализации этого используются переопределения стандартных методов или создание новых, специфичных для данного класса объектов. Метод, как правило, должен иметь форму, в минимальном случае это диалог подтверждения операции на базе TfrmBaseAction.
Новые формы создаются по следующему правилу. Если метод уже присутствует в родительском классе и будет переопределяться, то новую форму следует создавать наследуемой от нее, например метод редактирования. Если же реализуется новый метод, специфичный для данного класса, например изменение состава рабочей группы, то форма наследуется от одной из трех базовых –
TfrmBaseView, TfrmBaseEdit или TfrmBaseAction, в зависимости от назначения метода.Если метод имеет форму и будет переопределен, то весь код состоит из двух строчек, например:
procedure TDBOUser.View;
if MethodFormRef = nil then MethodFormRef := TfrmDBOUserView;
inherited View;
end;
Здесь объектная ссылка на создаваемый новый класс форм TfrmDBOUserView присваивается свойству MethodFormRef, если та еще не инициализирована потомками, а далее просто следует вызвать переопределяемый метод.
Если метод создается “с нуля”, код немного усложнится:
procedure TDBObject.View;
var frmView : TfrmBaseDialog;
begin
if MethodFormRef = nil then MethodFormRef := TfrmDBOView;
frmView := MethodFormRef.Create( Application );
frmView.InitDataEnvironment( DBOInfo );
end;
Здесь объектная ссылка на создаваемый новый класс форм TfrmDBOView также присваивается свойству MethodFormRef, а затем создается экземпляр формы и инициализируется его данные.
Проектирование новой формы переопределяемого метода не представляет особенных проблем. В меню нажимаем File® New... и на закладке ONTARIO выбираем родительскую форму переопределяемого метода, например, для метода Edit класса TDBOUser, создаем новую форму, наследуемую от TfrmDBOEdit. Как видно, в форме уже заложена логика редактирования для абстрактного класса, то есть возможность изменять и записывать в базу свойство “Наименование”. Для начала изменим имя формы на TfrmDBOUserEdit, заголовок окна самой формы на “Пользователь” и свойство “Наименование” на “Ф.И.О.”. Теперь сохраним форму в файле UserEdit.pas каталога \ONTARIO\CLASSES\ и перейдем в модуль DBOUser.pas и переопределим метод Edit:
procedure TDBOUser.Edit;
begin
if MethodFormRef = nil then MethodFormRef := TfrmDBOUserEdit;
inherited Edit;
end;
А в заголовке Interface Uses добавим название файла модуля – UserEdit. Можно запускать компиляцию и на выполнение, после чего в ответ на выбор “Редактирование” для объекта класса “Пользователь” на экране появится только что сотворенная нами форма с измененным заголовком и названием свойства. Но успокаиваться на достигнутом пока рано, ведь ничего существенно нового, по сравнению с базовым классом, пользователь не умеет. Научим его хранить в поле Ext собственное имя регистрации пользователя на SQL Server. Для этого добавим в форму новый компонент TDBOMaskEdit из палитры компонентов ONTARIO и привяжем его к полю Ext уже имеющегося источника данных. Для того, чтобы сохранить изменения в этом поле, необходимо создать процедуру его записи в базу, то есть ту часть метода Edit, которая хранится на сервере (см. файл ONTARIO\SERVER\Kernel\Users.sql).
CREATE PROCEDURE TDBOUser_Save
@OID int,
@Login varchar( 128 )
AS
UPDATE Objs
SET Ext = @Login
WHERE OID = @OID
GO
В форме создадим компонент TStoredProc, назовем его spTDBOUser_Save, свяжем его с DatabaseName = dbOntario (dbOntario класса TDataBase – основной и единственный компонент связи с БД в приложении), в качестве процедуры выберем TDBOUser_Save, определим тип передаваемых параметров. Теперь, чтобы это все заработало, как уже было сказано, необходимо переопределить стандартный метод SaveDataEnvironment также стандартным способом:
procedure TfrmDBOUserEdit.SaveDataEnvironment;
begin
inherited;
spTDBOUser_Save.Active := false; { на всякий случай }
spTDBOUser_Save.ParamByName( '@OID' ).AsInteger := DBOInfo.OID;
spTDBOUser_Save.ParamByName( '@Login' ).AsString := edtLogin.EditText;
spTDBOUser_Save.ExecProc;
RetCode := spTDBOUser_Save.ParamByName( 'Result' ).AsInteger;
end;
Теперь - и это легко проверить - класс “Пользователь” умеет хранить и редактировать в поле Ext свое имя регистрации, причем, если попытка записи в БД этого имени по каким–либо причинам оказалась неудачной, транзакция откатывает все изменения, в том числе и “Ф.И.О.”.
В данном примере мы использовали уже имеющийся источник данных, однако в ближайшем будущем потребуется добавлять в форму другие источники, так как новые классы будут расширять номенклатуру свойств за счет полей дополнительных таблиц БД, связанных с основной Objs по ключу OID. Для этого следует создать процедуру, возвращающую множество значений этих свойств, связать ее с компонентом TStoredProc и далее с источником данных и компонентами отображения данных. Для инициализации процедуры и источника данных следует переопределить стандартный метод формы InitDataEnvironment и прописать в нем код инициализации параметров и открытия компонента TStoredProc, как это сделано, например в форме TfrmDBOStaffCardView. Новый источник данных в формах для редактирования обязательно должен быть связан со свойством формы DataChanged. Для этого в новых компонентах TDataSource будущих форм достаточно в процедуре обработки события OnDataChange внести ссылку на метод родительской формы edtNameChange.
Новую форму следует исключить из автоматически создаваемых при запуске. Кроме того, если форма не будет в дальнейшем использоваться для наследования, ее следует вовсе исключить из проекта.
Для создания формы “с нуля” правила остаются прежними, изначально в форме не будет никаких компонентов, хотя она и будет уметь определять, когда надо вызывать метод
SaveDataEnvironment и управлять транзакциями.Если новый метод не связан с экранной формой, а, например, выполняет некоторые действия, то его код не подлежит регламентации, за исключением того, что если происходит изменение базы данных, необходимо самостоятельно инициировать начало транзакции, обработку возможных ошибок (исключительных ситуаций) и фиксацию или откат транзакции. Как правило, все методы, производящие изменения в БД связаны с какой-либо формой, например, с диалогом "ОК-Отмена" формы класса TfrmBaseAction, и не требуют явного управления транзакциями.
Пример метода, не имеющего экранной формы:
<Название метода>;
var
spTDBODocTemplate_View: TStoredProc;
ProcParam : TProcParam;
DBOObjetcRef : TDBObjectRef;
NewDoc : TDBODoc;
DBODocInfo : TDBODocInfo;
begin
spTDBODocTemplate_View :=
TStoredProc.Create( Self );
spTDBODocTemplate_View.DatabaseName :=
DatabaseName;
spTDBODocTemplate_View.StoredProcName :=
'TDBODocTemplate_View';
ProcParam := TParam.Create(
spTDBODocTemplate_View.Params, ptInput );
ProcParam.Name := '@OID';
spTDBODocTemplate_View.ParamByName( '@OID'
).DataType := ftInteger;
spTDBODocTemplate_View.ParamByName( '@OID'
).AsInteger := DBOInfo.OID;
ProcParam := TParam.Create(
spTDBODocTemplate_View.Params, ptResult );
ProcParam.Name := 'Result';
spTDBODocTemplate_View.ExecProc;
end;
Методом “по умолчанию” объекта является выполняемый по его открытию пользователем в клиентском приложении. Объект может быть открыт одним из способов: пользователь нажимает “Ctrl+O”, “Enter” или делает двойной щелчок мыши на объекте в окне проводника.
В системе определено свойство абстрактного объекта DefaultMethod. Первоначально это метод просмотра View, но программист может переназначить метод “по умолчанию” в конструкторе нового класса и тогда при открытии объекта будет выполняться уже другой метод. Например, так при двойном щелчке на объекте “Отчет” он запускается:
constructor TDBOReport.Create( Owner: TComponent );
begin
inherited Create( Owner );
DefaultMethod := ViewParams;
AddMethodAfter( 'ViewParams', 'View', 'Запустить отчет', ViewParams,
DBO_ACCESS_READ );
end;
Иногда бывает необходимым эмулировать активизацию некоторого объекта м выполнение его метода из какого–либо участка программы клиентского приложения, как если бы пользователь раскрыл контекстное меню объекта и выбрал там некоторое действие. Для этого используется механизм сообщений, пересылаемых главной форме приложения. Например, данный фрагмент кода активизирует объект с OID=SomeOID и запустит его метод “по умолчанию”.
. . .
SendMessage( Application.MainForm.Handle, WM_DBO_SENDER, Self.Handle, 0 );
PostMessage( Application.MainForm.Handle, WM_DBO_DISPATCH, CM_VIEW, SomeOID );
. . .
Вместо CM_VIEW можно использовать другие константы (описаны в DBOConst.pas):
CM_ACTIVATE = 0; { с активизацией меню методов }
CM_CREATE = 1; { только с вызовом метода создания }
CM_DELETE = 2; { только с вызовом метода удаления }
CM_MOVE = 3; { только с вызовом метода перемещения }
CM_VIEW = 4; { только с вызовом метода по умолчанию }
CM_PRINT = 5; { только с вызовом метода печати - по умолчанию }
Как было показано в предыдущей главе, любой объект может инициировать в системе создание другого объекта. Клонирование осуществляется вызовом метода CreateClone абстрактного объекта. Первоначально этот метод не публикуемый и для того, чтобы им мог воспользоваться пользователь, его надо опубликовать в контекстном меню. Наиболее простой вариант, когда создается объект того же класса, что и инициирующий это создание. Происходит простое клонирование объекта. Так, например, можно клонировать товарную позицию и создать на ее основе новую.
constructor TDBOGood.Create( Owner: TComponent );
begin
. . .
// публикуем метод клонирования
AddMethodAfter( 'CreateClone', 'ViewRemain', 'Клонировать товарную
позицию', CreateClone, DBO_ACCESS_CREATE );
. . .
end;
А в форме создания данного объекта класса следует переопределить виртуальный метод InitClone( MasterOID: integer ). В качестве параметра MasterOID метод получает OID инициировавшего клонирование объекта:
procedure TfrmDBOGoodCreate.InitClone( MasterOID:
integer );
begin
inherited InitClone( MasterOID );
spTDBOGood_View.Active := false;
spTDBOGood_View.ParamByName( '@OID' ).AsInteger := MasterOID;
spTDBOGood_View.Open;
// инициализация атрибутов
edtName.Text := DBOInfo.Name;
edtBarCode.Text := DBOInfo.Ext;
edtCatCode.Text := spTDBOGood_View.FieldByName( 'CatCode' ).AsString;
edtCatName.Text := spTDBOGood_View.FieldByName( 'CatName' ).AsString;
edtProducer.Text := spTDBOGood_View.FieldByName( 'Producer' ).AsString;
edtCountry.Value := spTDBOGood_View.FieldByName( 'CountryID' ).AsInteger;
edtMeasure.Value := spTDBOGood_View.FieldByName( 'MeasureID' ).AsInteger;
edtMass.Value := spTDBOGood_View.FieldByName( 'Mass' ).AsFloat;
edtDX.Value := spTDBOGood_View.FieldByName( 'DX' ).AsFloat;
edtDY.Value := spTDBOGood_View.FieldByName( 'DY' ).AsFloat;
edtDZ.Value := spTDBOGood_View.FieldByName( 'DZ' ).AsFloat;
edtTax.Value := spTDBOGood_View.FieldByName( 'TaxSchemeID' ).AsInteger;
spTDBOGood_View.Active := false;
// клонирование списка (вхождение в группы)
spTDBOGood_ViewGroups.Active := false;
spTDBOGood_ViewGroups.ParamByName( '@OID' ).AsInteger := MasterOID;
spTDBOGood_ViewGroups.Open;
while not spTDBOGood_ViewGroups.EOF do begin
dbgGroups.AddLogAction( spTDBOGood_ViewGroups.FieldByName( 'GroupID'
).AsInteger, DBG_INSERTED );
spTDBOGood_ViewGroups.Next;
end;
spTDBOGood_ViewGroups.First;
end;
Если необходимо не просто клонировать объект, а создать объект другого класса, то здесь изменяется только публикация метода в контекстном меню. Необходимо создать некоторый новый метод в инициирующем классе, состоящий из двух строк кода: определения названия класса и вызова метода клонирования абстрактного объекта. Например, расходная накладная (
TDBOOutInv) создает счет–фактуру (TDBOFInv) на своей основе.// новый метод создания
procedure TDBOOutInv.CreateFInv;
begin
CloneClassName := 'TDBOFInv';
CreateDBOClone;
end;
constructor TDBOOutInv.Create( Owner : TComponent );
begin
. . .
// публикуем новый метод
AddMethodAfter( 'CreateFInv', 'Edit', 'Создать счет-фактуру на
основе', CreateFInv, DBO_ACCESS_CREATE );
. . .
end;
В форме же создания счета–фактуры точно так же переопределяется виртуальный метод InitClone( MasterOID: integer ), который в качестве параметра
MasterOID получит в этом случае также OID инициировавшего создание объекта. Только это уже будет объект другого класса, в данном примере, OID расходной накладной.В системе определен механизм шаблонов для облегчения создания некоторых классов объектов по заданному образцу. Например, документы часто имеют один и тот же заголовок, поэтому нет нужды каждый раз заставлять пользователя его заполнять, достаточно предоставить ему для этого шаблон.
Все создаваемы шаблоны должны наследоваться от класса шаблонов абстрактного объекта (TDBOTemplate). Этот класс имеет публикуемый в контекстном меню метод “Создать объект по шаблону” (CreateFromTemplate), который инициирует в системе создание объекта заданного в шаблоне класса в заданной папке, с заданным наименованием, расширением, датой. Из этих параметров обязательным для заполнения в шаблоне является только класс создаваемого объекта.
Программисту необходимо лишь дополнить атрибутику шаблона точно так же, как он расширяет обычный класс. В форме же создания для объекта класса, указываемого в шаблоне, необходимо переопределить виртуальный метод InitFromTemplate( TemplateID: integer ), который в качестве параметра получает OID объекта–шаблона. Например, так создается по шаблону абстрактный документ (TDBODoc), а, соответственно, и все его потомки:
procedure TfrmDBODocCreate.InitFromTemplate(
TemplateID: integer );
var
spTDBODocTemplate_View: TStoredProc;
ProcParam : TParam;
begin
inherited;
spTDBODocTemplate_View := TStoredProc.Create( Self );
spTDBODocTemplate_View.DatabaseName := spTDBObject_Create.DatabaseName;
spTDBODocTemplate_View.StoredProcName := 'TDBODocTemplate_View';
ProcParam := TParam.Create( spTDBODocTemplate_View.Params, ptInput );
ProcParam.Name := '@OID';
spTDBODocTemplate_View.ParamByName( '@OID' ).DataType := ftInteger;
spTDBODocTemplate_View.ParamByName( '@OID' ).AsInteger := TemplateID;
ProcParam := TParam.Create( spTDBODocTemplate_View.Params, ptResult );
ProcParam.Name := 'Result';
spTDBODocTemplate_View.ParamByName( 'Result' ).DataType := ftInteger;
spTDBODocTemplate_View.Open;
DBODocTemplateInfo.Subj1ID := spTDBODocTemplate_View.FieldByName( 'Subj1ID'
).AsInteger;
DBODocTemplateInfo.Subj2ID := spTDBODocTemplate_View.FieldByName( 'Subj2ID'
).AsInteger;
DBODocTemplateInfo.WhereID := spTDBODocTemplate_View.FieldByName( 'WhereID'
).AsInteger;
DBODocTemplateInfo.CurrencyID := spTDBODocTemplate_View.FieldByName( 'CurrencyID'
).AsInteger;
DBODocTemplateInfo.RateID := spTDBODocTemplate_View.FieldByName( 'RateID'
).AsInteger;
DBODocTemplateInfo.TaxSchemeID := spTDBODocTemplate_View.FieldByName( 'TaxSchemeID'
).AsInteger;
DBODocTemplateInfo.TargetID := spTDBODocTemplate_View.FieldByName( 'TargetID'
).AsInteger;
spTDBODocTemplate_View.Destroy;
edtWhere.Value := DBODocTemplateInfo.WhereID;
end;
При реализации данного метода рассматривается то обстоятельство, что в потомках, например, операционном документе можно будет пользоваться уже готовой структурой
DBODocTemplateInfo без необходимости вызывать процедуру просмотра атрибутов объекта–шаблона TDBODocTemplate_View.procedure
TfrmDBOOperDocCreate.InitFromTemplate( TemplateID: integer );
begin
inherited;
edtSubj1.Value := DBODocTemplateInfo.Subj1ID;
edtSubj2.Value := DBODocTemplateInfo.Subj2ID;
edtCurrency.Value := DBODocTemplateInfo.CurrencyID;
edtRate.Value := DBODocTemplateInfo.RateID;
edtTaxScheme.Value := DBODocTemplateInfo.TaxSchemeID;
edtTarget.Value := DBODocTemplateInfo.TargetID;
end;