Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
C++Builder 2010 and DataSnap
A couple of years ago, I wrote a series of articles for the C++Builder Developer's Journal about C++Builder and Database Development, where I left you with part 7 of the BCB Database Development series.
In that article, I demonstrated how to use DataSnap in C++Builder 2007 on Windows Vista to build multi-tier database applications.In the years that followed, DataSnap has undergone a significant overhaul.No longer based on COM, the new DataSnap multi-tier architecture is more lightweight, but also feels like “not yet finished” in all places.Unfortunately, C++Builder support is one of the areas which is a lacking compared to the Delphi support for the new DataSnap.Delphi 2010 includes two DataSnap Wizards to produce different kinds of projects, for example, with all components and properties already connected and ready to go.With C++Builder we still have to do all that manually, and some things will take more efforts than others.
In this article I will take you all the way, from start to deployment, and even to server methods which out-of-the-box are not possible in C++ for C++Builder, but with a little help from Delphi (or a C++Builder helper class) we can still get them as well.Anyway, more than enough to cover lots of time and pages, so let’s get started.
C++Builder 2010 Enterprise
You will need the Enterprise of higher edition of C++Builder to be able to participate.
Since there are no special DataSnap wizards in C++Builder, we need to start with a new VCL Forms Application (you can also start with a Windows Service Application, but the normal VCL Forms Application is the easiest one to demonstrate and debug) When saving the project, I save the form (UnitX) in file ServerContainer.cpp.This will be the place to place the DataSnap server communication components.The project itself is saved in DS2010BCBServer.cbproj in my case.
I’ve set the Name property of the main form to DSForm, and the Caption to “C++Builder DataSnap 2010 Server Container”, so we know what’s going on when we see this form running.
The DataSnap Server category on the Tool Palette contains the new DataSnap components that we need to use to produce a DataSnap server application (see Figure 1).
Two components are always needed, for any and all DataSnap Server applications: the TDSServer and TDSServerClass components.
TDSServer
First, place a TDSServer component on the main form.
The TDSServer is the actual “engine” of the DataSnap Server, and we can start, pause, or stop this engine using the corresponding commands.By default, the TDSServer has the AutoStart property set to True, to automatically start the engine when the application itself starts.There is also a property HideDSAdmin which is set to False by default, but can be set to True to hide some of the methods that the DataSnap Server exposes that are not of use for the average DataSnap Server application (if you leave this property set to True, then you’ll see the methods later when we connect the DataSnap client to the server).
The TDSServer class has five events that we can hook into, to trace what’s going on with the server.The events are OnConnect, OnDisconnect, OnError, OnPrepare and OnTrace.The OnTrace gives us interesting details on the commands and data being sent back and forth between the clients and the server.Even if you do not want to trace the communications, you should at least write an event handler for the OnError event, to ensure that the server will be notified (or at least someone will be notified) of an error situation.The TDSErrorEventObject has a property Error with the exception that was causing the error, so the least we can do is present that: information.
void __fastcall TDSForm::DSServer1Error(TDSErrorEventObject *DSErrorEventObject) { ShowMessage(DSErrorEventObject->Error->Message); }
Apart from the Error property, the DSErrorEventObject also has properties for the DbxContext, the Transport, the Server and the DbxConnection to inspect when needed to get more details on the error.
TDSServerClass
Now, place a TDSServerClass component next to it.
Assign the Server property of the TDSServerClass to the TDSServer component.
The TDSServerClass is the component that tells the DataSnap Server which class type to “service”, and how to service this type.The “how” is determined by the LifeCycle property, which by default is set to Session.The alternative values are Invocation and Server.This LifeCycle property defines how long an instance of the designated class will “live”.With LifeCycle set to Server, we will get a single instance of the server class, which is used to handle all incoming requests.This means that the class should be thread safe, not use class fields, but only local variables inside methods to avoid threading issues.We can use an object like this to count the number of incoming requests (hits) for example.
The other extreme is the LifeCycle value of Invocation, which means that each incoming requests will result in a new instance of the DataSnap server class, which handles the request and then gets killed again.Truly stateless, since the server class will never know anything from a previous request.
The default setting of LifeCycle to Session means that all incoming requests from a single source (a Session) will get a single instance of the DataSnap server class.This instance will handle all requests from that source, so it’s possible to maintain state (i.e.to ask for “the next 10 records”, since the server will remember the previous position).The default setting of Session results in the easiest way to build DataSnap servers and clients, but not in the most scalable architecture.A few thousand concurrent requests in a timespan of only one minute will be tough to handle by any DataSnap server if LifeCycle is set to Session (but a lesser problem for LifeCycle set to Server or Invocation).
We must use the OnGetClass event hander of the TDSServerClass to specify which class to use as DataSnap server class.This should be a data module if we want to expose TDataSetProvider components – which usually is the case.So, let’s add a data module first, and then get back to the form and implement the OnGetClass event handler.
DataSnap Data Module
To add a data module, do File | New - Other (or right-click on the DS2010BCBServer project and select Add New | Other), and in the Object Repository go to the C++Builder Files category and pick the Data Module.
I’ve set the name of the new data module to DSDataModule, and saved it in file DSDataMod.cpp.Make sure to remove the data module from the list of auto-created forms, by removing the DSDataModule from the list of Auto-create forms in the Project Options.An instance will be created dynamically at run-time, based on the settings of the TDSServerClass’ LifeCycle value, remember?
We can now add our data access components on the data module, exposing them using TDataSetProvider components.
For our example, place a TSQLConnection, TSQLDataSet and TDataSetProvider on the data module.We can use the TSQLConnection component to connect to a database using dbExpress, for example to a SQL Server database (I leave it up to the reader to connect to a database here) like the Northwind example database.
Make sure to connect the SQLConnection property of the TSQLDataSet to the TSQLConnection component.Then, we can set the CommandType to ctQuery, ctTable or ctStoredProc and specify a SQL command, a tablename or a stored procedure name in the CommandText property.Using the Northwind database, I’ve used a ctQuery with CommandText as follows:
select "EmployeeID", "FirstName", "LastName", "City", "Country" from "Employees"
The TDataSetProvider has a DataSet property that needs to point to the dataset that we want to export to the clients, in this case TSQLDataSet1.The name of the TDataSetProvider is important, since this is a name that the developers at the client side will see.And TDataSetProvider1 doesn’t mean a lot, so we should change that to a more sensible name like dspEmployees.Which results in the following data module at design-time:
DataSnap Server Module
So far so good.
Unfortunately, this is still “only” a data module, and for DataSnap 2010 we actually need something else: a TDSServerModule.The New Server Module wizard, however, is also not available for C++Builder developers, which is why we had to start with a TDataModule instead.
In order to transform the TDataModule into the required type, we need to make a few changes by hand in DSDataMod.h.First of all, add a line with
#include <DSServer.hpp>
Then, change the ancestor class of TDSDataModule from TDataModule to TDSServerModule:
class TDSDataModule : public TDSServerModule // was: TDataModule
Then, switch to the DSDataMod.cpp file, and make one modification to the source code: in the constructor, call TDSServerModule(Owner) instead of TDataModule(Owner), as follows:
__fastcall TDSDataModule::TDSDataModule(TComponent* Owner) : TDSServerModule(Owner) // was: TDataModule(Owner)
This will ensure that our TDSDataModule class is derived from TDSServerModule and not from TDataModule.
OnGetClass
Now we can go back to the server container (main) form in ServerContainer.cpp.
We need to make sure DSDataMod is used, to press Alt+F11 and add DSDataMod.cpp to the Header of the ServerContainer unit.
We can now implement the OnGetClass event of the TDSServerClass component as follows:
void __fastcall TDSForm::DSServerClass1GetClass( TDSServerClass *DSServerClass, TPersistentClass &PersistentClass) { PersistentClass = __classid(TDSDataModule); }
This will make sure that the TDSDataModule class type is used as our persistent class.
Transport
With the TDSServer and TDSServerClass components in place, it’s time to add the transport layer.
DataSnap 2010 offers two choices here: HTTP or TCP/IP.
The HTTP transport protocol is implemented by the TDSHTTPService component.This component has a HttpPort property, by default set to port 80, that you may need to change (especially if you have a web server already running on your development machine).Apart from that, the TDSHTTPService component must explicitly be “started” (and stopped) using the Active property.You may encounter some funny behavior if you set Active to True at design-time and then also run the application (which results in two instances listening to the same port) – another good reason not to set this property to Active at design-time, but to use code for that, for example in the OnCreate event handler of the form.
The TDSHTTPService component also has a helpful AuthenticationManager property that we can connect to the TDSHTTPServiceAuthenticationManager component.This will enforce the use of HTTP Authentication (which can be verified in the OnHttpAuthenticate event handler of the TDSHTTPServiceAuthenticationManager component Warning: since the protocol itself is still HTTP, the HTTP Authentication information (username and password) are being sent in plain text, so anyone with a packet sniffer can read them.It would be better to use the HTTPS protocol in combination with HTTP Authentication, but unfortunately, DataSnap 2010 does not support HTTPS just yet (see QC #81335 for more details on this missing feature).
Apart from HTTP, DataSnap also support TCP/IP, and this is implemented by the TDSTCPServerTransport component.This one does not come with built-in authentication, but we can always implement the OnConnect event handler of the TDSServer component and check the values of the DSAuthenticationUser and DSAuthenticationPassword ConnectProperties values manually (the same property values are used for the automatic HTTP Authentication check), but that’s a story for another day.
To continue our demo, place a TDSTCPServerTransport component on the form, and point the Server property to the TDSServer component.We can configure this component, specifically we should probably change the default port by modifying the Port property from the default 211 and use another value.Make sure to remember that value, since the client needs to specify the new port as well, of course.
Compile and run the DS2010BCBServer project (communicating using TCP/IP over the default port 211)
Now you can build DataSnap 2010 clients, connecting using TCP/IP over port 211, using DSProviderConnection to get to the DataSetProvider exported from the DSDataModule.
DataSnap Client
We can now add a DataSnap Client application, typically by adding it to the same project group.
Right-click on the project group and do Add | New Project - VCL Forms Application.I’ve saved the form in MainForm.cpp and the project itself in DSClient.cbproj.
In order to connect to the (running!) DataSnap server, we should place a TSQLConnection component on the form, set the Driver property to DataSnap and the driver subproperties should by default be set to communicate using TCP/IP over port 211.Note that we may need to change that port number if we modified it at the server side.Apart from that, all we need is to set the LoginPrompt property to False and the Connected property to True to see if we can connect to the server.
When this is verified, we can add another component, this time from the DataSnap Client category on the Tool Palette (a category which mainly hosts older DataSnap Client components, by the way).We need to place a TDSProviderConnection component on the form, and connect its SQLConnection property to the TSQLConnection component.Next we need to specify the value for the ServerClassName.This was the type name of the data module at the server side – the type that was used for our Persistent Class as assigned in the OnGetClass event of the TDSServerClass, remember? The actual type is TDSDataModule, and we need to enter that as value for the ServerClassName property.It would have been nice to show a drop-down list of all exported server class names, but alas that’s not the case.
The next step involves placing a TClientDataSet, connecting its RemoteServer property to the TDSProviderConnection component.Then, if all previous connections were made correctly (and the ServerClassName property of the TDSProviderConnection component is also correct), we can open up the ProviderName property of the TClientDataSet, and this will show a list of exported names of TDataSetProvider components from the server module.In this case, we should see only dspEmployees.If you do not see the choice dspEmployees, you need to verify the previous steps for the DataSnap Client, and also make sure the DataSnap Server itself is running (and not blocked by the firewall).
Finally, we can add a TDataSource, TDBGrid, TDBNavigator and other data-aware controls you want to use to display the data from the DataSnap Server in the thin (or smart) client.If we set the Active property of the TClientDataSet to True, we will see live data at design-time in the DataSnap Client.
A little warning: make sure to set the Active property of the TClientDataSet back to False when you save and close the project. And also ensure the Connected property of the TSQLConnection component is set to False.The reason will become obvious once you open the project group again: by default the main form of the last project will be shown, but if these components are “active”, then they will try to make a connection to the DataSnap Server.Which is the first project in the group, but usually not running when you just open the project group.So this will lead to a certain period in which C++Builder may be frozen after you get a connection timeout.To avoid that problem, I always ensure that Active and Connected are set to False before saving and closing the project.
DataSnap Server Methods
Apart from exporting TDataSetProviders from the server side to the clients, DataSnap 2010 also offers the support of so-called server methods.
These are methods that we can define as public functions in the TDSServerMethods class, and who will be exported to the outside world as well.In theory that is, since this doesn’t work quite out-of-the-box for C++Builder.The problem is that this exporting of server methods relies on so-called RTTI Method Information, which is currently only supported for Delphi with a {$METHODINFO ON} compiler directive.
There are two workarounds for this problem.In this article, I will show you one, which is based on the fact that C++Builder can compile Delphi code.But it’s not a pure C++ solution.If you want the pure C++Builder solution, without Delphi code, you may want to check out Hanno Nagland’s C++Builder helper functions and macros at http://cc.embarcadero.com/item/27643
Delphi Server Methods
Like I said, I will stick to showing you how to use C++Builder to compile Delphi code (sorry).
For that, we need a .pas and .dfm file, which you may not be able to create if you have C++Builder 2010 without the full RAD Studio 2010 (which also includes the Delphi personality).Assuming you only have C++Builder, you need to create a file ServerModule.dfm with the following contents:
object BCBServerModule: TBCBServerModule OldCreateOrder = False Height = 480 Width = 640 end
And a file ServerModule.pas with the following lines of Delphi code:
unit ServerModule; interface uses DSServer; type TBCBServerModule = class(TDSServerModule) private { Private declarations } public { Public declarations } function EchoString(const S: String): String; end; implementation {$R *.dfm} { TBCBServerModule } function TBCBServerModule.EchoString(const S: String): String; begin Result := S; end; end.
This is just an empty Server Module, but we can copy the TSQLConnection, TSQLDataSet and TDataSetProvider from our TDSDataMod to the TDSServerModule if you want.
Adding server methods can now be done in the Delphi unit, using Delphi syntax.As an example, the above unit already includes the EchoString function.You can add more public server methods, and the {$METHODINFO ON} – define for the parent class – will ensure that these server methods will be exported from the DataSnap server.
In both cases, we should ensure that the TDSServerClass component points to the right server module type.In my Delphi server module example, the OnGetClass event handler should be changed as follows:
void __fastcall TDSForm::DSServerClass1GetClass(TDSServerClass *DSServerClass, TPersistentClass &PersistentClass) { //PersistentClass = __classid(TDSDataModule); PersistentClass = __classid(TBCBServerModule); // Delphi }
This will ensure that we use the TBCBServerModule that exposes the EchoString function with RTTI method info attached.
Feel free to add any other custom additional server methods, but remember they have to be implemented in Delphi (unless you use the C++Builder helper functions and macros from Hanno Nagland).
Server Methods Client (in C++)
Apart from defining server methods, we should also be able to call them.
Fortunately, this is where we can use pure C++ again.
Assuming you have exported DataSnap server methods – either using the Delphi syntax, or by using the C++Builder help functions and macros from Hanno Nagland – we can return to the DataSnap client project in C++Builder.Make sure the DataSnap server is running, and then right-click on the TSQLConnection component on the DataSnap client form.Select the option “Generate DataSnap client classes”, as shown in the following screenshot.
The result is a generated C++Builder proxy class of the server methods, with the following definition of the TBCBServerModuleClient class.
class TBCBServerModuleClient : public TObject { private: TDBXConnection *FDBXConnection; bool FInstanceOwner; TDBXCommand *FEchoStringCommand; public: __fastcall TBCBServerModuleClient::TBCBServerModuleClient(TDBXConnection *ADBXConnection); __fastcall TBCBServerModuleClient::TBCBServerModuleClient(TDBXConnection *ADBXConnection, bool AInstanceOwner); __fastcall TBCBServerModuleClient::~TBCBServerModuleClient(); System::UnicodeString __fastcall EchoString(System::UnicodeString S); };
Two constructors, one destructor and our single server method called EchoString.We can call the constructor by passing the DBXConnection property of the TSQLConnection component as argument, and then we can call the EchoString method.In pure C++Builder code, this would be as follows:
void __fastcall TForm1::btnEchoStringClick(TObject *Sender) { SQLConnection1->Open(); TBCBServerModuleClient* WS = new TBCBServerModuleClient(SQLConnection1->DBXConnection); ShowMessage(WS->EchoString("Called from C++ Client")); SQLConnection1->Close(); }
And although the server side it not perfect yet (out-of-the-box), I can only encourage people to vote for the C++Builder feature requests in Quality Central such as QC #83542 (for the missing DataSnap Wizards).
DataSnap Deployment
The “old” DataSnap servers, based on COM, would automatically register themselves in the old days (using C++Builder 6 to 2006).
This protocol was changed with C++Builder 2007, since Windows Vista would complain if an application tried to register itself while not being run “as administrator”.
With DataSnap 2010, there is no more COM, and no need (or way) to register the location of the DataSnap server.An incoming client connection can no longer automatically cause the server to be started.In other words: when deploying a DataSnap server application, we should ensure that the server application is always up-and-running to allow the client(s) to connect to it.A regular VCL Forms application may not be the best server type, which is why you may want to turn your DataSnap server into a Windows Service Application, or into a WebBroker (ISAPI) project.If you do the latter, then you can use the web module to place your DataSnap server components (that are currently residing on the main form).Both a Windows Service and a Web Server application will be available without the need for someone to logon to the server machine (and starting the DataSnap server application).
Summary
In this article, I’ve shown how we can use C++Builder 2010 to build DataSnap 2010 server and client applications.
Although some support is still missing – like the DataSnap Wizards and direct C++Builder support for server methods – we can get functional servers and clients.
Fortunately, Embarcadero has added the missing functionality of DataSnap 2010 in the next release: C++Builder XE.