Главная ]
Delphi5: новые возможности в MIDAS технологии
Программирование
Базы данных



 

Сергей Трепалин, независимый эксперт

Delphi5: новые возможности в MIDAS технологии

Аннотация

Работа с данными с использованием многозвенной архитектуры (технология MIDAS) поддерживается в языке программирования Delphi, начиная с третьей версии. В Delphi4 был добавлен ряд компонентов, которые поддерживали различные протоколы обмена данными с удаленными серверами приложений. В Delphi5 технология MIDAS получила свое дальнейшее развитие. В настоящей публикации анализируются изменения, которые произошли в MIDAS технологии в Delphi5 по сравнению с Delphi4, а также приводятся описания новых возможностей, которые реализованы в Delphi5 для многозвенных приложений. Приводятся примеры реализации многозвенных приложений с использованием Delphi5.

Изменения в палитре компонент

В Delphi5 произведены следующие изменения в компонентах, которые используются для создания многозвенных приложений:

            - отсутствует компонент TProvider. При анализе изменений в исходных кодах, поставляемых вместе с Delphi, можно отметить отсутствие реализации и обьявления интерфейса IProvider, который является базовым для создания многозвенных приложений в Delphi4. Причина отказа от интерфейса IProvider а также описание создания сервера приложений без экспортирования IProvider описаны ниже.

            - убраны компоненты TRemoteServer, TMidasConnection и TOLEnterpriseConnection, которые обеспечивают соединения с удаленным сервером приложений. В Delphi4 компонент TMidasConnection обеспечивал обмен данными через протоколы DCOM, OLEEnterprise и TCP/IP. Этот компонент просто дублировал возможности других компонент – TDCOMConnection, TOLEnterpriseConnection и TSocketConnection. Поэтому его отсутствие не уменьшило возможностей Delphi5 по сравнению с Delphi4. Отсутствие компонента TRemoteServer, который использовался в Delphi4 для поддержки приложений, созданных на Delphi3, также не влияет на возможности создания приложений. А вот отсутствие компонента TOLEnterpriseConnection а также отсутствие утилит OLEnterprise на дистрибутивном диске, свидетельствует о том, что, по-видимому, Borland прекратила поддержку технологии Entera.

            - появляется новый компонент TWEBConnection, который используется для экспонирования данных на WEB страницах. На мой взгляд, это революционное изменение, которое позволяет достаточно просто реализовать межплатформенного клиента. Именно наличие этих новых возможностей привели к отказу от интерфейса IProvider для обмена данных между клиентом и сервером приложений так как технология интерфейсов разработана компанией Microsoft и применима только к Windows платформам. К сожалению, обьем данной публикации не позволяют привести примеры использования компонента TWEBConnection.

            - появляются новые компоненты TXMLBroker и TMidasPageProducer, которые помещаются на отдельной закладке InternetExpress и работают вместе с компонентом TWEBConnection. Их обсуждение выходит за рамки данной публикации.

Сервер приложений

Создание сервера приложений. Сервер приложений делается таким же способом, как и в Delphi4: создается новое приложение и к нему добавляется удаленный модуль данных посредством выполнения команды File/New/Multitier/Remote Data Module. После этого необходимо заполнить диалог, который является стандартным для создания серверов OLE автомации (рис. 1):

 


 


Рисунок 1. Диалог эксперта создания удаленного модуля данных.

 Если сервер приложения является исполняемым (*.exe) файлом, то в этом диалоге имеет значение только параметр Instancing, который следует выбирать либо Single Instance (запускается отдельная копия приложения для каждого клиента) либо Multiply Instance (одна копия приложения обслуживает нескольких клиентов, при этом создается отдельный экземпляр TRemoteDataModule для каждого клиента), но ни в коем случае не Internal! На этот модуль данных помещаются невизуальные компоненты доступа к данным. Обязательно помещение компонента TSession, если значение параметра Instancing равно Multiply Instance. Как правило, сервером баз данных является SQL сервер, который требует наличия имени пользователя и пароль. Диалог для ввода имени пользователя и пароля не должен появляться на сервере приложений, поэтому на удаленном модуле данных устанавливается компонент TDatabase. Далее на этот модуль помещают компоненты доступа к данным – TQuery, TTable и TStoredProcedure. Описанная процедура создания модуля данных полностью идентична в Delphi4 и Delphi5.

            Далее имеются отличия. В Delphi4 возможны два варианта. Можно щелкнуть правой кнопкой мыши над компонентом доступа к данным и в всплывающем меню выбрать строку Export <имя компонента> from data module. Также, для каждого компонента доступа к данным, можно поставить компонент TProvider (или TDatasetProvider) и в его свойстве DataSet сослаться на компонент доступа к данным. После этого также необходимо экспортировать данный класс из модуля данных нажатием правой кнопки мыши на компоненте с последующим выбором соответствующей строки меню. При применении любого из этих способов в библиотеке типов, которая создается на сервере, определяются ряд интерфейсов типа IProvider. Клиент получает ссылки на эти интерфейсы и вызывает их методы.

            В Delphi5 возможен только один способ экспонирования данных – а именно: каждому компоненту доступа к данным ставится в соответствие компонент TDataSetProvider. В его свойстве DataSet следует сослаться на компонент доступа к данным – TTable, TQuery, TStoredProcedure. В отличии от Delphi4, никакой новой команды для экспорта IProvider в всплывающем меню не появляется. В Delphi5 не требуется экспортировать (то есть, прописывать в библиотеке типов) IProvider – он просто отсутствует! Простая установка на TRemoteDataModule компонента TDataSetProvider является достаточной для того, чтобы его “видел” клиент.

            Имеются отличия и в интерфейсах, которые экспонируются в библиотеках типов: в Delphi4 экспонируется интерфейс - потомок IDataBroker, который, в свою очередь, имеет один метод, добавленный к методам IDispatchGetProviderNames. В Delphi5 экспонируется IAppServer, к которому добавлен ряд методов - AS_ApplyUpdates, AS_GetRecords, AS_DataRequest, AS_GetProviderNames, AS_GetParams, AS_RowRequest, AS_Execute. Интерфейс IDataBroker сохранился в Delphi5 для компиляции приложений, код которых был создан на Delphi4. Ранее аналоги этих методов (кроме AS_GetProviderNames и AS_Execute) вызывались через IProvider. Исправлена ошибка Delphi4 – а именно, обращение к данным теперь защищены критическими секциями. В Delphi4, при наличии нескольких клиентов на сервере приложений и больших запросах периодически возникали сбои, связанные с доступом к данным из нескольких потоков.

            Изменилась и идеология динамического экспонирования данных – то есть, вызов конструктора TDataSetProvider во время выполнения приложений. Это необходимо в тех случаях, когда заранее неизвестно число компонент доступа к данным, которые нуждаются в экспонировании. В Delphi4 можно было динамически создать обьект TProvider и далее прописать ссылку на него в библиотеке типов – например, через коллекцию IProvider. После этого он становится доступным удаленному клиенту. В Delphi5 также можно динамически создать компонент TDataSetProvider, но прописывать ссылку на него в библиотеке типов не требуется. Вместо этого необходимо вызвать метод RegisterProvider класса TRemoteDataModule и после этого он становится доступным для работы с удаленным клиентом.

TDataSetProvider. Как уже упоминалось, каждому компоненту доступа к данным на сервере приложений, обязан быть добавлен компонент TDataSetProvider. Некоторые свойства и события этого компонента совпадают с соответствующими свойствами и событиями компонента TProvider, но имеется и ряд новых. Новое свойство Exported может принимать значение True (TDataSetProvider виден удаленному клиенту) или False. В Delphi4 аналог этого свойства заключается в выполнении (или отсутствия выполнения) команды “Export ProviderXXX from data module”, которая вызывается при нажатии правой клавиши мыши над компонентом доступа к данным (или TProvider). Существенно расширен набор опций для TDataSetProvider. Значения флагов poDisableInserts, poDisableEdits и poDisableDeletes очевидны из их названий. Появление этих флагов внесло существенную гибкость в многозвенную архитектуру. Ранее (в Delphi4) нескольким клиентам, которые обращались к серверу приложений, предоставлялись равные права на манипуляции с данными. Если необходимо разрешить редакцию данных хотя бы для одного клиента, то остальные автоматически получали на это право. Теперь же сервер приложений может отдельным клиентам разрешить редакцию данных, другим запретить в рамках единственного соединения с сервером базы данных.

poAllowMultiRecordUpdates – разрешает изменения в данных, которые затрагивают несколько записей. При отсутствии этого флага такие изменения не допускаются.

