donderdag 12 januari 2012

"Smart" business (OP4JS, Delphi/Pascal for Javascript)


After reading various blog posts about OP4JS (now named “Smart Mobile Studio”), I was very curious if I could use it for my current customer. We have a couple of RemObjects services, which contain our business logic (multi tier, SOA (service oriented architecture)), and we would like to create a HTML client for it (for iPad and Android).
I did a simple test with the latest alpha release, and it turned out to be relative easy! (it is an alpha, so no code completion, internal debugging, delphi shortcuts etc yet, so I had to do everything by hand :-) ).

First I made a simple RemObjects server using the Delphi Wizard:
Delphi -> New -> Other -> RemObjects SDK -> VCL Standalone:
I selected HTTP server + JSON message format (needed for Javascript communication).

Default it already contains 2 functions: “Sum(A,B: Integer): Integer” and “GetServerTime: TDatetime” (see below for screenshot of the RemObjects Service Builder). When compiling the server for first time, it will generate a datamodule for the server side implementation for these 2 functions (I won’t post the implemention: a “sum” function is too simple :-) ). So we have a RemObjects HTTP server now, which listens to the default port 8099 and uses the JSON data format (normally we use TCP + Binary messages for fast and low overhead messaging).

Next, we need to generate some code for the client side. In the RemObjects Service Builder we can generate various client implementations: C++, PHP, Delphi (also automatic done in Delphi IDE) and also Javascript. In the latest RemObjects release they added a completely new Javascript implementation but I could not get it working yet, so I have taken the "old" JSON RPC implementation:

You need to download some extra Javascript files, because it is dependent on the Yahoo YUI toolkit (I have tested the RemObjects Javascript implementation before, so I already had these files).

After that, I started “Smart Mobile Studio” and played with the delivered demos. I created a new “visual project”, and added 2 edits, a button, and another edit to the form (all typing by hand, no designer yet):

Procedure TForm1.InitializeObject;
Begin
 inherited;

 FEdit1:=TW3EditBox.Create(self);
 FEdit1.setBounds(10,10,100,32);
 FEdit1.text:='2';

 FEdit2:=TW3EditBox.Create(self);
 FEdit2.setBounds(10,50,100,32);
 FEdit2.text:='3';

 FBackBtn:=TW3Button.Create(self);
 FBackBtn.setBounds(10,95,100,32);
 FBackBtn.Caption:='Sum';
 FBackBtn.Visible:=true;
 FBackBtn.OnClick := ButtonClicked;

 FEdit3:=TW3EditBox.Create(self);
 FEdit3.setBounds(10,140,100,32);
 FEdit3.text:='';
End;
I created a “button click” procedure, which calls my RemObjects server. Because I have to call some pure Javascript code, I used the “asm” keyword for my “low level” code:
Procedure TForm1.ButtonClicked(Sender:TObject);
var
 s1, s2, s3: string;
Begin
 s1 := Self.FEdit1.text;
 s2 := Self.FEdit2.text;
 asm
   var service = new NewService("http://localhost:8099/JSON");
   service.Sum(@s1,
               @s2,
               function(sumresult)
               {
                  TW3EditBox.setText(Self.FEdit3,sumresult);
               });
 end;
End;
Note: I had to put the values of the 2 edits in seperate variables, because I could not directly use “@FEdit1.text” in the “asm” block? For the result callback (async!) I had to do something different, so I used “TW3EditBox.setText(FEdit3,result)” because this code is generated by SmartMobileStudio too when you type “FEdit3.text := ‘value’.

This code almost works: I have to include the client Javascript files somewhere. You can include Javascript in the project, but also in the main index.html (like you normally do with html).   
 script type="text/javascript" src="lib/yahoo.js">
 script type="text/javascript" src="lib/event.js">
 script type="text/javascript" src="lib/json.js">
 script type="text/javascript" src="lib/connection.js">
However, SmartMobileStudio uses a temp dir when running from the IDE (so it misses my extra .js files), so I just compiled the project, and ran the generated index.html directly from the project dir in Google Chrome.

So it’s done now? No, I got an error in Chrome, because I call an other HTTP server from my (local) index.html:
I had to enable cross site calls in myRemObjects HTTP server:
 ROServer.SendCrossOriginHeader := True;
But then Chrome complains about “Access-Control-Allow-Headers”, so I manually changed this by overriding the RemObjects implementation of the internal Indy “Other” function:

ROHttpServerCommandOther           := ROServer.IndyServer.OnCommandOther; ROServer.IndyServer.OnCommandOther := InternalServerCommandOther;

procedure TServerForm.InternalServerCommandOther(AContext: TIdContext;
 ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
 //original RO handling
 ROHttpServerCommandOther(AContext, ARequestInfo, AResponseInfo);

 //need to set extra allow-header
 if ROServer.SendCrossOriginHeader then
  AResponseInfo.CustomHeaders.Values['Access-Control-Allow-Headers'] := 'Origin,   X-Requested-With, Content-Type';
end;

And then it works! After the initial problems, it should be easy now to extend the functionality further. For example, I would like to create a "Javascript to Delphi wrapper class" generator based on the .rodl interface file, so I have type safety again (no asm sections in code, only in wrapper class).
I hope to blog about this more in the future...

Note: demo code + binaries can be found here. First start "\ROSmartTest\NewProject.exe" and then "\Project1\Project1\index.html"

1 opmerking:

Eric zei

To get technical:

“@FEdit1.text” in the “asm” block?

Currently you would need "@Self.FEdit1" in the asm, but "text" is a property, so not safe in asm (since it could change between a field access or a getter, which could be renamed by the obfuscator, etc.).

However you could use variants to minimize the asm, with something like (out of the top of my head):

var service, setResultFunc : Variant;
...
asm
@service = new NewService("http://localhost:8099/JSON");
@setResultFunc = function(sumresult) {
@TW3EditBox.setText(@Self.FEdit3,sumresult);
};
end;
service.Sum(Self.FEdit1.Text, Self.FEdit2.Text, setResultFunc);

This way the property reads are handled by the compiler rather than the asm, and the asm should survive obfuscation unscathed.