DataSnap connectivity for iOS using Delphi XE2 and FireMonkey

By: Anders Ohlsson

Abstract: This article discusses how you can connect to existing DataSnap servers from an iOS application using RAD Studio XE2

    Problem: Data connectivity for iOS?

Let's start with a disclaimer. There is currently no data connectivity in FireMonkey for iOS in the currently shipping version of RAD Studio XE2. Data connectivity for iOS is of course very important, and it is being planned, prioritized and road mapped. Please see edn.embarcadero.com and blogs.embarcadero.com for more information about roadmaps.

    Solution: Mobile DataSnap connector for ObjectiveC!

This article discusses how you can use the ObjectiveC mobile DataSnap Connector that does ship with RAD Studio XE2.

First, a shout-out. Phil Hess was of great assistance in helping me getting a grasp on ObjectiveC versus Pascal. He parsed all the ObjectiveC header files for the mobile DataSnap connector and sent them to me. The result is a collection of about 70 files. The files are as follows:

  • dsproxybase (directory of 56 files)
  • sbjson (directory of 13 files)
  • DSProxyBase.pas
  • AnonClassDefinitionsDsproxybase.pas
  • AnonClassDefinitionsSbjson.pas

    Parsing the headers of the ObjectiveC connector

Phil took the existing ObjectiveC to FreePascal parser from here:

http://web.me.com/macpgmr/ObjP/Xcode4/iOS_Parsing_Status.html

He had to patch it to fix a few things in order to parse SBJson and DSProxyBase properly. The resulting parsed Pascal files then needed a few edits to work around thigs that the parser does not yet handle.

Instead of providing all the steps on how to reproduce the parsing, I'm simply providing the files as parsed and edited by Phil Hess here, along with the entire project (server and client) as demonstrated in this article:

http://cc.embarcadero.com/item/28579

    Writing the DataSnap server

We will start by creating a DataSnap server by selecting File | New | Other | Delphi Projects | DataSnap Server | DataSnap REST Application.

Hide image
Click to see full-sized image

We go through the expert and select Stand-Alone VCL application in this case:

Hide image
Click to see full-sized image

We then select a port and test it:

Hide image
Click to see full-sized image

On the next screen we simply go with the defaults (sample methods and sample web files) for simplicity:

Hide image
Click to see full-sized image

On the next screen we'll pick TDataModule as the ancestor:

Hide image
Click to see full-sized image

We then finish up and save the project as EmployeeServer.dproj somewhere on disk.

    Adding our business logic

Now we need to add some actual useful methods that implement our business logic. In this case we will provide two main methods - GetRecordCount and GetRecord. We'll discuss GetRecords a bit later.

Let's first add a TSQLConnection and a TSQLDataSet to our ServerMethodsUnit1 (default name):

Hide image

We're simply going to connect to one of the demo databases that ship with RAD Studio XE2 (EMPLOYEE.GDB) in the Samples directory. The TSQLConnection and the TSQLDataSet will have the following parameters set, respectivelly:

Hide image
Hide image

Here's the declaration of our TServerMethods1 (default naming again). We add GetRecordsCount, GetRecord and GetRecords.

type
  TServerMethods1 = class(TDataModule)
    MyDB: TSQLConnection;
    MyDS: TSQLDataSet;
  private
    { Private declarations }
  public
    { Public declarations }
    function GetRecordCount: Integer;
    procedure GetRecord(RecNo: Integer; var FirstName, LastName: String);
    function GetRecords : TJSONArray;
  end;

The implementation of GetRecordCount is very simple as follows:

function TServerMethods1.GetRecordCount: Integer;
begin
  MyDS.Open;
  Result := MyDS.RecordCount;
  MyDS.Close;
end;

The implementation of GetRecord is also fairly simple. It's also very inefficient, but let's not get into that here... This is only done so that I can get a specific record by record number as a simple demo.

procedure TServerMethods1.GetRecord(RecNo: Integer; var FirstName, LastName: String);
var
  i: Integer;
begin
  MyDS.Open;
  for i := 0 to RecNo-1 do
    MyDS.Next;
  FirstName := MyDS.FieldByName('FIRST_NAME').AsString;
  LastName := MyDS.FieldByName('LAST_NAME').AsString;
  MyDS.Close;
end;

Finally, as a comparison, I have another method - GetRecords - that gives me all of the records as a TJSONArray. It also has a comment section that describes how to get the data back out of the array on the Win/Mac side (different code applies to iOS).

function TServerMethods1.GetRecords : TJSONArray;
var
  NewArr : TJSONArray;
  NewObj : TJSONObject;
  Val : TJSONValue;
  Pair : TJSONPair;
begin
  NewArr := TJSONArray.Create;
  MyDS.Open;
  while not MyDS.EOF do begin
    NewObj := TJSONObject.Create;
    NewObj.AddPair('LastName',MyDS.FieldByName('LAST_NAME').AsString);
    NewObj.AddPair('FirstName',MyDS.FieldByName('FIRST_NAME').AsString);
    NewArr.AddElement(NewObj);
    MyDS.Next;
  end;
  MyDS.Close;
  Result := NewArr;

  // You get the data back by doing this:
  // TJSONObject(NewArr.Get(index)).Get('LastName').JsonValue.Value;
  // TJSONObject(NewArr.Get(index)).Get('FirstName').JsonValue.Value;
end;

    Testing the DataSnap server

We can now compile and run the DataSnap server. It pops up on the screen as below:

Hide image
  

If we click the Open Browser button we get presented with the test harness. Notice that we haven't written a client yet. This is all done by the fact that we wrote the above server.

Hide image
Click to see full-sized image

Here we can expand the nodes for our actual server methods that do the real work of the server. Below GetRecordCount as executed (result is 42 records):

Hide image
Click to see full-sized image

GetRecord looks as follows. Notice that we provide the input value (RecNo = 0), and we get back Robert Nelson as our record data.

Hide image
Click to see full-sized image

Finally, GetRecords as executed (gives us all records):

Hide image
Click to see full-sized image

    Writing the iOS client

Now, on to writing the iOS client that will talk to our server. We create a new FireMonkey HD application for iOS. The UI part is the simple part here:

Hide image

Now we have to write the actual code behind the button that retrieves the data... Disclaimer: There may very well be much more elegant ways of writing this, especially the string conversions between FPC/ObjC and Delphi.

The first thing that is necessary at the top of the unit is the following directive to the FreePascal compiler in order to compile our FPC/ObjC type conversion code.

unit Unit1;

{$IFDEF FPC}
{$modeswitch ObjectiveC1}
{$ENDIF}

In the interface section we'll add iPhoneAll, SBJson and DSProxyBase. The iPhoneAll unit is part of FPC as shipping with RAD Studio XE2 and gets installed when you install the Mac parts for iOS development.

The SBJson and DSProxyBase units are the ones Phil parsed for me, and necessary in order to work with JSON and communicating with the DataSnap server.

interface

uses
  SysUtils, Types, UITypes, Classes, Variants, FMX_Types, FMX_Controls, FMX_Forms,
  FMX_Dialogs, FMX_Edit, FMX_Layouts, FMX_Memo, FMX_ListBox
{$IFDEF FPC}
  ,iPhoneAll, SBJson, DSProxyBase
{$ENDIF}
  ;

We'll declare a connection variable first:

var
  Connection : DSRESTConnection;

Next up, GetRecordCount:

function GetRecordCount: Integer;
var
  cmd : DSRESTCommand;
begin
  cmd := Connection.CreateCommand;

{$IFDEF FPC}
  cmd.setRequestType(GET);
  cmd.setText(NSSTR(PChar('TServerMethods1.GetRecordCount')));
  cmd.prepare(
    NSArray.arrayWithObjects(
      DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
        NSSTR(PChar(String(''))),4{ReturnValue},Int32Type,NSSTR(PChar(String('Int32')))),
      nil
    )
  );
{$ENDIF}

  cmd.execute;

{$IFDEF FPC}
  Result := cmd.getParameterByIndex(0).getValue.GetAsInt32;
{$ENDIF}
end;

Let's break this one down. We declare a REST command variable first:

var
  cmd : DSRESTCommand;

We create the command:

  cmd := Connection.CreateCommand;

We then set the request type (REST request types include GET/POST/etc):

  cmd.setRequestType(GET);

Next, we set the text of the request. This is the fully qualified name of the method we're going to call on the DataSnap server:

  cmd.setText(NSSTR(PChar('TServerMethods1.GetRecordCount')));

We then prepare the command. Prepare takes one parameter. This parameter is an array of DSRESTParameterData. In this case the only parameter is the return value from GetRecords. Since it is a return parameter, it is nameless (empty string). We pass it as Int32Type. We also pass Int32 as a string as the last parameter.

  cmd.prepare(
    NSArray.arrayWithObjects(
      DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
        NSSTR(PChar(String(''))),4{ReturnValue},Int32Type,NSSTR(PChar(String('Int32')))),
      nil
    )
  );

We then execute the command:

  cmd.execute;

Finally, we extract the return value and return it from our function:

  Result := cmd.getParameterByIndex(0).getValue.GetAsInt32;
end;