poNoReset – при обращению к методу IAppServer.AS_GetRecords из TClientDataSet игноирует grReset опцию.

            poAutoRefresh – автоматически выполняет команду Refresh класса TClientDataSet при каждом выполнении команды ApplyUpdates.

            poPropogateChanges – изменения, которые осуществляются с данными в обработчиках событий BeforeUpdateRecord или AfterUpdateRecord класса TDataSetProvider автоматически передаются клиенту, где они совмещаются с имеющимися данными.

poAllowCommandText позволяет во время выполнения изменять SQL запрос компонента TQuery, расположенном на сервере приложений или название хранимой процедуры компонента TStoredProc. Только при включенной этой опции возможнj использование метода Execute и свойства CommandText компонента TClientDataSet (см. ниже).

 

TDataSetProvider имеет также ряд новых событий. Новые пары – AfterApplyUpdates и BeforeApplyUpdates, AfterExecute и BeforeExecute, AfterGetParams и BeforeGetParams, AfterGetRecords и BeforeGetRecords, AfterRowRequest и BeforeRowRequest имеют тип TRemoteEvent и взаимодействуют с методами с тем же названием, определенными в классе TClientDataSet. Они вызываются при вызове методов ApplyUpdates, Execute, FetchParams, считывания данных с сервера приложений (изменение свойства Active в True) класса TClientDataSet. Временная диаграмма вызова этих методов следующая (на примере вызова метода ApplyUpdates):

1.       В клиентном приложении при выполнении кода встречается команда ApplyUpdates

2.       Клиентное приложение вызывает обработчик события TClientDataSet.BeforeApplyUpdates

3.       Клиентное приложение обращается к серверу приложения посредством вызова метода IAppServer.AS_ApplyUpdates

4.       Серверное приложение вызывает событие TDataSetProvider.BeforeApplyUpdates

5.       На сервере выполняются необходимые изменения в данных, при этом создается новый пакет данных, в котором содержатся все изменения.

6.       Серверное приложение вызывает обработчик события TDataSetProvider.AfterApplyUpdates

7.       Клиентное приложение вызывает обработчик события TClientDataSet.AfterApplyUpdates

8.       Продолжается выполнение кода клиентного приложения.

Данная диаграмма может быть нарушена при возникновении исключительной ситуации или, например, при наличии ReconcileError. Для других команд клиентного приложения временная диаграмма аналогичная. Помимо нотификации о возникновении той или иной ситуации, данные обработчики событий могут быть использованы для передачи приватных данных. Для понимания механизма передачи приватных данных следует обратить внимание на то, что все эти события имеют тип TRemoteEvent, который определен следующим образом:

TRemoteEvent = procedure(Sender: TObject; var OwnerData: OleVariant) of object;

В переменной OwnerData можно помещать любые данные, при этом:

1.       В обработчике события BeforeXXXX TClientDataSet данные помещаются в переменную OwnerData.

2.       В обработчике события BeforeXXXX TDataSetProvider можно считать значение переменной OwnerData. Заполнять эту переменную здесь не имеет смысла – ее эначение не будет никуда передаваться.

3.       В обработчике события AfterXXXX TDataSetProvider следует поместить данные в переменную OwnerData. Считывать ее не имеет смысла – ее значение не определено.

4.       И, наконец, в обработчике события AfterXXXX TClientDataSet можно получить данные, определенные в п.3

Например, если администратору сервера приложений необходима информация о клиентах, которые вызывали команду ApplyUpdates, то на форму сервера приложений можно поместить компонент TListBox и сделать один общий обработчик события BeforeApplyUpdates для всех компонент TDataSetProvider:

procedure TTest.DataSetProvider1BeforeApplyUpdates(Sender: TObject;

  var OwnerData: OleVariant);

var

  S:string;

begin

  S:=(Sender as TDataSetProvider).Name+' '+OwnerData+' '+DateTimeToStr(Now);

  Form1.ListBox1.Items.Add(S);

end;

Соответственно, в клиентном приложении делаются обработчики события BeforeApplyUpdates для компонент TClientDataSet:

procedure TForm1.ClientDataSet1BeforeApplyUpdates(Sender: TObject;

  var OwnerData: OleVariant);

begin

  OwnerData:='MyName';

end;

