45803 (MIDAS. Практическое применение), страница 3
Описание файла
Документ из архива "MIDAS. Практическое применение", который расположен в категории "". Всё это находится в предмете "информатика" из , которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "рефераты, доклады и презентации", в предмете "информатика, программирование" в общих файлах.
Онлайн просмотр документа "45803"
Текст 3 страницы из документа "45803"
Теперь все просто: все модули данных, которые работают с документами, используют эти две функции, и если RegisterDoc возвращает false (а это произойдет только в том случае, если номер уже есть в списке), то пользователю выдается сообщение, что с документом уже работают. Функция UnregisterDoc просто удаляет номер из списка.
На клиенте понадобится, кроме доступа к двум провайдерам, еще пара функций – получение нового значения CLIENT_ID для справочника клиентов и получение полного имени клиента. Для этого необходимо создать описание этих функций в библиотеке типов.
В зависимости от того, какой синтаксис используется в редакторе библиотеки типов (IDL или Pascal), объявление этих функций выглядит по-разному, ниже приведены их описания в protected-секции модуля данных:
protected class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override; function NewClientID: Integer; safecall; function Get_ClientName(ClientID: Integer): WideString; safecall; |
На IDL это выглядит так:
[id(0x00000001)] HRESULT _stdcall NewClientID([out, retval] long * Result); [propget, id(0x00000004)] HRESULT _stdcall ClientName([in] long ClientID, [out, retval] BSTR * Value); |
Реализация этих функций довольно проста. Надо вызвать хранимые процедуры, и выдать возвращаемое ими значение в качестве результата:
function TrdmCommon.NewClientID: Integer; begin lock; with spNewID do try ExecProc; Result := paramByName('ID').AsInteger; finally unlock; end; end; function TrdmCommon.Get_ClientName(ClientID: Integer): WideString; begin lock; try with spClientFullName do begin paramByName('ID').AsInteger := ClientID; ExecProc; Result := paramByName('FULL_NAME').AsString; end; finally unlock; end; end; |
Теперь основной модуль готов, и можно перейти к написанию следующего модуля данных, предназначенного для работы с документом.
Рисунок 3.
Здесь уже все немного сложнее. Разумеется, здесь тоже есть соединение с базой ibdDoc, настроенное на сервер БД. Хранимая процедура spNewID выдает на этот раз номер для нового документа, используя процедуру DOC_TITLE_ID, аналогичную процедуре CLIENT_ID.
На этот раз в модуле данных, помимо компонентов запросов к серверу, присутствуют два компонента TСlientDataSet и два провайдера данных. Эти дополнительные компоненты предназначены именно для организации расчетов на сервере. Поскольку, как мы договорились, на сервере приложений должна рассчитываться сумма документа, то на нем должно быть известно содержимое документа до того, как оно будет сохранено в БД. Разумеется, это можно осуществить, используя события провайдера для предварительной обработки пакета изменений, поступившего от клиентской части, но мне хотелось показать возможность организации работы с документом как с полноценным объектом.
Идея простая: пусть весь удаленный модуль данных работает с одним документом. В таком случае этот модуль будет выглядеть для клиентской части как полноценный объект, владеющий всеми данными документа и предоставляющий клиентской части все необходимые свойства. Разумеется, от пакетов данных никто не отказывается.
Таким образом, организуется следующий алгоритм работы: Клиентская часть создает на сервере либо новый документ, либо открывает существующий (удаление документов уже реализовано). Сервер приложений создает модуль данных и, если необходимо, закачивает в него содержимое документа с сервера БД. После этого клиентская часть и удаленный модуль данных совместно обрабатывают эти данные, занимаясь каждый своим делом: клиентская часть предоставляет средства для изменения этих данных пользователем, а удаленный модуль производит все необходимые расчеты.
К одному компоненту транзакции ibtDoc присоединено на этот раз два запроса ibqTitle и ibqBody, соответственно выбирающих одну строку заголовка документа (select * from DOC_TITLE where DOC_ID = :DocID) и все строки этого документа (select * from DOC_BODY where DOC_ID = :DocID).
ПРИМЕЧАНИЕ Хотя MIDAS требует наличия своей IBTransaction для каждой пары компонентов "IBQuery-провайдер", в данном случае это необязательно. Провайдеры не будут начинать и завершать транзакции, открываться и закрываться транзакция будет явно, в соответствующих методах. |
К этим запросам присоединены провайдеры dspTitleInner и dspBodyInner, назначение которых – получить данные с сервера БД и передать их в соответствующие ClientDataSet. Свойство Exported у этих провайдеров установлено в false, они нужны только внутри сервера приложений, и видеть их на клиентской части незачем. Соответственно, клиентский набор данных cdsTitle (компонент TClientDataSet) получает одну строку заголовка из dspTitle и cdsBody, содержимое документа из dspBody.
Для того, чтобы клиентская часть могла получать и изменять данные документа, к клиентским наборам данных cdsTitle и cdsBody присоединены провайдеры данных, dspTitle и dspBody, соответственно. Свойству Exported этих провайдеров оставлено значение по умолчанию, True, зато свойство ResolveToDataSet установлено в True, для того, чтобы эти провайдеры не пытались работать с ClientDataSet с помощью запросов. Таким образом, клиентская часть может получать и изменять данные не из TIBQuery, но из TClientDataSet, причем совершенно об этом не догадываясь. По команде с клиентской части изменения, передаются серверу приложений, который и сохраняет их в БД.
Теперь посмотрим, что нам нужно для подобной реализации. Функции для синхронизации обработки документов RegisterDoc и UnregisterDoc уже есть, нужно их только использовать. С их помощью гарантируется, что одновременно один и тот же документ редактироваться не будет, поэтому у провайдеров данных dspTitleInner и dspTitleBody достаточно установить UpdateMode = upWhereKeyOnly, и указать ключевые поля у запросов. Содержимое документа может состоять из нескольких строк, поэтому у dspBodyInner и dspBody нужно установить флаг poAllowMultiRecordUpdates. Теперь нужно разобраться с полями клиентских наборов данных, установив у них соответствующие свойства. Я остановлюсь здесь только на свойстве ProviderFlags. Поскольку поле «Ссылка на документ» (DOC_ID) на клиентской части не нужно, ему можно задать флаг pfHidden. Разумеется, у всех ключевых полей (DOC_ID и LINE_NUM) и в наборе данных заголовка, и в содержимом документа надо указать флаг pfInKey. У провайдеров dspTitle и dspBody нужно установить политику обновления UpdateMode = upWhereKeyOnly, клиентская часть у модуля данных одна, и другие значения совершенно ни к чему.
Теперь компоненты для хранения и обработки данных подготовлены, осталось написать сами методы работы с ними.
Давайте разберемся, что именно требуется. Модуль rdmDoc предназначен как для создания нового документа, так и для редактирования существующего. Этот модуль можен находиться в одном из трех состояний, описанных в перечислении TObjState:
osInactive: данных нет, документ не редактируется,
osInsert: создан новый документ и
osUpdate – происходит изменение существующего документа.
Состояние хранится в переменной Fstate, находящейся внутри модуля. Сразу после создания и после окончания обработки документа модуль данных должен находиться в неактивном состоянии.
Переход из одного состояния в другое должен обеспечиваться соответствующими методами. Я назвал эти методы DoInactiveState (перевод в неактивное состояние), DoOpen (открыть существующий документ) и DoCreateNew (создание нового документа). При редактировании или добавлении документа нужно знать его уникальный номер, записываемый в поле DOC_ID. Для этого достаточно объявить в секции private переменную FDocID: integer, которая и будет его хранить.
В библиотеке типов нужно реализовать методы, которые будут создавать документ или открывать существующий, а также сохранять изменения. Кроме этого, понадобится свойство, позволяющее получить в любой момент сумму по документу. Сумма каждой строки содержимого пусть рассчитывается на клиентской части.
Итак, приступим. Сначала описываются методы перехода между состояниями, они предназначены для внутреннего использования, и поэтому их объявления содержатся в секции private:
procedure DoInactiveState; procedure DoCreateNew; procedure DoOpen(DocID: integer); |
Рассмотрим их по порядку.
procedure TrdmDoc.DoInactiveState; begin UnregisterDoc(FDocID); FDocID := 0; cdsTitle.Active := False; cdsBody.Active := False; ibtDoc.Active := False; FState := osInactive; end; |
Процедура DoInactiveState удаляет документ из списка редактируемых, закрывает все клиентские наборы данных, а также производит откат транзакции (если она была активна).
procedure TrdmDoc.DoOpen(DocID: Integer); begin if DocID = 0 then Exit; try if not RegisterDoc(DocID) then raise Exception.Create('Документ редактируется'); FDocID := DocID; // и только здесь, иначе DoInactiveState удалит документ ibdDocs.Connected := True; ibtDoc.StartTransaction; with cdsTitle do begin params.paramByName('DocID').AsInteger := FDocID; Active := True; if BOF and EOF then raise Exception.Create('Документ не найден'); end; with cdsBody do begin params.paramByName('DocID').AsInteger := FDocID; Active := True; end; FState := osUpdate; ibtDoc.Commit; except DoInactiveState; raise; end; end; |
DoOpen предназначена для открытия существующего документа, идентификатор DOC_ID которого равен входному параметру DocID. Первым делом с помощью RegisterDoc производится проверка того, что документ в данный момент не редактируется. Затем идентификатор документа запоминается, и в клиентские наборы данных загружаются данные документа. В случае ошибки состояние документа переводится в osInactive.
procedure TrdmDoc.DoCreateNew; var NewDocID: Integer; begin try NewDocID := NewID; if not RegisterDoc(NewDocID) then raise Exception.Create('Документ редактируется'); FDocID := NewDocID; ibdDocs.Connected := True; ibtDoc.StartTransaction; with cdsTitle do begin params.paramByName('DocID').AsInteger := FDocID; Active := True; Append; Post; end; with cdsBody do begin params.paramByName('DocID').AsInteger := FDocID; Active := True; end; ibtDoc.Commit; FState := osInsert; except DoInactiveState; raise; end; end; |
Процедура DoCreateNew предназначена для создания нового документа. Она практически аналогична предыдущей, за исключением того, что идентификатор документа получается от сервера БД с помощью процедуры NewID, которая обращается к хранимой процедуре на сервере. Реализация процедуры DoCreateNew очень похожа на аналогичную реализацию в rdmCommon.
Для того, чтобы вставка новой записи в документ происходила верно, достаточно написать обработчик cdsTitle.OnNewRecord, задающий начальное значение полей записи, и практически такой же обработчик для cdsBody:
procedure TrdmDoc.cdsTitleNewRecord(DataSet: TDataSet); var Day, Month, Year: Word; begin DecodeDate(Date, Year, Month, Day); with cdsTitle do begin FieldByName('DOC_ID').AsInteger := FDocID; FieldByName('DOC_NUM').AsString := IntToStr(FDocID) + '/' + IntToStr(Year); FieldByName('DOC_DATE').asDateTime := Date; FieldByName('DOC_SUM').asCurrency := 0; FieldByName('FROM_ID').AsInteger := 0; FieldByName('TO_ID').AsInteger := 0; end; end; procedure TrdmDoc.cdsBodyNewRecord(DataSet: TDataSet); begin cdsBody.FieldByName('DOC_ID').AsInteger := FDocID; end; |
В дополнение ко всему нужна еще одна процедура в секции private, для подсчета суммы документа:
function TrdmDoc.CalcSum: Currency; begin Result := 0; if not cdsBody.Active then Exit; with cdsBody do begin First; while not EOF do begin Result := Result + FieldByName('COUNT_NUM').asCurrency * FieldByName('PRICE').asCurrency; Next; end; end; end; |
В функции CalcSum просматривается содержимое документа и рассчитывается общая сумма, которая возвращается в качестве результата.
Теперь надо позаботиться о клиентской части, то есть создать необходимые внешние методы сервера в библиотеке типов. Описание этих методов, созданное редактором библиотек типов, выглядит следующим образом:
protected function ApplyChanges: WideString; safecall; function Get_DocID: Integer; safecall; procedure CreateNewDoc; safecall; procedure Set_DocID(Value: Integer); safecall; function Get_DocSum: Currency; safecall; |
Функциональность этих методов такова: