Midas & COM Tips and Tricks

By: Bill Todd

Abstract: Shows how to build applications using multiple modules that communicate via COM and share a Midas server. Demonstrates transfering files, arrays and other data structures using COM.

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:

  1. 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.
  1. Makes applications that consist of many modules, such as an accounting system, easy to deploy by deploying just those modules the user needs.
  1. Allows all modules to share a common database connection.
  1. Makes modules easily sharable across applications regardless of the programming language that is used.
  1. 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.


Server Response from: ETNASC02