Much in the same way, GetRecord gets implemented as below. Notice that we now have one input parameter (RecNo), two output parameters (FirstName and LastName) and no return value (it's a procedure).

procedure GetRecord(RecNo: Integer; var FirstName, LastName: String);
var
  cmd : DSRESTCommand;
begin
  cmd := Connection.CreateCommand;

  cmd.setRequestType(GET);
  cmd.setText(NSSTR(PChar('TServerMethods1.GetRecord')));
  cmd.prepare(
    NSArray.arrayWithObjects(
      DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
        NSSTR(PChar(String('RecNo'))),1{Input},Int32Type,NSSTR(PChar(String('Int32')))),
      DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
        NSSTR(PChar(String('FirstName'))),2{Output},WideStringType,NSSTR(PChar(String('String')))),
      DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
        NSSTR(PChar(String('LastName'))),2{Output},WideStringType,NSSTR(PChar(String('String')))),
      nil
    )
  );
  cmd.getParameterByIndex(0).getValue.setAsInt32(RecNo);

  cmd.execute;

  FirstName := String(PChar(cmd.getParameterByIndex(1).getValue.GetAsString.UTF8String));
  LastName := String(PChar(cmd.getParameterByIndex(2).getValue.GetAsString.UTF8String));
end;

Now for the "easy" part. Actually calling our methods and getting that data displayed in our client. Notice that the first thing we do here is create and set up the DataSnap connection. We provide the host address, the port, and the protocol to be used.

We then call GetRecordCount and iterate over all records to get them displayed in our UI.

procedure TForm1.GetDataButtonClick(Sender: TObject);
var
  i : Integer;
  FirstName, LastName : String;
begin
  Connection := DSRESTConnection.alloc.init;
  Connection.setConnectionTimeout(5);
  Connection.setHost(NSSTR(PChar(String(HostEdit.Text))));
  Connection.setPort(StrToInt(PortEdit.Text));
  Connection.setProtocol(NSSTR(PChar(String('http'))));

  for i := 0 to GetRecordCount-1 do begin
    GetRecord(i,FirstName,LastName);
    Memo1.Lines.Add(LastName+', '+FirstName);
  end;
end;

end.

    Compiling the client

Once the client is created. We run dpr2xcode to generate the extra information needed for Xcode. We then open the project in XCode.

    Testing the client

Actual screen shot of the client running on my iPhone 4:

Hide image
Click to see full-sized image

Just as a side-note, I also wrote code that allows you to test the client under Win32. Here's a screen shot of what the client looks like on Win32.

Hide image
Click to see full-sized image

    Comparing the implementation to Windows

If you have experience with JSON and DataSnap on Windows, then you'll be familiar with the following.

This is what GetRecordCount could be implemented as on the client side:

var
  GetRecordCountParams : array[0..0] of TDSRESTParameterMetaData =
    (
      (Name : ''; Direction : 4{ReturnValue}; DBXType : TDBXDataTypes.Int32Type; TypeName : 'Int32')
    );

function GetRecordCount: Integer;
var
  cmd : DSRESTCommand;
begin
  cmd := Connection.CreateCommand;
  cmd.RequestType := 'GET';
  cmd.Text := 'TServerMethods1.GetRecordCount';
  cmd.prepare(GetRecordCountParams);
  cmd.execute;
  Result := cmd.Parameters[0].Value.AsInt32;
end;

GetRecord could be implemented as such:

var
  GetRecordParams : array[0..2] of TDSRESTParameterMetaData =
    (
      (Name : 'RecNo'; Direction : 1{Input}; DBXType : TDBXDataTypes.Int32Type; TypeName : 'Int32'),
      (Name : 'FirstName'; Direction : 2{Output}; DBXType : TDBXDataTypes.WideStringType; TypeName : 'String'),
      (Name : 'LastName'; Direction : 2{Output}; DBXType : TDBXDataTypes.WideStringType; TypeName : 'String')
    );

procedure GetRecord(RecNo: Integer; var FirstName, LastName: String);
var
  cmd : DSRESTCommand;
begin
  cmd := Connection.CreateCommand;
  cmd.RequestType := 'GET';
  cmd.Text := 'TServerMethods1.GetRecord';
  cmd.prepare(GetRecordParams);
  cmd.Parameters[0].Value.SetInt32(RecNo);
  cmd.execute;

  FirstName := cmd.Parameters[1].Value.AsString;
  LastName := cmd.Parameters[2].Value.AsString;
end;

Finally, the button event handler that gets the number of records and iterates over the dataset to get all records looks almost exactly the same:

procedure TForm1.GetDataButtonClick(Sender: TObject);
var
  i : Integer;
  FirstName, LastName : String;
begin
  Connection := TDSRestConnection.Create(Self);
  Connection.Host := HostEdit.Text;
  Connection.Port := StrToInt(PortEdit.Text);
  Connection.Protocol := 'http';

  for i := 0 to GetRecordCount-1 do begin
    GetRecord(i,FirstName,LastName);
    Memo1.Lines.Add(LastName+', '+FirstName);
  end;
end;

    Resources

Download the whole project (server and client) from here:

http://cc.embarcadero.com/item/28579

ObjectiveC to FreePascal parser:

http://web.me.com/macpgmr/ObjP/Xcode4/iOS_Parsing_Status.html

    Contact

Please feel free to email me with feedback to aohlsson at embarcadero dot com.

Server Response from: ETNASC04