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:
Writing the DataSnap server
We will start by creating a DataSnap server by selecting File | New | Other | Delphi Projects | DataSnap Server | DataSnap REST Application.

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

We then select a port and test it:

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

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

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):

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:

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
public
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;
end;
Testing the DataSnap server
We can now compile and run the DataSnap server. It pops up on the screen as below:
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.

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):

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

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

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:

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;
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
,iPhoneAll, SBJson, DSProxyBase
;
We'll declare a connection variable first:
var
Connection : DSRESTConnection;
Next up, GetRecordCount:
function GetRecordCount: Integer;
var
cmd : DSRESTCommand;
begin
cmd := Connection.CreateCommand;
cmd.setRequestType(GET);
cmd.setText(NSSTR(PChar('TServerMethods1.GetRecordCount')));
cmd.prepare(
NSArray.arrayWithObjects(
DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
NSSTR(PChar(String(''))),4,Int32Type,NSSTR(PChar(String('Int32')))),
nil
)
);
cmd.execute;
Result := cmd.getParameterByIndex(0).getValue.GetAsInt32;
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,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,Int32Type,NSSTR(PChar(String('Int32')))),
DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
NSSTR(PChar(String('FirstName'))),2,WideStringType,NSSTR(PChar(String('String')))),
DSRESTParameterMetaData.parameterWithName_withDirection_withDBXType_withTypeName(
NSSTR(PChar(String('LastName'))),2,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:

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.

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; 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; DBXType : TDBXDataTypes.Int32Type; TypeName : 'Int32'),
(Name : 'FirstName'; Direction : 2; DBXType : TDBXDataTypes.WideStringType; TypeName : 'String'),
(Name : 'LastName'; Direction : 2; 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
Contact
Please feel free to email me with feedback to aohlsson at embarcadero dot com.
Connect with Us