Delphi Labs: DataSnap XE - "Plain Old Delphi Object" Parameters

By: Pawel Glowacki

Abstract: In this article we are going to have a look at different types of parameters you can pass between DataSnap client and server applications. As a demo project we are going to pass parameters that are plain Delphi classes. In Java Enterprise Edition there is a concept of “Plain Old Java Objects” for passing data between applications. Here we are going to use the same pattern for exchanging data between clients and servers, so it should be OK to call parameter types used in the demo: “Plain Old Delphi Objects” or in short “PODOs”!

    Introduction

DataSnap architecture allows for exchanging a broad range of parameter types between client and server applications. DataSnap servers can be implemented in Delphi or C++Builder, and there are even more options for DataSnap clients. You can build DataSnap clients in Delphi, C++Builder, RadPHP, Delphi Prism and in JavaScript. Depending on the language implementation of a client application there can be some obvious limitations in terms of parameters that you can use in defining DataSnap server methods. The biggest range of possible parameter types is in the scenario where the server and client are both Delphi applications.

If we use Delphi to develop DataSnap client and server applications, we can exchange a lot of different data types including basic, DBX-related and special types, like collections, JSON or even arbitrary TObject-descendants.

If your object contains references to other objects that would probably not work, but if it just an independent data structure, the DataSnap framework will remove some of the complexity related to the process of serialization and deserialization.

