You May Not Need Distributed Apps, But You Do Need Midas
by Bill Todd
Borland developed Midas as a tool for creating multi-tier
distributed applications. But Midas is the best way to build any
database application, particularly large applications, even when
you do not need a distributed application.
Improving Transactions in Desktop Databases
Consider desktop databases. If you are working with Paradox or
Dbase tables and you need transaction support you are limited
because the only transaction isolation level is read uncommitted
(also called dirty read). On top of that, transactions do not
rollback if a crash occurs which means that a crash can leave your
database in an inconsistent state. However, if you use
ClientDataSets you effectively get serializable transaction
isolation and automatic rollback if a crash occurs.
Since ClientDataSets hold a copy of data in memory and work on
that local copy you will not see inserts, deletes or updates made
by other users or other datasets in the same application. As far as
changes to the data go, this gives you serializable transaction
isolation. No matter how many times you scan your data you will
always see the snapshot you started with.
ClientDataSets hold all changes in memory in their Delta
property until you call ApplyUpdates. This means that an
application or workstation crash effectively rolls back your
changes because all of the changes in Delta will be lost. The only
flaw in using a ClientDataSet to simulate transactions is that a
crash while ApplyUpdates is executing can still leave your database
in an inconsistent state. However, If you call ApplyUpdates often
enough to ensure that only a few records are being updated the
update takes only a fraction of a second. This is a much smaller
window of vulnerability than using local transactions where the
database is in an inconsistent state from the time the first change
is posted until the transaction is either committed or rolled back.
For a user manually making multiple changes this can be several
minutes.
Improving Concurrency With Any Database
One problem with transactions on any database is that
transactions that are active for a long time reduce other
user’s ability to update the database. This happens because
each time a changed row is posted to the database it must be locked
and the lock must be held until the transaction completes to ensure
that no other user can change the row. This problem is particularly
onerous if the database uses page level locking since a page lock
makes it impossible for other users to change rows on a locked page
that are not part of the transaction.
Cached updates were originally added to Delphi to overcome this
problem and ClientDataSet does the job even better. If you use
ClientDataSets to edit your data changes are posted to the local
copy of the data in the ClientDataSet’s cache. The database
is not aware that any changes have been made until you call
ApplyUpdates. Since the transaction is active on the server only
while the call to ApplyUpdates is being processed, typically a
fraction of a second, locks are not held for a long time and
concurrency is improved.
Supporting Multiple Backends
Suppose you are writing a vertical market application. You know
that some of your potential customers are already committed to a
database server platform so you need to build versions of your app
that will run on Oracle, Microsoft SQL Server and Interbase. You
are not only faced with supporting different databases but the
components used to access the databases are different. For
Interbase the best choice is Interbase Express, for SQL Server the
best choice is ADO Express and for Oracle you can use either the
BDE or ADO Express.
Midas makes this much easier. For each database build a thin
Midas server application that contains only the database specific
code. All application logic resides in the Midas client
application. This lets you maintain a single code base for the
majority of your application in the Midas client with the Midas
servers providing just the connection to the database. This allows
you to deliver the common Midas client and the Midas server that
matches the database the client is using. Another problem this
solves is the case where you have customized the application for a
particular client and the client then decides to change their
database backend. With the Midas solution all you have to do is
install the Midas server that works with the new backend. No
changes to the customized client are required.
Building Modular Applications
Combining Midas with Microsoft’s Component Object Model
(COM) lets you build large complex applications from multiple COM
servers that share a common database connection. Using Midas and
COM together:
- Makes team development easier to manage by allowing each member
of the team to work on a module that can be compiled and tested
independently.
- Makes applications that consist of many modules, such as an
accounting system, easy to deploy by deploying just those modules
the user needs.
- Allows all modules to share a common database connection.
- Makes modules easily sharable across applications regardless of
the programming language that is used.
- Makes supporting multiple databases easier. Even if you do not
need to support multiple databases now it means you can design your
application so you can change databases more easily in the
future.
The next several sections of this paper will cover building a
simple application that demonstrates using Midas and COM together.
It also shows one way to implement callbacks from
a COM server to its client. To examine using Midas and COM to build
a modular application I will create a very simple example that
consists of a Midas server and two Midas clients. The first Midas
client will be the application’s main form and will display
data from the sample Customer and Order tables. This application is
an EXE. The second Midas client will display data from the Order
table and is implemented as an in-process Automation server. The
Midas server is also implemented as an in-process automation server
DLL. The roles played by the three programs can be confusing. To
clarify who does what the following table shows each application,
the roles it plays and how it is implemented.
|
Project Name
|
Purpose
|
Roles
|
Implemented As
|
|
DemoDllServer
|
Provides connection to database
|
Midas Server
|
ActiveX Library DLL
|
|
DemoClient
|
Contains customer form
|
Midas Client
COM Client
|
EXE
|
|
DemoOrders
|
Contains orders form
|
Midas Client
COM Server
|
ActiveX Library DLL
|
Building the Midas Server
The Midas server has only one unusual feature. It is implemented
as a DLL so it will not display a form or show an icon on the task
bar. While having the server show on the task bar may be acceptable
for a distributed system where no one normally sees the screen of
the machine that hosts the Midas server, it is not a good idea for
an application where the server and client will run on the same PC
because the user may be confused by the extra icon and may try to
close the server. The solution is to implement the Midas server as
a DLL so it has no user interface. Implementing the Midas server as
a DLL also improves performance. To create a Midas server as a DLL
start by choosing File | New from the menu and selecting the
ActiveX page of the Object Repository. Double click the ActiveX
Library icon to create a new ActiveX library project. Since Midas
uses COM to handle communications between the Midas client and the
Midas server an ActiveX library is used to provide the required COM
support.
From here on the process is the same as creating a Midas server
that is an EXE. Select File | New, go to the Multitier page and add
a Remote Data Module to the project. Figure 1 shows the remote data
module for the sample application.
Figure 1 – The remote data module
This application is written in typical client/server style. When
the user opens the application no data is displayed. Instead the
user must enter some selection criteria that will fetch a
reasonable number of records. To implement this approach the SQL
statement for the CustomerQry component is:
select * from customer
where CustNo = -1
This allows the Customer ClientDataSet in the DemoClient
application to be opened immediately without displaying any data
since there is no customer record whose customer number is minus
one. Both a DataSetProvider (CustomerProv) and a DataSource
(CustomerSrc) are connected to the CustomerQry component by setting
their DataSet property to CustomerQry. In the Options property of
the DataSetProvider poAllowCommandText is set to True so the client
application can change the SQL property of CustomerQry to select
different sets of customer records. OrdersQry supplies the order
records for the current customer record. Its SQL property is set
to
select * from orders
where (CustNo = :CustNo)
and its DataSource property is set to CustomerSrc so the :CustNo
parameter’s value will be supplied by the current record in
CustomerQry. This will cause the order records to be stored in the
customer dataset as a nested dataset.
The DemoOrders application allows the user to search the entire
Orders table and select an order by order number or all of the
orders for a customer number. To provide access to all orders a
second Query component, OrdersAllQry, that is not linked to the
CustomerQry is needed. Once again the SQL statement is set to
retrieve no records by selecting all columns from Orders where the
order number is minus one. The DataSetProvider for the OrdersAllQry
also has its poAllowCommandText option set to True. Since this
Midas server is a DLL you cannot register it by running it.
Instead, choose Run | Register ActiveX Server from the Delphi menu
to compile and then register the Midas server.
The Midas server in a typical three tier distributed application
not only provides the connection to the database but may also
provide business rule enforcement or other services to its clients.
However, in this article we are discussing a single application
that consists of multiple modules. All of the modules will be Midas
clients using the same Midas server and both the clients and the
server will run on the same machine. Suppose you are writing a
vertical market application using this architecture. If you need to
support multiple databases you may want to limit the code in the
Midas server to just that code which is specific to a particular
database, such as Oracle or Microsoft SQL Server, and keep all of
the code that is common to all databases in the client modules.
This lets you maintain multiple Midas servers for multiple
databases with no code replication.
Building the COM Client
Figure 2 shows the application’s main form. It consists of
two DBGrids and two DBNavigators. The top grid and navigator
display customer information and the bottom grid and navigator
display order data. Figure 3 shows the data module for this
application.
Figure 2 – The main form
Figure 3 – The main form’s data module
The data module contains a DCOMConnection component, two
ClientDataSets and two DataSources. The DCOMConnection
component’s name is DemoConn and its ServerName property is
set to DemoDllSrvr.DllDemoServer. The RemoteServer property of
CustomerCds is set to DemoConn and its ProviderName property is set
to CustomerProv. The OrdersCds component’s DataSetField
property is set to CustomerCdsOrdersQry so it will derive its data
from the nested dataset in the CustomerCds records. The Edit menu
contains a Find choice that displays the dialog shown in figure 4.
This lets the user select a customer record by customer number or
select all of the records in a specified state using the
FindCustomer method in the data module named CustomerDm. If you are
interested in this code look at the complete sample
application.
Figure 4 – The Find Customer dialog
The File menu on the main form contains an Orders choice that
lets the user open a form that can be used to search for any order
by customer number or order number. The orders grid is connected to
a pop-up menu component that offers the user two choices. The
first, Show This Order, will open the Orders form and show the
current order record. The second, Show All Orders For This
Customer, will open the Orders form and show all of the orders for
the customer number contained in the current order record in the
grid.
Building the COM Server
Now the fun begins. The next step is to create the Orders form
and the methods the Customer form must use to open the Orders form,
find the orders for a customer and find a specific order by its
order number. However, the Orders form is going to be in a separate
application, which is an Automation server and the Customer form
will call the Orders form’s methods through its interface
using Automation.
To create the Orders application, go to the ActiveX page in the
Object Repository and double click ActiveX Library. Add a form and
a data module to the application. The finished form is shown in
figure 5 and the data module in figure 6.
Figure 5 – The Orders form
Figure 6 – The Orders data module
The DCOMConnection component in figure 6, OrdersConn, connects
to the Midas server, DemoDllSrvr.DllDemoServer, just as the
DCOMConnection component in the Customer data module did. The
RemoteServer property of OrdersCds is set to OrdersConn and
the ProviderName is set to OrdersAllProv.
The next step is to turn this DLL into an Automation server.
Return to the ActiveX Page of the Object Repository, double click
the Automation Object wizard, and enter OrdersServer for the
CoClass name. Also check the Generate Event Support Code checkbox.
When the Type Library Editor appears add the following methods to
the IOrderServer interface then click the Refresh button. If you
want to see captions under the Type Library Editor toolbar buttons
right click the toolbar.
| Method |
Param |
Type |
|
FindByOrderNo
|
OrderNo
|
long
|
|
FindByCustNo
|
CustNo
|
long
|
|
OpenOrdersForm
|
|
|
|
CloseOrders
|
|
|
|
FindCustomer
|
|
|
|
GetCustNo
|
CustNo
|
Variant *
|
The code for the first three methods is straightforward and is
found in the OrdersAuto unit and shown here in figure 7.
procedure TOrderServer.FindByOrderNo(OrderNo: Integer);
begin
OrderDm.FindByOrderNo(OrderNo);
end;
procedure TOrderServer.FindByCustNo(CustNo: Integer);
begin
OrderDm.FindByCustNo(CustNo);
end;
procedure TOrderServer.OpenOrdersForm;
begin
OrderDm := TOrderDm.Create(nil);
OrderForm := TOrderForm.Create(nil);
OrderForm.Show;
end;
Figure 7 – The FindByOrderNo, FindByCustNo and
OpenOrdersForm methods
The first two methods, FindByOrderNo and FindByCustNo call the
methods with the same name in the orders data module. The
implementation section of the orders data module is shown in figure
8. Both methods close the orders ClientDataSet, assign a new SQL
statement to its command text property and then re-open the
ClientDataSet. When the ClientDataSet is opened the value of
CommandText is passed to the Midas server and assigned to the SQL
property of the OrdersAllQry component before the query is opened.
The customer EXE program calls these methods to display a
particular order or the orders for a specific customer in the
Orders form. The third method, OpenOrdersForm, creates the data
module, OrderDm, and the OrdersForm and shows the orders form. The
customer EXE program calls this method to make the Orders form
visible.
implementation
uses FindOrderF;
{$R *.DFM}
procedure TOrderDm.FindOrder;
{Displays the Find Order dialog. Calls the appropriate find method
based on which edit box on the Find Order dialog has a value.}
begin
FindOrderForm := TFindOrderForm.Create(Self);
try
with FindOrderForm do
begin
ShowModal;
if OrderNoEdit.Text <> '' then
FindByOrderNo(StrToInt(OrderNoEdit.Text))
else if CustNoEdit.Text <> '' then
FindByCustNo(StrToInt(CustNoEdit.Text))
else
MessageDlg('You must enter an order number or customer number.',
mtError, [mbOK], 0);
end; //with
finally
FindOrderForm.Free;
end; //try
end;
procedure TOrderDm.FindByOrderNo(OrderNo: Integer);
{Finds an Order record given its OrderNo.}
begin
with OrdersCds do
begin
Close;
CommandText := 'SELECT * FROM Orders WHERE ' +
'(OrderNo = ' + IntToStr(OrderNo) + ')';
Open;
end;
end;
procedure TOrderDm.FindByCustNo(CustNo: Integer);
{Finds all of the Order records for the specified Customer.}
begin
with OrdersCds do
begin
Close;
CommandText := 'SELECT * FROM Orders WHERE ' +
'(CustNo = ' + IntToStr(CustNo) + ')';
Open;
end;
end;
Figure 8 – The OrdersDm methods
The FindOrder method of the orders data module is called from
the Edit menu of the Orders form. It displays the FindOrdersForm
dialog box that lets the user find one or more orders by order
number or customer number.
Calling Back to the COM Client
With the methods described so far the COM client application
that displays the customer form can call methods in the COM server
to open the orders form and find orders by order number or customer
number. However, the COM server needs to be able to call back to
the client for two reasons. First, when a user is viewing an order
the user needs to be able to display the customer record for that
order. Put another way, the orders form must be able to tell the
customer form to find a specific customer record and show itself.
The second problem is that the COM server application shows the
orders form modelessly. That means that the COM client has no way
to know when it can close the COM server. The only solution is that
the COM server must notify the COM client when the user closes the
orders form.
There are three ways for the server to communicate with the
client. The first is to add an Automation object to the client
application so the server can connect to the client and call
methods of the automation object’s interface. Doing this
means that the application that contains the customer form is both
a COM client of and a COM server to the orders application DLL and
the orders DLL is both a client of and server to the customer
application.
The second method involves creating a callback interface to the
COM client application. To do this you must add an interface to the
client and create an object that implements the interface. When the
COM client connects to the COM server it must create an instance of
the callback object then call a method of the COM server and pass
the interface reference, as a parameter, to the COM server. Using
this interface reference the server can call methods on the
client.
The third technique is to let the server fire events on the
client through the server’s dispinterface. This is the
easiest to implement in Delphi 5, thanks to wizards that do most of
the work. Although this technique has some limitations it will
suffice for most applications so it is the method used in this
paper. The key to using callback events was to check the Generate
Event Support Code checkbox when adding the Automation Object to
the COM server. This causes two interfaces to be added to the COM
server’s type library. We have already added methods to the
first interface, IOrderServer. The second interface is a dispatch
interface named IOrderServerEvents. It is now time to open the Type
Library Editor again and add two methods to
the IorderServerEvents interface. The first is named
OnCloseOrders and the second is named OnFindCustomer.
After adding the OnFindCustomer event click
the Parameters tab then click the Add button to add a new
Parameter. Name the parameter CustNo
and leave its type set to Long.
The OnCloseOrders event will be
fired when the user closes the Orders form to notify the COM client
that it can close its connection to the orders COM server. The
OnFindCustomer event will fire when the user selects View |
Customer from the menu. This event will notify the COM client that
it should find and display the customer record whose customer
number matches the customer number of the current order record.
The code in figure 9 fires the events. CloseOrders and
FindCustomer are methods that were added to the IOrderServer
interface earlier. CloseOrders is called from the OnDestroy event
handler of the Orders form. FindCustomer is called from the OnClick
event handler of the View | Customer menu item.
procedure TOrderServer.CloseOrders;
begin
FEvents.OnCloseOrders;
end;
procedure TOrderServer.FindCustomer;
begin
FEvents.OnFindCustomer(OrderDm.OrdersCdsCustNo.AsInteger);
end;
Figure 9 – Firing the dispinterface events
To call these methods you must have a reference to the
OrderServer Automation object. To get this reference two changes,
shown in figures 10 and 11 are made to the OrdersAuto unit. First,
a global variable, OrderServer, is added to the interface section
of the unit. Next, a line is added to the TOrderServer
object’s Initialize method to assign Self to the OrderServer
global variable. The OrderServer variable now provides a reference
to the OrderServer Automation object that can be used to call its
methods from the Orders form’s OnDestroy event handler and
the menu item’s OnClick event handler or from anywhere else
in the DemoOrders application. Note that if you just want to fire
an event from a method in the IOrderServer interface you can omit
these two steps. We needed a reference to the Automation object
only because we needed to fire the events from elsewhere in the
application.
var
OrderServer: TOrderServer
Figure 10 – The Automation object reference variable
declaration
procedure TOrderServer.Initialize;
begin
inherited Initialize;
FConnectionPoints := TConnectionPoints.Create(Self);
if AutoFactory.EventTypeInfo <> nil then
FConnectionPoint := FConnectionPoints.CreateConnectionPoint(
AutoFactory.EventIID, ckSingle, EventConnect)
else FConnectionPoint := nil;
OrderServer := Self;
end;
Figure 11 – The OrderServer reference variable is
initialized
The last step is to implement the events in the COM client. With
the DemoClient project open in the IDE, select Project | Import
Type Library from the menu to display the Import Type Library
dialog shown in figure 12. Select DemoOrders Library in the list
box and make sure that Generate Component Wrapper is checked. This
will create a component, of type TOrderServer, and add it to your
component palette. When you click the Install button you will be
asked if you want to install this component in a new package or an
existing package. You will probably find it more convenient to put
all of the server components for the project you are working on in
their own package. Whatever you do, do not install this component
in one of the existing Delphi component packages. Once you have
selected a package click OK then Yes to the dialog informing you
that the package will be built then installed. The component that
is created is a wrapper around the COM server and can be used to
connect to the server and call its methods. The OrderServer
component also has an event for each event you added to the
IOrderServerEvents interface in the COM server.
Figure 12 – the Import Type Library Dialog box
Drop an instance of the TOrderServer componet on the Customer
form and name it OrderServer. Set its AutoConnect property to False
so the connection to the COM server will not be opened
automatically when the program starts. Switch to the Events page of
the Object Inspector and create event handlers for the
OnCloseOrders and OnFindCustomer events. The code for both event
handlers is shown in figure 13.
procedure TCustomerForm.OrderServerCloseOrders(Sender: TObject);
begin
OrderServer.Disconnect;
end;
procedure TCustomerForm.OrderServerFindCustomer(Sender: TObject;
CustNo: Integer);
begin
CustomerDm.FindByCustNo(CustNo);
Show;
end;
Figure 13 – The OnCloseOrders and OnFindCustomer event
handlers
All that remains is to implement the OnClick event handlers for
the File | Orders menu choice and the Order grid’s pop-up
menu. The code for these event handlers is shown in figure 14.
procedure TCustomerForm.Orders1Click(Sender: TObject);
begin
OrderServer.Connect;
OrderServer.OpenOrdersForm;
end;
procedure TCustomerForm.ShowThisOrder1Click(Sender: TObject);
begin
with OrderServer do
begin
Connect;
OpenOrdersForm;
FindByOrderNo(CustomerDm.OrdersCds.FieldByName('OrderNo').AsInteger);
end; //with
end;
procedure TCustomerForm.ShowAllOrdersForThisCustomer1Click(
Sender: TObject);
begin
with OrderServer do
begin
Connect;
OpenOrdersForm;
FindByCustNo(CustomerDm.OrdersCds.FieldByName('CustNo').AsInteger);
end; //with
end;
Figure 14 – The menu item event handlers
Moving Data Between Server and Client
What do you do when you need to move data that is not stored in
a database table between a COM server and a COM client? Stuff it in
a variant and pass it as a parameter. Notice that I am no longer
talking about a Midas server and client but any COM server and
client. While some of the techniques in this section will be
demonstrated with a Midas server and client using the IAppServer
interface they will work equally well between any COM server and
client using any interface that you can add methods to.
Passing Tabular Data
If you need to pass tabular data the easiest thing to do is
stick it in a ClientDataSet and pass that as demonstrated in the
PassData sample application that accompanies this paper. This app
consists of a COM server and a COM client. The client’s main
form, shown in figure 15, contains a Database, Query,
DataSetProvider, ClientDataSet and DataSource connected to the
DBGrid to display the data in the DBDEMOS sample customer table.
The Send Data buttons OnClick event handler is shown in figure
16.
Figure 15 – the COM client’s main form
procedure TMainForm.SendBtnClick(Sender: TObject);
begin
PassDataServer := CoPassData.Create;
PassDataServer.PassData(CustCds.Data);
end;
Figure 16 – the Send Data button’s OnClick event
handler
The client application uses the server’s type library
interface unit so it can connect to the server by calling the
server’s coclass’s Create method and assigning the
interface reference to the variable PassDataServer. PassDataServer
is declared as a private member variable of the form and its type
is IPassData. IPassData is the interface implemented by the COM
server. The second line calls the PassData method of the IPassData
interface and passes the ClientDataSet’s Data property as a
parameter.
Figure 17 shows the server’s PassData method. This method
takes a single parameter of type OleVariant that is used to pass
the ClientDataSet’s Data property from the client to the
server. The server application’s main form contains a
ClientDataSet, DataSource and a DBGrid. The code in figure 17
assigns the CdsData parameter to the ClientDataSet’s Data
property and opens the ClientDataSet causing the data that was
passed from the client to appear in the grid on the server’s
form.
procedure TPassData.PassData(CdsData: OleVariant);
begin
with MainForm.CustCds do
begin
Data := CdsData;
Open;
end; // with
end;
Figure 17 – the PassData method
If you need to pass the changes that have been made to the
sending ClientDataSet’s data, which are contained in the
ClientDataSet’s Delta property, just add another OleVariant
parameter and assign Delta to it. Unfortunately, Delta is a
read only property so you cannot assign the Delta parameter to the
Delta property of the receiving ClientDataSet. Note that the
ClientDataSet in the server is not connected to a remote server or
provider in this example but it could be.
Passing Flat File Data
One of the neat things about Midas is that the data that the
Midas server sends to the client can come from anywhere. It does
not have to be stored in a database table. One of the techniques in
the PassOther sample application supplies data to the Midas client
from a comma delimited ASCII file. The easiest way to do this is to
drop a ClientDataSet and DataSetProvider on the server’s
remote data module. Use the Object Inspector to edit the
ClientDataSet’s FieldDefs property and add the field
definitions you need for your data. Next write a BeforeGetRecords
event handler for the DataSetProvider which gets the data, in this
case from the ASCII file, and loads it into the ClientDataSet. The
DataSetProvider then gets the data from the ClientDataSet and sends
it to the client application in the normal way. Figure 18 shows the
BeforeGetRecords event handler.
procedure TPassOther.TextProvBeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
var
AFile: TextFile;
FieldVals: TStringList;
Rec: String;
begin
FieldVals := TStringList.Create;
try
with TextCds do
begin
{If the ClientDataSet is active empty it otherwise create it using the
FildDefs entered at design time. Calling CreateDataSet both creates the
in memory dataset and opens the ClientDataSet.}
if Active then
EmptyDataSet
else
CreateDataSet;
{Open the ASCII file.}
AssignFile(AFile, OwnerData);
Reset(AFile);
{Loop through the ASCII file. Read each record and assign it to the
CommaText property of the TStringList FieldVals. This parses the
record and assigns each field to a string in the StringList. Insert a
new record in the ClientDataSet and assign the StringList elements to
the fields.}
while not System.EOF(AFile) do
begin
Readln(AFile, Rec);
FieldVals.Clear;
FieldVals.CommaText := Rec;
Insert;
FieldByName('Name').AsString := FieldVals[0];
FieldByName('Date').AsDateTime := StrToDate(FieldVals[1]);
FieldByName('Unit').AsString := FieldVals[2];
Post;
end; //while
System.CloseFile(AFile);
{Be sure to reposition the ClientDataSet to the first record so the
DataSetProvider will start with the first record when building its
data packet to send to the client.}
First;
end; //with
finally
FieldVals.Free;
end; //try
end;
Figure 18 – the server’s BeforeGetRecords event
handler
The BeforeGetRecords event handler starts by creating a
StringList named FieldVals that is used to parse the records from
the comma delimited ASCII file. Next it checks to see if the
ClientDataSet is active and if so empties it. If not, it calls
CreateDataSet which both creates the in memory dataset using the
FieldDefs supplied at design time and opens the ClientDataSet. The
AssignFile and Reset calls open the ASCII file. Notice that the
name of the file in the call to AssignFile is the OwnerData
parameter passed to the event handler. OwnerData is provided so the
client can pass any information it wants to the server by setting
the value of OwnerData parameter in the client application’s
ClientDataSet’s BeforeGetRecords event. Since OwnerData is a
variant you can pass any type of data including a variant array of
variants. This gives you the ability to pass as many values of any
type as you wish.
The While loop reads a record from the text file into the string
variable Rec, clears the StringList, and assigns Rec to the
StringList’s CommaText property. When you assign a string to
CommaText it is parsed on any commas or spaces which are not
enclosed in quotation marks and each substring is assigned to an
element of the StringList. Next, a new record is inserted into the
ClientDataset and the values from the StringList are assigned to
the fields. Finally the new record is posted. Once the end of the
text file is reached a call to CloseFile closes the ASCII file.
Next, a call to First moves the ClientDataSet’s cursor to
the first record. This is critical because the DataSetProvider will
start with the current record when it builds the data packet to
send to the client. If you leave the ClientDataSet positioned to
the last record the last record is the only one that will be sent
to the Midas client. Finally, a call to the StringList’s Free
method releases its memory.
On the client side things are even easier. When you open the
ClientDataSet in the Midas client application its BeforeGetRecords
event fires. Figure 19 shows the code for the client’s
BeforeGetRecords event.
procedure TMainDm.TextCdsBeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
{Assign the file name to OwnerData which is passed to the Midas client
automatically.}
OwnerData := ExtractFilePath(Application.ExeName) + 'text.txt';
end;
Figure 19 – the client’s BeforeGetRecords event
handler
The only thing that happens here is that the name of the text
file is assigned to the OwnerData parameter. OwnerData is
automatically sent to the Midas server where, as you have seen, it
appears as a parameter to the DataSetProvider’s
BeforeGetRecords event.
Sending a File You Do Not Want to Display
Using ClientDataSet’s is great for data you want to
display on a form. But suppose you need to send a file from a COM
server to its client that you do not want to display in a
ClientDataSet. Its quite easy even if you need to send a file that
is too large to fit in memory. The File tab of the sample
application contains a Copy File button and Memo component. Figure
20 is the code from the Copy File Button’s OnClick event
handler. This procedure begins by declaring a constant, ArraySize,
which determines the size of the variant array used to transfer the
file from the COM server to the client. This sample program
displays the blocks of data read from the server in the Memo
component on the form. In an application where you were
transferring a large amount of data and storing it in memory or
writing it to a file you would want to use a much larger array, for
example 4K or 16K, to transfer more data on each call to the
server.
Since we want to put the data into the Memo component the string
of bytes returned from the server must be put in a string variable,
in this case S. The call to SetLength sets the size of S to the
size of the array. Next the DComConnection component is opened to
establish a connection to the server and the memo is emptied.
Transferring the file is accomplished by three custom methods
added to the server application’s IAppServer interface using
the Type Library Editor. The first, OpenFile, takes a single
parameter, the name of the file to be transferred. The While loop
calls the second IAppServer method, GetFileData. GetFileData passes
the variant, VData, as an out parmameter and the array size by
value and returns the number of bytes actually read from the file.
This will be the size of the array for every block except the last
one, which may contain fewer bytes if the file size is not an even
multiple of the block size. If the number of bytes returned by a
call to GetFileData is zero the end of the file has been reached
and the while loop is exited.
The next step is to put the bytes returned in the array into the
string variable, S, and add the string to the Memo component. To
access the data in the variant array faster the array is locked by
the call to VarArrayLock(VData) which returns a pointer to the
actual data array in the variant. The pointer is assigned to the
variable PData which is declared as type PByteArray. PByteArray is
declared in the System unit a pointer to an array of type Byte. The
data is moved from the array to the string variable by calling
Move(PData^, S[1], ByteCount). The Move procedure copies a
specified number of bytes from one location in memory to another.
The first parameter is the source location, the second parameter is
the destination and the third parameter is the number of bytes to
copy. Note that Move performs no error checking of any kind so be
careful to use the correct parameters because strange things will
happen at runtime if you overwrite the wrong area of memory. In
addition, Move does not perform any type checking. You can move any
bit pattern into a string or any other kind of variable. Once the
data has been moved from the array to the string the variant array
is unlocked and the string is added to the Memo. Once the entire
file has been copied a call to the third custom method of
IAppServer, CloseFile, closes the file on the server.
procedure TMainForm.CopyFileBtnClick(Sender: TObject);
const
ArraySize = 20;
var
VData: Variant;
PData: PByteArray;
S: String;
ByteCount: Integer;
begin
with MainDm.Conn do
begin
{Allocate the string variable S to hold the number of bytes returned in
the variant array.}
SetLength(S, ArraySize);
{Connect to the Midas server and empty the memo component.}
if not Connected then Open;
Memo.Lines.Clear;
{Call the server's OpenFile method. This creates the TFileStream on the
server that is used to read the file. The name of the file to read is
passed as a parameter.}
AppServer.OpenFile(ExtractFilePath(Application.ExeName) + 'text.txt');
{Read data from the server until the entire file has been read.}
while True do
begin
{Read a block of data from the server. GetFileData returns the actual
number of bytes read. The parameter is a variant array of bytes passed
by reference.}
VData := Unassigned;
ByteCount := AppServer.GetFileData(VData, ArraySize);
{If the number of bytes read is zero the end of the file has
been reached.}
if ByteCount = 0 then Break;
{Lock the variant array and get a pointer to the array values.}
PData := VarArrayLock(VData);
try
{The read that reaches the end of the file may return fewer bytes
than requested. If so, resize the string variable to hold the number
of bytes actually read.}
if ByteCount < ArraySize then SetLength(S, ByteCount);
{Move the data from the variant array to the string variable.}
Move(PData^, S[1], ByteCount);
finally
VarArrayUnlock(VData);
end; //try
Memo.Lines.Add(S);
end; //while
AppServer.CloseFile;
end; //with
end;
Figure 20 – the Copy File button’s OnClick event
handler
On the server side the methods OpenFile, GetFileData and Close
file were added to the IAppServer interface using the type library
editor. Figure 21 shows the code from the remote data
module’s unit for the OpenFile method. OpenFile contains a
single line of code which creates a FileStream object for the file
passed as a parameter to the method. The file is opened in read
mode and is shared for reading but no writing is allowed. The
FileStream is assigned to the variable Fs which is a private member
variable of the remote data module.
procedure TPassOther.OpenFile(FileName: OleVariant);
begin
{Create the TFileStream object in read mode. Allow other applications
to read the text file but not write to it.}
Fs := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
end;
Figure 21 – the OpenFile method
Figure 22 shows the GetFileData method. This method has an out
parameter which is the variant that will return the variant array
of bytes containing the file data. After creating the variant array
GetFileData locks it for fast access and assigns the pointer
returned by VarArrayLock to the local variable PData. Next it calls
the FileStream read method passing the address PData points to as
the location to store the data and passing VarArrayHighBound(Data,
1) + 1 as the number of bytes to read so the number of bytes read
is always equal to the size of the array. The number of bytes
actually read is assigned to Result and is returned by the
function. Finally, a call to VarArrayUnlock unlocks the variant
array.
function TPassOther.GetFileData(out Data: OleVariant;
ArraySize: Integer): Integer;
var
PData: PByteArray;
begin
Data := VarArrayCreate([0, ArraySize - 1], varByte);
{Lock the variant array and get a pointer to the array of bytes. This makes
access to the variant array much faster.}
PData := VarArrayLock(Data);
try
{Read data from the TFileStream. The number of bytes to read is the
high bound of the variant array plus one (because the array is zero
based). The number of bytes actually read is returned by this function.}
Result := Fs.Read(PData^, VarArrayHighBound(Data, 1) + 1);
finally
VarArrayUnlock(Data);
end; //try
end;
Figure 22 – the GetFileData method
Figure 23 show the CloseFile method which frees the FileStream
object and sets its instance variable to nil. The OnDestroy event
handler for the remote data module also frees the FileStream if Fs
is not nil just in case the client program does not call
CloseFile.
procedure TPassOther.CloseFile;
begin
if Assigned(Fs) then
begin
Fs.Free;
Fs := nil;
end;
end;
Figure 23 – the CloseFile method
Sending Arrays or Other Memory Structures
You can also send an array, a Pascal record or any other data
structure that exists in memory by stuffing it into a variant array
of bytes. Figure 24 shows the GetArray method of the sample Midas
server. This method declares a ten element integer array and loads
it with the numbers one through ten. A variant, VData, is passed as
a var parameter by the client application. GetArray calls
VarArrayCreate to create a variant array of bytes whose size is
equal to the size of the integer array to be returned. Next the
variant array is locked, the integer array is moved into it and the
variant array is unlocked.
procedure TPassOther.GetArray(var VData: OleVariant);
var
IntArray: array[1..10] of Integer;
I: Integer;
PData: PByteArray;
begin
{Put some numbers in the array.}
for I := 1 to 10 do IntArray[I] := I;
{Create the variant array of bytes. Set the upper bound to the size
of the array minus one because the array is zero based.}
VData := VarArrayCreate([0, SizeOf(IntArray) - 1], varByte);
{Lock the variant array for faster access then copy the array to the
variant array and unlock the variant array.}
PData := VarArrayLock(Vdata);
try
Move(IntArray, PData^, SizeOf(IntArray));
finally
VarArrayUnlock(VData);
end; //try
end;
Figure 24 – the GetArray method
Figure 25 shows the OnClick event handler for the Copy Array
button in the PassOther sample application. This method connects to
the Midas server by calling the Open method of the DcomConnection
component. It then calls the GetArray method of the server passing
a variant variable as its parameter. Next, the variant, which now
contains the array, is locked and the data is moved from the
variant array of bytes to the integer array IntArray. Finally the
variant array is unlocked and the integers are displayed in the
memo component on the form.
procedure TMainForm.CopyArrayBtnClick(Sender: TObject);
var
IntArray: array[1..10] of Integer;
VData: Variant;
PData: PByteArray;
I: Integer;
begin
{Connect to the server application.}
if not MainDm.Conn.Connected then
MainDm.Conn.Open;
{Call the server's GetArray method and pass a variant parameter.}
MainDm.Conn.AppServer.GetArray(VData);
{Lock the variant array, copy the data to the array and
unlock the variant array.}
PData := VarArrayLock(VData);
try
Move(PData^, IntArray, SizeOf(IntArray));
finally
VarArrayUnlock(VData);
end; //try
{Display the array values in the memo.}
for I := 1 to 10 do
ArrayMemo.Lines.Add(IntToStr(IntArray[I]));
end;
Figure 25 – the Copy Array button’s OnClick event
handler
Conclusion
Midas provides a powerful flexible way to work with both local
databases and remote database servers. It has proven to be so
useful that it is the cornerstone of Borland’s new DB Express
technology.