One thing I often ponder is why I like the RemObjects SDK for my developments, when things like REST are “free” and perhaps more standard. The simple answer is that my code looks just like any other code I write. For this example, I have written a demonstration “chat” application. You log in, you can post messages, and you get all the messages from the server. This is the procedure that sends a message to the server:
procedure TfrmLogin.SendMessage(szMessage : String); begin m_xChatService.SendMessage(szMessage, SendMessageComplete, OnLinkError); end;
There’s no building up a URL, no encoding of parameters. Just a normal function call to a class member. If this wasn’t a WebBuilder function, I’d not have the SendMessageComplete callback – it would just wait until the call returned, but the browser is always asynchronous, so we have the callback:
procedure TfrmLogin.SendMessageComplete(nResult : Integer; szHumanResult: String); begin Report('Sent: ' + szHumanResult); end;
That’s all there is in the client for normal coding. On the server, this is the code that implements the service:
function TChatService.SendMessage(const szMessageJSON: Utf8String; out szHumanResult: Utf8String): Integer; begin Result := 1; g_xMessageList.Add(szMessageJSON); end;
Of course there is an amount of setting up, but once you have done the core plumbing once, you just use the RemObjects Service Editor to define the functions and their parameters. You can use all the usual types of parameters, like integers and strings (ANSI, UTF8, Wide), binary streams and more. You can send complex structures, but I tend to use JSON in UTF8Strings for such. It then generates all the files you need (and can ignore).
TL;DR – that’s it
If you want to know how easy it is, that’s the summary. You can actually fiddle about with the function definitions any time you want. If you add a new parameter to the SendMessage function and then call it from old code, it will probably give an error. During development, that doesn’t matter because you recompile both sides and the error goes away. Once you ship, that’s not so good if you can’t control the clients, so you an do things like have a “GetVersion” call. I use that to pass in the client version, and get back the server version and an “OK” response. If the server can’t work with the client version, it doesn’t give the OK. If the client can’t work with that server, it can take action. But most important, they can adjust their behaviours to suit. So the client can see that the server doesn’t support the new SendMessageEx function, and restrict itself to SendMessage.
The rest of this article discusses the detailed steps needed to make the sample Chat application in Elevate WebBuilder. I hope to cover everything important, but you can download the full source and executable if you want to try it. It is not polished at all – I want to keep it simple to demonstrate the SDK use.
The Detailed Project
If you want to get into the details, this is for you. You start by using the Delphi New Project dialog to create a new RemObjects Service project.
When you build the project, the RemObjects IDE add-ins will automatically create the data module for you. You also have a module which is the Windows Service interface, and I rename this to something sensible (ServiceModule in my example). You will probably want to use the Delphi property editor to set the name to something suitable.
So now we can open the RemObjects SDK service editor (top of their new menu in the Delphi IDE) and define the requirements of our service. You can create multiple “services” that the server will provide if you wish. You might for example have an administration service, separate to your end user service. You can also decide whether you want the user to have to be logged in to the service before it can access a particular service. To enforce this, you create a login service, and that either creates a session, or doesn’t (see code later). The services you require to be secure are then set to require a valid session using a property. After you have defined your services, you build in Delphi, and the IDE add-in auto-creates modules for each service that doesn’t have one yet.
At this point it is worth being sure of the difference between the “service” which is provided over the web, and the “Service” which is the Windows Service application that is installed to run in the background and provide those web services. Delphi provides the normal user interface for the Windows Service. You can see that RemObjects SDK put a number of suitable components on it so that it was functional:
You can choose from a variety of communications means, but the way seems certain that the HTTP channel and JSON messages are the way forward, so that is what it is using here. The main thing to note is that the service module has the Session Manager. This is what the various services will link to for user verification. Just occasionally I find that the modules get unlinked if I’m doing complex changes, and the service will then fail with an error. Easy to fix by re-linking to the session manager. Also worth noting is that the SDK offers a variety of session managers, from the easiest memory based one to a full multi-platform compatible database backed one. You can thus scale up if needed.
So, the next thing is to implement the services, which is done in the modules. In the ChatService_Impl.pas of the sample, I made a simple TStringList holder for the messages. I’d never do this in a real project because it is not thread safe at all, and the modules will be called from many threads, but it serves the purpose of the demonstration. In complex Services, you may want to do some advanced things, like have database connections. The SDK supports a way to allow you to derive a custom Session object from the standard one, and you can then add any other variables you wish. Session lifetime is also easily controlled with the SDK.
As you add new functions to the interface, or edit the existing ones, you will find that Delphi then complains with an error saying things are missing. Control-click on the IChatService reference to jump to its location. You can then copy the function definition, return to the error, and paste it, which is thus much more reliable. You can use the Delphi auto-completion for new functions, and then fill them in.
In our Login service, the freshly created Login function has a warning about an undefined return value – we need to fill in the code. The SDK will automatically create a session, so we have to be sure to destroy it if the login failed. If the login succeeds, then we want to keep it, and the reference here ensures this. (The way sessions worked changed some time back, but this is how to do it today.)
function TLogin.Login(const szUserName: Utf8String; const szPassword: Utf8String; out szHumanResult: Utf8String): Boolean; begin Result := (szPassword = 'secret'); if not Result then begin DestroySession; // Informs the SessionManager to discard the session because the login failed end else begin Session; end; end;
The Service module has the TROInMemorySessionManager (add one if required). Then, link the Login Module’s SessionManager property to the service module’s session manager. This ensures that the login creates a session that the other services can use. Now we must set RequiresSession in the ChatService_Impl data module. This ensures that the user is already logged on before any of your code will be called. Note that RequiresSession is not set in the Login module, so that we can create the session there. (We must include the ServiceModule in the ChatService_Impl unit’s section so that it can be referenced in the component as the session manager. Likewise for the Login, except that should not require a session.)
That’s the service all done. There is also a Delphi Client application created, and that serves no purpose in this example, so will be ignored other than to say you can of course use your service from multiple places – many of mine have an application which provides the management user interface and uses the services interface to pass control messages back and forth.
Having a service is nice, and is what you want for deployment so that it just sits in the background regardless of anyone being logged in. Essential on a web service. But it isn’t so good for debugging, but making your service debuggable is simple.
Using the normal Delphi tools, create a new VCL application in the program group. I tend to put this in a sub-directory of the service, called TestHarness, just to keep it tidy. A default form is created too. Name this something sensible, like frmTestHost. Add FormCreate and FormClose functions:
procedure TfrmTestHost.FormCreate(Sender: TObject); var bStarted : boolean; begin bStarted := False; Caption := Caption + ' Live';
DemoChatService := TDemoChatService.Create(self); DemoChatService.ServiceStart(nil, bStarted); end;
procedure TfrmTestHost.FormClose(Sender: TObject; var Action: TCloseAction); var bStopped : boolean; begin DemoChatService.ServiceStop(nil, bStopped); FreeAndNil(DemoChatService); end;
Okay, this code is quite simple. In the FormCreate, it creates an instance of the service module, then tells it to start. In the FormClose, it tells the service to stop, then cleans it up. That’s the easy part.
In the new test host, create a new RO Service using the RemObjects menu “Convert to RemObjects Server Host” (wording may vary). Then, in the editor, use the Edit menu “Use existing RODL” to link to the existing RODL. This ensures that the test host is set up correctly for RemObjects SDK to serve the services of the Service. Add the service implementation files to the test host. When you build, a dummy service module will also be created – you can ignore this.
Completing the details
So, now we have a standard application that will run easily, and which we can use for a faster debug cycle. Now we just have to sort out the details.
For the ROMessage component, I have these properties set. You may like to investigate their meaning, but I know they work for me like this:
SessionIdAsId = True WrapResult = True SendExtendedException = True IncludeTypeName = True
Okay, so now we have our web service. We need something to connect to it, and I’ve found the Elevate Software WebBuilder really productive as a Delphi developer. It has lots of high level language capabilities that make writing solid browser based single-page applications easy. For this demo I created a new project with two forms: one for login, one for the chat display.
As I write this, Elevate WebBuilder 2 is about to be launched, but the details are the same for both as the user interface is not important for this sample (I took no time polishing it, sorry!). This image shows the WebBuilder IDE and the login form:
We have two fields and a button to log in with. On the Chat display, we have an editor, a Send button, and a list into which the messages will be put. Now we have to make the WebBuilder application do something useful.
First, we add the ROSDKUtils to the units at the top of the form. This is a file that came from Robert Devine on the peer-support newsgroups and has had a few small modifications over time I think. Add this to the project.
external RemObjects: TRemObjects;
Now we want to create a connection using that object class, which we can call in the FormCreate.
procedure TfrmLogin.CreateConnection; var szServer : String; begin szHostAddress := window.location.host; szServer := 'https://' + szHostAddress + '/JSON'; FChannel := Remobjects.SDK.HTTPClientChannel.Create(szServer); FMessage := Remobjects.SDK.JSONMessage.Create; m_xNewUserService := TChatService.Create(FChannel, FMessage, 'ChatService'); end;
This determines the connection point from the current hosts information, and then creates a channel and a message object which are used to communicate. Sorry, I use a sort of mangled “hungarian” notation for my variable names, and the m_x prefix means that it is an object reference member of the TfrmWelcome object.
external TOnErrorCallback = procedure (msg: TMessage; ex: EError) of object; external TOnGetServerTimeCallback = procedure(nResult : Integer ) of object; external TOnSendMessageCallback = procedure(nResult : Integer; szHumanResult: String ) of object; external TOnGetMessagesCallback = procedure(nResult : Integer; nTopMessage: Integer; szMessageJSON: String ) of object;
external TChatService = class public constructor Create(chnl: THTTPClientChannel; msg: TJSONMessage; strServiceName: string); function GetServerTime ( szMessage: String; OnGetServerTimeComplete: TOnGetServerTimeCallback; OnGetServerTimeError: TOnErrorCallback): DateTime; function SendMessage ( szMessageJSON: String; OnSendMessageComplete: TOnSendMessageCallback; OnSendMessageError: TOnErrorCallback): Integer; function GetMessages ( nMessagesFrom: Integer; OnGetMessagesComplete: TOnGetMessagesCallback; OnGetMessagesError: TOnErrorCallback): Integer; property fMessage: TMessage read; end;
You can of course type all this in yourself. And you sort of have to, as it is not available automatically, but fortunately I got bored of updating these manually, so I wrote a tool that parses the automatically generated RemObjects Delphi Interface definitions, and outputs the required format for WebBuilder. Download the tool here. It isn’t perfect (it doesn’t like functions with no parameters) but it is a big time save. Put the output into a unit on its own so that you can update it easily with a copy and paste, then include the unit in your main file. For the demo, I put the definitions in uChatInterface.wbs.
Making it work
Okay, all the setup is done, let’s make it work. We created a connection to the Chat interface in FormCreate, so we have a link to the server. When the user clicks the Login button, we actually want to connect to a different interface (“service”), to get that session created. No problem, we just use the same connection but to a different interface.
if m_xLoginSrvc = nil then m_xLoginSrvc := TLogin.Create(FChannel, FMessage, 'Login'); m_xLoginSrvc.Login(szEmail, szPassword, OnServerLoginComplete, OnLinkError);
The last line is where the work is done, and where we came in – a simple function call to make talking to the server look really easy. The only difference is that the code here keeps running, and the user can continue to interact with the application in the browser. When the call to the server completes, the OnServerLoginComplete function is called, and we can then take appropriate action if we logged in, or not. If we connected okay, the sample shows the Chat form. If the link gets broken, it hides it so the user can log in again. The sample also has code to handle reconnection where the server session is lost, and it can log in again and then retry the request. Sending a message to the chat server is pretty much the same.
To get the recent messages, the code uses a timer. This is fairly simple, but it has a few idioms that are worth noting. First, it doesn’t do anything if the form isn’t visible, so that a disconnection and hide of form stops it working. More importantly, it has a form member variable to remember when a call has been made. Because calls are asynchronous, and the internet can be slow, it is possible that the timer will trigger before you get the response. While you can have multiple calls all happening at the same time, you have to take care that they won’t break things by messing up the state. So my code will make one call of a type and await the response before doing anything else. But remember to clear this variable if the link breaks, otherwise you’ll not be able to restart.
procedure TfrmChat.Timer1Timer(Sender: TObject); begin if not Visible then exit; if m_bFetching then exit; m_bFetching := True; frmLogin.GetMessages(m_nCurrentMessage, OnGetMessagesComplete); end;
So the timer kicks off the request, and when a response comes back, it is processed by adding the message to the listbox.
procedure TfrmChat.OnGetMessagesComplete(nResult : Integer; nTopMessage: Integer; szMessageJSON: String ); begin m_bFetching := false; if nResult = 1 then begin m_nCurrentMessage := nTopMessage; stMessages.Items.Add(szMessageJSON); end; end;
One further point worth making – you need to try to keep your data transfers as small as possible. That’s why this sample remembers the last message index, and passes that in to the server each time. If nothing else has been added, the message string is empty, and the result is zero. If this was a real application, I would probably make the messages JSON so that more than one can be passed at a time, and have user names etc. Transmitting bulk updates is also more efficient.
If you have any questions, I hang out on the Elevate WebBuilder newsgroups. I hope though that the full demo project should answer what you need to know. I have included a “just run it” executable that will show it in operation.