You can find in-depth discussion of different DataSnap parameter types on Jim Tierney’s blog (http://blogs.embarcadero.com/jimtierney) and on the “DataSnap: Features and Integration with User Types” CodeRage5 conference presentation. The recording is available at http://channel-e.embarcadero.com/index.php?option=com_jvideodirect&x=1&v=7S1VFt1153jb3.

In general when a given type is not supported, the DataSnap proxy generator will just ignore it during client proxy code generation.

In this tutorial I’m going to discuss steps needed to implement a DataSnap server that uses as parameter types plain Delphi “TObject-descendants”. In Java Enterprise Edition (JEE) there is a concept of “Plain Old Java Object” (http://en.wikipedia.org/wiki/Plain_Old_Java_Object) for passing data between applications. Here we are going to use the same pattern for exchanging data between clients and servers written in Delphi, so it should be OK to call these parameter types: “Plain Old Delphi Objects” or in short “PODOs”.

Let’s give it a try.

    Building the Server

Click on the “File – New – Other” menu and inside the “New Items” dialog double-click on the “DataSnap Server” wizard in “Delphi Projects – DataSnap Server” category to create a standalone Delphi DataSnap server application.

Hide image
DataSnapServerWiz

In the first tab of the wizard select keep the default “VCL Forms Application” as the application type.

Hide image
Click to see full-sized image

expand view>>

Uncheck generating sample methods option and keep all the default values for other server features, so our service is going to use TCP/IP protocol for communication with no authorization/authentication. We are going to implement server methods ourselves later.

Hide image
Click to see full-sized image

expand view>>

On the third screen keep the default TCP/IP port 211 and check if it is open.

Hide image
Click to see full-sized image

expand view>>

On the last page of the wizard keep the default base class of the server methods implementation and click on “Finish”.

Select “File – Save All” to save all the files in the new project generated by the wizard. In my case I am going to create a new “C:\DataSnapLabs\PlainOldDelphiObjectParams\” folder for my files. Name the server unit “FormServerUnit” and keep the default names of the server methods and server container units (for example “ServerMethodsUnit2” and “ServerContainerUnit2”) and name the whole project as “PODOServer”.

Let’s define a custom Delphi class that we are going to use as a parameter.

The code in both server and client projects need to know types of classes that we are going to exchange, so it makes a lot of sense to define these objects in a separate unit that is “used” by both projects.

Add a new unit to the server project and save it as “SharedStuffUnit”.

Replace its contents with the following implementation of a fictional “TPerson” class.

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.

Now we need to “use” this new unit in the unit where our server methods are implemented.

Open the server methods unit and Select “File – Use Unit” from the menu and the new unit to the “uses” clause in the interface section.

Let’s implement a server method called “GetPerson” that will accept two string parameters for the first name and last name and returns a “TPerson” instance initialized with these values.

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.

OK. Our server is ready. Now it just needs to be started (“Run – Run Without Debugging”) and minimized. It is required to be running during the development of our client application.

    Building the Client

Right-click on the project group icon in the “Project Manager” and select “Add New Project”.

Choose for Delphi “VCL Forms Application”.

Save All.

Save all new units in the same directory as server source files.

I’m going to save the main form unit as “FormClientUnit”, project as “PODOClient” and the whole project group as “PODOGroup”.

Now there is an important step that is easy to overlook! Because both server and client are in the same directory that is why Delphi compiler would find our “SharedStuffUnit” unit and “TPerson” class, but in general we need to make sure to explicitly add the shared unit to the client project as well.

The easiest way to achieve this is to select “SharedStuffUnit” in the Project Manager and “drop” it on the client project node.

Hide image
adding shared unit to client app

Now we need to generate DataSnap client proxy code, so we could invoke server methods from the client. The easiest way to do so is with the “DataSnap Client Module” wizard.

You can keep all the default values at all four wizard screens.

Hide image
add client module wizard

On the first tab of the wizard keep the default server location to “Local Server”.

Hide image
Click to see full-sized image

On the second tab keep the default server type which is standalone.

expand view>>

Hide image
Click to see full-sized image

On the next screen keep the default value for connection protocol, which is “TCP/IP”.

expand view>>

Hide image
Click to see full-sized image

On the last page keep the default port 211 and check if you can connect to the server with “Test Connection” button.

expand view>>

Hide image
Click to see full-sized image

Click “Finish” button.

expand view>>

The wizard will add two new units to our client project: “ClientModuleUnit1” and the “ClientClassesUnit1”.

Save all and keep default names for new units.

Let’s inspect the contents of the client classes unit, which was generated by the DataSnap Client Proxy generator.

// 
// 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 proxy generator automatically used the shared unit and also generated code to unmarshal a “TPerson” instance from its “over-the-wire” JSON representation! Very clever!

The last step is to build a simple GUI to call our server method that returns a “TPerson” instance.

Open client form unit.

Select “File – Use Unit” and add all unused unit to the implementation section of the client form unit.

Add two “TEdit” and one “TButton” components to the form.

Name the first edit “edtFirstname” and the second “edtLastname”.

Change button’s “Caption” property to “Show Person” in Object Inspector.

Double-click on the button and implement the on-click event that calls a server method “GetContact” passing the contents of edits as parameter values and displays the result of “ToString” method of the “TPerson” instance received from the server.

Here is the listing of the client unit:

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.

If you enter some strings as “firstname” and “lastname”, you should be able to see them displayed in the dialog.

Hide image
client in action

That’s it! We just managed to build a simple demo consisting of a Delphi DataSnap server and client with a server method that returns a custom object.

A “Plain Old Delphi Object” that is!

    Memory Management

A closer inspection into the event handler code for retrieving an object instance from a proxy class and calling its methods reveals some issues. Shouldn’t we check if “p” is not nil, before accessing it members? Are we leaking memory? Should we free the “p” reference at the end of the method?

First of all it is always a good idea to check if the reference returned from a server method is “nil” or “not nil”. This could be an application logic to return “nil” for certain parameter values, so we should always be prepared for reading a “nil” reference.

The answer to the second question, if we need to call “p.Free” at the end of the “Button1Click” method, is more complex. In this particular scenario we do not need to free “p” reference because it is owned by the server methods proxy class. If we look into the “ClientModuleUnit1” unit, we will see that “TClientModule1” class has “InstanceOwner” property that is initialized to “true” in its constructor.

However the internal server methods instance class is not created in the constructor. It is rather “lazy created” in the “ServerMethods2Client” property getter. This is also the moment when the current value of “InstanceOwner” is passed to its constructor.

If we want to manually manage the lifetime of received instances from server methods proxy classes, we can set “ClientModule1.InstanceOwner” property to “false”, but it has to be done before reading from “ClientModule1.ServerMethods2Client” property.

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

  // internal server methods instance is lazy created on first get,
  // so it has to be set before reading
  // "ClientModule1.ServerMethods2Client" property
  // as it would not have any effect afterwards
  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;

Another good question would be how is the “TPerson” reference constructed at the client anyway?

The “GetPerson” class generated by the DataSnap Client Proxy generator has some very interesting code.

//  …

  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;

// …

This code was generated automatically from the running DataSnap server. Very cool! The generator knew that it has to use our “SharedStuffUnit” in the generated code. The value returned from “GetPerson” method is in fact the result from “TJSONUnMarshal.Unmarshal” method type-casted to “TPerson” reference.

    Summary

It is like diving deeper and deeper. The DataSnap architecture is very elegant, and features extensible JSON converter/reverter framework for serializing and deserializing objects. If you want go deeper have a look into the “DBXJSON” and “DBXJSONReflect” units. There are plenty of very useful documentation comments in there!

In fact the support for serializing and deserializing Delphi objects is very useful on its own! Not only in conjunction with DataSnap architecture and passing objects over the wire.

DataSnap architecture allows for exchanging a broad range of parameter types between client and server applications. Depending on the language implementation of a client there can be some obvious limitations. The biggest range of possible parameter types is where the server and client are both Delphi applications.

In this “Delphi Labs” episode we have looked into the steps of building Delphi DataSnap server and client that returns an instance of a simple “TPerson” class with two string properties.

We have also discussed different options for managing memory of object references received from server methods.

At the of end of this episode we looked closer into the source code generated by DataSnap Client proxy generator and discovered JSON serialization and deserialization architecture that can be used on its own, not only in conjunction with DataSnap architecture and passing objects over the wire.

You can find in-depth discussion of different DataSnap parameter types on Jim Tierney’s blog (http://blogs.embarcadero.com/jimtierney) and on the “DataSnap: Features and Integration with User Types” CodeRage5 conference presentation. The recording is available at http://channel-e.embarcadero.com/index.php?option=com_jvideodirect&x=1&v=7S1VFt1153jb3.

The source code for this article is available at the Code Central (http://cc.embarcadero.com/item/28276) and the video version of this demo is available on YouTube (http://www.youtube.com/watch?v=yWP2ZkVRx08).

More information about "Delphi Labs":

Server Response from: ETNASC03