При выполнении команды TClientDataSet.ApplyUpdates, помимо изменений данных на сервере базы данных, на сервере приложений появляется визуальная информация (рис. 2):


Рисунок 2. Передача приватных данных серверу приложений

 


куда заносится строка “MyName”, определенная на клиенте. В Delphi4 для аналогичной передачи приватных данных потребуется создать новый метод в интерфейсе-потомке IDataBroker стандартным способом для сервера OLE автомации – через редактор библиотеки типов. Соответственно, на клиенте везде где вызывается команда ApplyUpdates необходимо вызвать этот метод. В Delphi5 можно сделать то же самое, поэтому приведенный выше пример можно рассматривать как добавление к возможностям обмена приватными данными.

            Обратный процесс передачи приватных данных – от сервера приложений к клиенту – требует реализации обработчиков событий TDataSetProvider.AfterApplyUpdates на сервере приложений и TClientDataSet.AfterApplyUpdates на клиенте. Способ реализации остальных пар событий (Execute, и.т.д) аналогичен.

            TDataSetProvider имеет также новый обработчик события OnGetTableName. Этот обработчик вызывается в тех случаях, когда команда ApplyUpdates применяется к данным, полученным с использованием компонент TQuery со значением False параметра RequestLive (или когда этот параметр True, но выборка такова, что не может быть автоматически изменена – “мертвая” выборка) или TStoredProc. Программист, создающий сервер приложений должен использовать этот обработчик события для задания имени таблицы, в которой будут происходить изменения.

            Во многих методах TDataSetProvider появился новый параметр Delta:TDataSet. Этот набор данных является не пустым, если в данные вносятся изменения. При этом все записи, которые изменялись, копируются в Delta. В нем сохраняются как оригинальные значения полей для записи, так и измененные.

            И еще несколько замечаний по поводу сервера приложений. В примерах выше использовался стандартный способ разработки сервера, который заключается в использовании компонент TTable, TQuery и TStoredProc с палитры DataAccess в качестве источника данных для компонента TDataSetProvider. Хотелось бы обратить внимание, что TDataSetProvider (и клиентный компонент – TClientDataSet) не требует, чтобы источником данных являлся компонент – потомок TDBEDataSet. Этот факт позволяет создать сервер приложений, который не использует BDE для доступа к данным. Например, можно использовать ADO технологию, на сервере приложений размещаются компоненты TADOTable, TADOQuery, TADOStoredProc или прямой доступ к Interbase – на сервере приложений размещаются компоненты TIBTable, TIBQuery, TIBStoredProc. Этот факт дает программистам значительную мобильность при создании сервера приложений.

MIDAS клиент

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

Создание клиентного приложения.

Начинается с создания нового приложения. На форму помещается один из компонентов для доступа к серверу приложений – TDCOMConnection или TSocketConnection. Далее, на клиентное приложение помещаются компоненты TClientDataSet (по одному для каждого экспонируемого сервером приложений TDataSetProvider) и посредством изменения свойства ProviderName они связываются с компонентами TDataSetProvider, расположенными на сервере приложений. Стандартный компонент TDataSource использует TClientDataSet в качестве источника данных. Далее все как в стандартном клиенте – размещаются контроли для показа данных и связыватся с TDataSource.

Компоненты для связи с удаленным сервером.

