(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Учебный пример: DataSnap XE - обычные объекты в качестве параметров.

Источник: embarcadero
Vsevolod Leonov

В этой статье мы рассмотрим различные типы параметров, которые можно передавать между клиентами и серверами DataSnap.В ходе выполнения примера мы будем передавать параметры, которые представляют собой обычные классы Delphi. В Java Enterprise Edition существует такая концепция -

"Plain Old Java Objects" -

 для передачи данных между приложениями. Здесь мы собираемся использовать этот же подход для обмена данными между клиентами и серверами, поэтому мы будем назвать их "Plain Old Delphi Objects" или "PODO"!

Введение

Архитектура DataSnap позволяет обмениваться данными между клиентом и сервером в широком диапазоне типов. Сервера DataSnap могут быть реализованы в Delphi или C++Builder, а для клиентских приложений есть и другие варианты. Можно создавать клиентов DataSnap в Delphi, C++Builder, RadPHP, Delphi Prism и JavaScript. В зависимости от языка реализации клиентского приложения существуют некоторые явные ограничения в передачи параметров, которые скажутся на реализации серверных методов DataSnap. Самый большой диапазон возможных параметров доступен, когда и сервер, и клиент реализованы в Delphi.

Если мы используем Delphi для создания клиентского и серверного приложения, мы можем обмениваться данными различных типов, включая базовые, на основе DBX, а также специальные, такие как коллекции, JSON или даже произвольные потомки TObject.

Если объекты содержат ссылки на другие объекты, то передача объектов, по понятным причинам, затруднена. Но если речь идет о независимых структурах данных, фреймворк DataSnap снимает значительную часть сложных вопросов, связанных с процессом сериализации и десериализации.

В целом, если определенный тип не поддерживается, генератор прокси DataSnap (DataSnap proxy generator) просто проигнорирует его при создании соответствующего кода.

В этом учебном примере я собираюсь поговорить о том, как передавать обычные объекты Delphi (классов наследников TObject). В Java Enterprise Edition (JEE) существует концепция "Plain Old Java Object" для передачи данных между приложениями. Здесь мы будем использовать тот же самый подход для передачи данных между клиентами и серверами, созданными в Delphi. Будем называть такие объекты "Plain Old Delphi Objects" или сокращенно "PODO".

Давайте попробуем.

Создание сервера

В меню кликните на "File - New - Other" и затем два раза на "DataSnap Server" в диалоге "New Items" в категории "Delphi Projects -

DataSnap Server" для создания независимого серверного приложения DataSnap.

На первой странице мастера оставьте по-умолчанию "VCL Forms Application" в качестве типа приложения.

Снимите "галочку" на опции генерации демонстрационных серверных методов ("sample methods").

Сохраните все значения по-умолчанию для сервера, так что наш сервис будет использовать протокол TCP/IP для взаимодействия без авторизации/аутентификации.

Мы будем реализовывать серверные методы сами.

На третьей странице оставим порт TCP/IP по-умолчанию как 211 и проверим его доступность.

На последней странице оставьте базовый класс для реализации серверных методов и по-умолчанию и кликните на "Finish".

Выберите "File - Save All" для сохранения файлов в новом проекте, созданном мастером. В моем случае я собираюсь создать новую папку "C:\DataSnapLabs\PlainOldDelphiObjectParams\" для своих файлов.

Назовите серверный модуль "FormServerUnit" и оставьте имена для модуля серверных методов и контейнера по-умолчанию (например, "ServerMethodsUnit2" и "ServerContainerUnit2", а имя всего проекта введите как "PODOServer".

Давайте создадим класс Delphi, который мы будем передавать как параметр.

Код проектов как сервера, так и клиента должен знать классы, которые мы будем передавать, поэтому целесообразно выделить для этого отдельный модуль, который будет использоваться в разделе "uses" в обоих проектах.

Добавьте новый модуль в серверный проект и сохраните его как "SharedStuffUnit".

Замените его содержимое на реализацию некого класса "TPerson".

unit SharedStuffUnit;

interface

type
  TPerson = class(TObject)
  private
    FLastName: string;
    FFirstName: string;
    procedure SetFirstName(const Value: string);
    procedure SetLastName(const Value: string);
  public
    constructor Create(const aFirst, aLast: string);
    function ToString: string; override;
    property FirstName: string read FFirstName write SetFirstName;
    property LastName: string read FLastName write SetLastName;
  end;

implementation

{ TPerson }

constructor TPerson.Create(const aFirst, aLast: string);
begin
  FFirstName := aFirst;
  FLastName := aLast;
end;

procedure TPerson.SetFirstName(const Value: string);
begin
  FFirstName := Value;
end;

procedure TPerson.SetLastName(const Value: string);
begin
  FLastName := Value;
end;

function TPerson.ToString: string;
begin
  Result := LastName + ', ' + FirstName;
end;

end.

Теперь нам нужно внести данный модуль в раздел "uses" модуля класса серверных методов.

Войдите в модуль серверных методов и выберите "File - Use Unit" из меню, а новый модуль попадет в раздел "uses" автоматически.

Давайте реализуем серверный метод, названный "GetPerson", который принимает два строковых параметра для имени и фамилии и возвращает экземпляр "TPerson", инициализированный данными значениями.

unit ServerMethodsUnit2;

interface

uses
  SysUtils, Classes, DSServer, 
  SharedStuffUnit; // for "TPerson"

type
{$METHODINFO ON}
  TServerMethods2 = class(TComponent)
  private
    { Private declarations }
  public
    function GetPerson(const aFirst, aLast: string): TPerson;
  end;
{$METHODINFO OFF}

implementation

{ TServerMethods2 }

function TServerMethods2.GetPerson(const aFirst, aLast: string): TPerson;
begin
  Result := TPerson.Create(aFirst, aLast);
end;

end.

Теперь наш сервер готов. Теперь нужно лишь запустить его ("Run - Run Without Debugging" и минимизировать его окно.

Сервер должен быть запущен в течение создания клиентского приложения.

Реализация клиентского приложения

Кликните правой кнопкой на иконке проектной группы в менеджере проектов и выберите "Add New Project".

Выберите для Delphi "VCL Forms Application".

Сохраните всё.

Сохраните новый модуль в ту же папку, что и файлы серверного проекта.

Я собираюсь сохранить модуль главной формы как "FormClientUnit", проект как "PODOClient", а всю проектную группу как "PODOGroup".

Важный шаг, который можно упустить из внимания! Так как и сервер, и клиент находятся в одной и той же папке, компилятор Delphi найдет наш модуль "SharedStuffUnit" и класс "TPerson", но в целом нам нужно добавлять модуль с общими данными в клиентский проект в явном виде.

Простой способ сделать это - выбрать "SharedStuffUnit" в менеджере проектов и "перетащить" его на узел клиентского проекта.

Теперь нам нужно сгенерировать код клиентского прокси DataSnap, чтобы мы могли вызывать серверные методы из клиента.

Самый простой путь сделать это - воспользоваться мастером "DataSnap Client Module".

Вы можете оставить значение по-умолчанию для всех четырех страниц мастера.

На первой странице оставьте размещение сервера как "Local Server" ("локальный сервер").

На второй странице оставим типа серверного приложения как standalone ("независимое, изолированное").

На следующей странице оставьте значение по-умолчанию для протокола связи, которое "TCP/IP".

На последней странице оставьте значения порта 211 и проверьте его доступность кнопкой "Test Connection".

Нажмите на кнопку "Finish".

Мастер добавит два новых модуля к нашему клиентскому проекту: "ClientModuleUnit1" и "ClientClassesUnit1".

Сохраните все новые файлы, оставив имена по-умолчанию.

Давайте изучим содержимое модулей клиентских классов, которые были созданы генератором клиентского прокси DataSnap.

// 
// Created by the DataSnap proxy generator.
// 3/21/2011 9:46:56 PM
// 

unit ClientClassesUnit1;

interface

uses DBXCommon, DBXClient, DBXJSON, DSProxy, Classes, SysUtils, DB, SqlExpr, DBXDBReaders, SharedStuffUnit, DBXJSONReflect;

type
  TServerMethods2Client = class(TDSAdminClient)
  private
    FGetPersonCommand: TDBXCommand;
  public
    constructor Create(ADBXConnection: TDBXConnection); overload;
    constructor Create(ADBXConnection: TDBXConnection; AInstanceOwner: Boolean); overload;
    destructor Destroy; override;
    function GetPerson(aFirst: string; aLast: string): TPerson;
  end;

implementation

function TServerMethods2Client.GetPerson(aFirst: string; aLast: string): TPerson;
begin
  if FGetPersonCommand = nil then
  begin
    FGetPersonCommand := FDBXConnection.CreateCommand;
    FGetPersonCommand.CommandType := TDBXCommandTypes.DSServerMethod;
    FGetPersonCommand.Text := 'TServerMethods2.GetPerson';
    FGetPersonCommand.Prepare;
  end;
  FGetPersonCommand.Parameters[0].Value.SetWideString(aFirst);
  FGetPersonCommand.Parameters[1].Value.SetWideString(aLast);
  FGetPersonCommand.ExecuteUpdate;
  if not FGetPersonCommand.Parameters[2].Value.IsNull then
  begin
    FUnMarshal := TDBXClientCommand(FGetPersonCommand.Parameters[2].ConnectionHandler).GetJSONUnMarshaler;
    try
      Result := TPerson(FUnMarshal.UnMarshal(FGetPersonCommand.Parameters[2].Value.GetJSONValue(True)));
      if FInstanceOwner then
        FGetPersonCommand.FreeOnExecute(Result);
    finally
      FreeAndNil(FUnMarshal)
    end
  end
  else
    Result := nil;
end;

constructor TServerMethods2Client.Create(ADBXConnection: TDBXConnection);
begin
  inherited Create(ADBXConnection);
end;

constructor TServerMethods2Client.Create(ADBXConnection: TDBXConnection; AInstanceOwner: Boolean);
begin
  inherited Create(ADBXConnection, AInstanceOwner);
end;

destructor TServerMethods2Client.Destroy;
begin
  FreeAndNil(FGetPersonCommand);
  inherited;
end;

end.

Прокси-генератор DataSnap автоматически "зацепит" общий модуль и также сгенерирует кода для де-маршализации экземпляра "TPerson" из JSON-представления, использованного для передачи по сети! Великолепно!

Последний шаг - создание простейшего GUI для вызова серверного метода, который возвращает экземпляр "TPerson".

Откроем модуль клиентской формы.

Выберем "File - Use Unit" и добавим неиспользованный доселе модуль в раздел uses секции implementation клиентского модуля.

Добавим на форму два компонента "TEdit" и один "TButton".

Назовите первое поле ввода "edFirstname", а второе - "edLastname".

Поменяйте свойство "Caption" кнопки на "Show Person" в инспекторе объектов.

Кликните два раза на кнопке и реализуйте событие "OnClick", в котором будет вызов серверного метода "GetContact", передавая содержимое полей ввода в качестве параметров, и в котором отобразится результат метода "ToString" экземпляра "TPerson", полученного из сервера.

Ниже представлен листинг клиентского модуля:

unit FormClientUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TFormClient = class(TForm)
    edtFirstname: TEdit;
    Button1: TButton;
    edtLastname: TEdit;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormClient: TFormClient;

implementation

uses ClientClassesUnit1, ClientModuleUnit1, SharedStuffUnit;

{$R *.dfm}

procedure TFormClient.Button1Click(Sender: TObject);
var p: TPerson;
begin
  p := ClientModule1.ServerMethods2Client.GetPerson(
    edtFirstname.Text, edtLastname.Text);
  ShowMessage(p.ToString);
end;

end.

Если вводятся некие строки типа "имя" и "фамилия", то должны увидеть на экране следующий диалог.

Вот так оно работает! Мы только что создали простой пример, состоящий из сервера и клиента DataSnap, в котором используется метод передачи объекта пользовательского типа.

Это и называется "Plain Old Delphi Object" (обычный объект Delphi).

Управление памятью

Глубокий анализ кода обработчика события на получение экземпляра объекта посредством вызова метода прокси-класса показал несколько интересных фактов.

Должны ли мы проверить ссылку "p" (p : TPerson) на "nil" до обращения по ней к памяти?

А не происходит ли утечка памяти? Должны ли мы освобождать ссылку "p" в конце метода?

Прежде всего, всегда является хорошей идеей проверка на "nil" или "not nil" ссылки, которая возвращается серверным методом.

Это может быть заложено в логику работы приложения, когда возвращается "nil" для некоторых значений входных параметров, поэтому мы всегда должны быть готовы к чтению ссылки "nil".

Ответ на вопрос, должны ли мы вызывать "p.Free" в конце метода "Button1Click", более сложный.

В данном конкретном сценарии нам не нужно освобождать ссылку "p", т.к. ей владеет прокси-класс серверного метода.

Если мы заглянем в модуль "ClientModuleUnit1", мы увидим, что класс "TClientModule1" имеет свойство "InstanceOwner", которое инициализируется как "true" в конструкторе.

Однако внутренний экземпляра класса серверного метода не создается в конструкторе. Он "лениво" создается в геттере свойства "ServerMethods2Client".

Это тот самый момент, когда текущее значение "InstanceOwner"-а передается в конструктор.

Если нам нужно непосредственно управлять жизненным циклом полученных экземпляров от классов серверных методов, мы можем задать свойство "ClientModule1.InstanceOwner"-а как "false", но это должно случиться до чтения свойства "ClientModule1.ServerMethods2Client".

procedure TFormClient.Button1Click(Sender: TObject);
var p: TPerson;
begin

  // внутренний экземпляр класса серверных методов "лениво" создается 
  // при первом обращении
  // так что всё нужно сделать перед чтением
  // свойства "ClientModule1.ServerMethods2Client" 
  // а если по-другому, то это не будет иметь эффекта
  ClientModule1.InstanceOwner := False;

  p := ClientModule1.ServerMethods2Client.GetPerson(
    edtFirstname.Text, edtLastname.Text);
  if p <> nil then
  begin
    ShowMessage(p.ToString);
    if not ClientModule1.InstanceOwner then
      p.Free;
  end;
end;

Другой хороший вопрос - как создается ссылка на "TPerson" на клиенте?

Метод "GetPerson", созданный генератором прокси DataSnap, имеет кое-что интересное в своём коде.

//  …

  if not FGetPersonCommand.Parameters[2].Value.IsNull then
  begin
    FUnMarshal := TDBXClientCommand(FGetPersonCommand.Parameters[2].ConnectionHandler).GetJSONUnMarshaler;
    try
      Result := TPerson(FUnMarshal.UnMarshal(FGetPersonCommand.Parameters[2].Value.GetJSONValue(True)));
      if FInstanceOwner then
        FGetPersonCommand.FreeOnExecute(Result);
    finally
      FreeAndNil(FUnMarshal)
    end
  end
  else
    Result := nil;

// …

Этот код генерируется автоматически при взаимодействии с запущенным сервером DataSnap. Впечатляет! Генератор знает, что он должен использовать наш модуль "SharedStuffUnit". Значение, возвращаемое методом "GetPerson" является результатом приведения типа ссылки к "TPerson", возвращаемой методом "TJSONUnMarshal.Unmarshal".

Итог

Это выглядит как погружение всё глубже и глубже. Архитектура DataSnap очень элегантна, также как и фреймворк JSON конвертации "туда-обратно" для сереализации и десереализации объектов.

Если вы хотите глубже изучить материал, то посмотрите модули "DBXJSON" и "DBXJSONReflect". В них много полезных комментариев!

Фактически поддержка сериализации и десериализации объектов Delphi полезна сама по себе! Это эффективно не только в связке с архитектурой DataSnap и передачей объектов по сети.

Архитектура DataSnap дает возможность обмениваться параметрами в широком диапазоне типов между клиентским и серверным приложением.

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

В данном учебном примере из серии "Delphi Labs" мы рассмотрели шаги, необходимые для создания клиента и сервера DataSnap, в процессе взаимодействия которых передается простой экземпляр класса "TPerson" с двумя строковыми свойствами.

Также мы обсудили различные варианты управления памятью с использованием ссылки на экземпляр, возвращаемый серверными методами.

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

Первым шагом является создание нового серверного приложения DataSnap с использованием мастера "DataSnap Server".

Запустите Delphi XE.

Выберите в главном меню "File -> New -> Other", а затем в диалоге "New Items" щелкните два раза на иконке "DataSnap Server" в категории "Delphi Projects ->

DataSnap Server".

На первой странице оставьте по-умолчанию опцию "Project type" как "VCL Forms Application".

На второй странице мастера также оставьте уже выбранные опции.

Далее мы заменим демонстрационные методы (Sample Methods) сервера нашими вариантами.

На третьем изображении мы оставим значение по-умолчанию "211" для номера порта TCP/IP.

Старайтесь всегда проводить проверку доступности порта путем нажатия на кнопку "Test Port".

На последней странице мы собираемся также воспользоваться опцией по-умолчанию для выбора базового класса как "TComponent",

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

Нажмите на кнопку "Finish", а мастер создаст новый проект с тремя модулями.

Сохраните весь проект выбором "File -> Save All".

Создайте новую папку для всех файлов для данного учебного примера "C:\DataSnapLabs\SimpleCalc\".

Сохраните главную форму приложения как "FormServerUnit", а для остальных имен воспользуйтесь стандартными названиями - "ServerContainerUnit1" и "ServerMethodsUnit1" - и сохраните проект как "SimpleCalcServer".

На этой стадии в окне Delphi Project Manager вы видите следующую картину:

Откройте модуль "ServerMethodsUnit1.pas" и реализуйте функциональность простого сервиса, выполняющего арифметические действия.

Замените демонстрационные методы, добавленные мастером ("EchoString" и "ReverseString"), на нужные нам варианты "Add" ("сложить"),

"Subtract" ("вычесть"), "Multiply" ("умножить") и "Divide" ("разделить").

Исходный код для модуля "ServerMethodsUnit1" должен выглядеть следующим образом:

unit ServerMethodsUnit1;

interface

uses
  Classes;

type
{$METHODINFO ON}
  TServerMethods1 = class(TComponent)
  private
    { Private declarations }
  public
    function Add(a, b: double): double;
    function Subtract(a, b: double): double;
    function Multiply(a, b: double): double;
    function Divide(a, b: double): double;
  end;
{$METHODINFO OFF}

implementation

{ TServerMethods1 }

function TServerMethods1.Add(a, b: double): double;
begin
  Result := a + b;
end;

function TServerMethods1.Subtract(a, b: double): double;
begin
  Result := a - b;
end;

function TServerMethods1.Multiply(a, b: double): double;
begin
  Result := a * b;
end;

function TServerMethods1.Divide(a, b: double): double;
begin
  Result := a / b;
end;

end.

Листинг 1: ServerMethodsUnit1.pas.

Вы полностью реализовали наш сервер. Для того чтобы реализовать клиентское приложение, сервер должен быть запущен.

Выберите "Run -> Run Without Debugging" для запуска сервера (вне отладчика) и минимизируйте его окно.

Не завершайте работу серверного приложения до окончания учебного примера.

Создаем простое клиентское приложение

Кликните правой кнопкой мыши на project group внутри Project Manager и выберите "Add New Project".

В диалоге "New Items" выберите "VCL Forms Application" из категории "Delphi Projects".

Нажмите "OK". К существующей project group должен добавиться новый проект.

Выполните сохранение нового проекта "File -> Save All".

Выберите папку, которую вы создали для сохранения файлов проекта сервера,

и сохраните туда главную форму клиентского приложения как "FormClientUnit",

сам проект как "SimpleCalcClient", а всю project group как "SimpleCalcGrp".

Теперь окно Project Manager должно выглядеть как:

Проверьте, что проект клиентского приложения активен, затем выберите "File -> New -> Other",

а в появившемся диалоге "New Items" выберите "DataSnap Client Module".

Как и в прошлый раз, пожалуйста, сохраните все опции по умолчанию во время работы с мастером.

На первой странице оставьте "DataSnap server location" как "Local server".

Наш сервер DataSnap является "stand alone" (независимое отдельное приложение), поэтому просто нажмите "Next".

Мы согласились использовать "TCP/IP" в качестве протокола, поэтому сохраним выбранную опцию.

Нажмите на "Test Connection", чтобы проверить, действительно ли сервер "слушает" порт 211, а затем нажмем "Finish".

Мастер добавить два модуля к нашему клиентскому приложению "ClientClassesUnit1" и "ClientModuleUnit1".

Последней задачей в этом учебном примере является реализация интерфейса клиентского приложения.

Начнем с того, что добавим ссылку на модуль "ClientModuleUnit1" в раздел uses главного модуля формы клиента.

Активируем данный модуль в редакторе и выбираем в меню "File -> Use Unit".

Теперь в коде формы клиента нужно правильно использовать методы,

экспонируемые (предлагаемые) через свойство "ClientModule1.ServerMethods1Client".

Эти методы имеют те же имена и сигнатуры, как и методы, реализованные на сервере.

Ниже показана реализация главной формы клиентского приложения.

unit FormClientUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm3 = class(TForm)
    EditA: TEdit;
    ButtonAdd: TButton;
    EditB: TEdit;
    ButtonSub: TButton;
    ButtonMult: TButton;
    ButtonDiv: TButton;
    LabelResult: TLabel;
    procedure ButtonAddClick(Sender: TObject);
    procedure ButtonSubClick(Sender: TObject);
    procedure ButtonMultClick(Sender: TObject);
    procedure ButtonDivClick(Sender: TObject);
  private
    { Private declarations }
  public
    function GetA: double;
    function GetB: double;
    procedure ShowResult(aValue: double);
  end;

var
  Form3: TForm3;

implementation

uses ClientModuleUnit1;

{$R *.dfm}

function TForm3.GetA: double;
begin
  Result := StrToFloat(EditA.Text);
end;

function TForm3.GetB: double;
begin
  Result := StrToFloat(EditB.Text)
end;

procedure TForm3.ShowResult(aValue: double);
begin
  LabelResult.Caption := FloatToStr(aValue);
end;

procedure TForm3.ButtonDivClick(Sender: TObject);
begin
  ShowResult(ClientModule1.ServerMethods1Client.Divide(GetA, GetB));
end;

procedure TForm3.ButtonMultClick(Sender: TObject);
begin
  ShowResult(ClientModule1.ServerMethods1Client.Multiply(GetA, GetB));
end;

procedure TForm3.ButtonSubClick(Sender: TObject);
begin
  ShowResult(ClientModule1.ServerMethods1Client.Subtract(GetA, GetB));
end;

procedure TForm3.ButtonAddClick(Sender: TObject);
begin
  ShowResult(ClientModule1.ServerMethods1Client.Add(GetA, GetB));
end;

end.

Листинг 2 : Реализация главной формы клиента .

Окончательно моя форма выглядит следующим образом:

Наверное, вам не нужно рассказывать, как воспроизвести ее!

Теперь вы видите, как просто создавать сервера и клиенты DataSnap и использованием Delphi XEJ

Итог

В этом учебном примере мы использовали Delphi XE для построения простого сервиса DataSnap XE в виде калькулятора,

который на уровне реализации представляет собой приложения Win32, при обмене данными между которыми используется протокол TCP/IP.

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 01.08.2011 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Delphi Professional Named User
Enterprise Connectors (1 Year term)
IBM Domino Utility Server Processor Value Unit (PVU) License + SW Subscription & Support 12 Months
SmartBear AQtime Pro - Node-Locked License (Includes 1 Year Maintenance)
Купить CommView for WiFi 1 лицензия
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
СУБД Oracle "с нуля"
Программирование на Visual С++
Работа в Windows и новости компании Microsoft
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100