TDCOMConnection использует разработанный фирмой Microsoft DCOM протокол обмена данными. Для использования DCOM протокола необходим первичный контроллер домена – подходящим образом сконфигурированный Windows NT сервер. Компьютер, на котором располагается сервер приложений, должен иметь инсталлированный DCOM – для Windows NT это делается автоматически, при инсталлировании системы, а для Windows 95/98 требуется инсталлировать DCOM отдельно. Используя утилиту DCOMCNFG.EXE необходимо прописать права пользователей на запуск сервера приложений. При таком способе доступа к серверу приложений необходимо, чтобы все клиенты и сам сервер приложений располагались внутри одного домена.

            Для TSocketConnection используется TCP/IP протокол обмена данными, который снимает ограничения о расположени сервера приложений и клиентов внутри одного домена. Фактически из любого места, используя модемное соединение, можно обратиться к серверу приложений. Компьютер, где находится сервер приложений, должен иметь запущенную утилиту scktsrvr.exe, которая поставляется вместе с Delphi5 (или scktsrvс.exe если сервер приложений находится в операционной системе Windows NT). Эта утилита не использует имена пользователей и их права для запуска сервера приложений – ответственность за аутентификацию возлагается на сервер приложений. Это с одной стороны дает опытным программистам значительную гибкость в разработке механизма аутентификации, а с другой стороны, при дилентатском походе, имеется существенная вероятность несанкционированного доступа к данным.

            Свойства и события для TDCOMConnection не изменились при переходе от Delphi4 к Delphi5, а для TSocketConnection было добавлено свойство SupportCallback. Возможные значения: True – клиентное приложение принимает нотификации от сервера приложений и False. Нотификации являются чрезвычайно важными в MIDAS технологии, например, если с сервером приложений работают несколько клиентов и один из них изменил данные, то разумно проинформировать об этом других клиентов для того, чтобы они могли считать новый набор данных. К сожалению, примеров нотификаций с использованием данного свойства не приводятся, а описания данного свойства в документации Delphi5 скудно и не позволяет сделать работоспособный пример. Будем надеяться, что в будущем появится более подробная документация и\или примеры.

            Методы: в Delphi5 отсутствует метод GetProvider по понятным причинам – IProvider не существует в Delphi5. Добавился внутренний метод GetServer, который возвращает ссылку на IAppServer. Вместо прямого вызова этого метода следует обращаться к свойству AppServer, так же, как и в Delphi4.

TClientDataSet

Данный компонент инкапсулирует все компоненты доступа к данным в клиентном приложении MIDAS. По сравнению с Delphi4, в Delphi5 добавлено ряд новых свойств и событий. Главные из них:

Constraints – ограничения, применимые к записи. Это свойство хорошо известно разработчикам клиент-серверных приложений в Delphi4 по компонентам TTable или TQuery. Но на компоненте TClientDataSet нельзя было определить свои ограничения на запись или импортировать их из словаря. Теперь этот недостаток устранен.

CommandText – великолепное свойство, которое позволяет естественным для программиста образом изменять содержимое SQL запроса на сервере приложений или имени хранимой процедуры и, как следствие, получать новый набор данных. В Delphi4 также можно было во время выполнения приложения изменить SQL запрос на новый, определенный на клиенте. Для этого необходимо было использовать редактор библиотеки типов сервера приложений и добавить новый метод (назовем его NewQuery) к интерфейсу – потомку IDataBroker. Этот метод в качестве параметра содержит константу типа WideString. В его реализации эта константа присваивается свойству SQL.Text компонента TQuery, при необходимости изменяя свойство Active. Клиент мог вызвать этот метод используя свойство AppServer компонента, ответственного за связь с сервером приложений (TSocketConnection или TDCOMConnection):

procedure TForm1.Button1Click(Sender: TObject);

begin

  ClientDataSet1.Close;

  SocketConnection1.AppServer.NewQuery(Memo1.Text);

  ClientDataSet1.Open;

end;

В Delphi5 также можно воспользоваться данной технологией, но изменение запроса при помощи свойства CommandText более естественно. Для того, чтобы изменение этого свойства приводило к изменению запроса, необходимо выполнение двух условий:

А) Компонент TClientDataSet (размещаемый на клиентном приложении) через свое свойство ProviderName должен ссылаться на TDataSetProvider (размещаемый на сервере приложений), который в свою очередь через свое свойство DataSet ссылается на компонент TQuery или TStoredProc, но не на TTable! Причина этого ясна – данные TTable не могут быть изменены (если, конечно, не менять свойство TableName).

Б) В свойство Options TDataSetProvider (сервер приложений) обязан быть включен флаг poAllowCommandText.

Новый запрос для примера выше осуществляется следующим образом:

procedure TForm1.Button1Click(Sender: TObject);

begin

  ClientDataSet1.Close;

  ClientDataSet1.CommandText := Memo1.Text;

  ClientDataSet1.Open;

end;

При этом, в отличии от Delphi4 не требуется создания нового метода на сервере приложений. Следует обратить внимание, что SQL запрос обязан возвращать набор данных. Если он не возвращает набор данных, то необходимо использовать метод Execute TClientDataSet (см. ниже).

            Свойство только для чтения DeltaSet, доступное во время выполнения, содержит в себе всю информацию об изменении в данных, которые были внесены с момента последнего вызова команды ApplyUpdates. В этом наборе данных сохраняются как оригинальное содержимое полей, так и модифицированные значения.

В Delphi5 для TClientDataSet появился ряд событий – AfterApplyUpdates, AfterExecute, AfterGetParams, AfterGetRecords, AfterRefresh, AfterRowRequest и такое же количество событий с приставкой Before… Эти события вызываются перед и после исполнением методов TClientDataSet: ApplyUpdates, Execute, FetchParams, DoGetRecords, Refresh, DoRowRequest. При этом методы DoGetRecords и DoRowRequest недоступны для вызова из экземпляра класса (определены в секции Protected TClientDataSet) – они являются частью системы обмена данными между сервером приложений и клиентом и вызываются при необходимости. Все эти методы, кроме Execute, имеют аналоги в Delphi4, но в Delphi4 для них отсутствовали приведенные выше нотификационные сообщения. Помимо чисто нотификаций, данные события можно использовать для передачи приватных данных от сервера приложений к клиенту и наоборот, как это было описано выше.

            TClientDataSet имеет три новых метода, которые экспонируются в секции public и доступны для вызова из экземпляра класса – SetProvider, DataRequest и Execute. SetProvider используется для назначения TDataSetProvider экземпляру TClientDataSet в тех случаях, когда клиент и сервер приложений совпадают, то есть, один исполняемый файл явлеется сервером приложений и клиентом. Такая ситуация возникает, например, когда разработчик сервера приложения хочет визуально показывать админинстратору сервера приложения последний набор данных, к которым был применен метод ApplyUpdates. Компонент TClientDataSet устанавливается в этом случае на сервере приложения и стандартным способом связывается с контролями для показа данных. В общем для всех экземпряров TDataSetProvider обработчике событий AfterApplyUpdates используется код:

procedure TForm1.DataSetProvider1AfterApplyUpdates(Sender: TObject;

  var OwnerData: OleVariant);

begin

  ClientDataSet1.Close;

  DBGrid1.Columns.Clear;

  ClientDataSet1.SetProvider(Sender as TDataSetProvider);

  ClientDataSet1.Open;

end;

Следует помнить, что метод SetProvider нельзя вызывать для удаленного TDataSetProvider, который экспонируется через IAppServer.

Метод DataRequest вызывает событие OnDataRequest удаленного TDataSetProvider и возвращает новый набор данных. Он может быть успешно использован для изменения набора данных, который возвращается по умолчанию - например, для фильтрации данных. В примере ниже возвращаются только имена, начинающиеся с буквы ‘B’:

Сервер приложений

function TForm1.Provider1DataRequest(Sender: TObject; Input: OleVariant): OleVariant;

begin

  with (Sender as TDataSetProvider) do begin

    DataSet.Filter := Input;

    DataSet.Filtered := True;

    DataSet.First;

    Result := Data;

  end;

end;

Клиент

procedure TForm1.Button1Click(Sender: TObject);

begin

  ClientDataSet1.Data := ClientDataSet1.DataRequest(‘Name’ = ‘B*’);

end;

Фильтрацию можно осуществлять во время выполнения без изменения свойства Active компонента-источника данных.

И, наконец, метод Execute используется для выполнении запроса для компонентов TQuery или TStoredProc, расположенных на сервере приложений. Как и для описанного выше свойства CommandText, TClientDataSet должен ссылаться на удаленный TDataSetProvider, который, в свою очередь, ссылается на TQuery или TStoredProc, но не TTable. При вызове метода Execute возможны два варианта. В первом из них имеется непустое значение свойства TClientDataSet.CommandText и в опции TDataSetSetProvider включен флаг poAllowCommandText. В этом случае метод Execute в качестве SQL запроса использует содержимое CommandText и параметры TClientDataSet.Params посылаются в качестве параметров запроса. После его выполнения новые значения Params доступны на TClientDataSet. При пустом поле CommandText или при отсутствии флага poAllowCommandText в качестве запроса используется текущее содержимое TQuery или TStoredProc.

            Таким образом, MIDAS3 следует рассматривать как дальнейшее развитие MIDAS технологии. Все изменения, которые были сделаны в классах для работы с MIDAS естественны и, я думаю, программисты их будут приветствовать.

 

 
Дизайн: Piton Alien
Rambler's Top100 Рейтинг@Mail.ru
Сайт создан в системе uCoz