Mirror

Moving Data via COM (Views: 710)

Problem/Question/Abstract:

Using COM to Transfer Any Type of Data

Answer:

What do you do when you need to move data - data not stored in a database table - between a COM server and a COM client? Simply stuff it in a variant, and pass it as a parameter. You can pass anything, from integer arrays to large binary files, this way.

I'm not talking about using a MIDAS server and client, but any COM server and client. Although the techniques in this article will work 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 put it in a ClientDataSet, and pass that. This is demonstrated in the PassData sample application that accompanies this article (see end of article for download details). This application consists of a COM server and a COM client.

The client's main form, shown in Figure 1, contains a Database, Query, DataSetProvider, ClientDataSet, and DataSource connected to the DBGrid to display the data in the DBDEMOS sample customer table. The following is the Send Data button's OnClick event handler:

procedure TMainForm.SendBtnClick(Sender: TObject);
begin
PassDataServer := CoPassData.Create;
PassDataServer.PassData(CustCds.Data);
end;


Figure 1: The COM client's main form.

The client application uses the server's type library interface unit so it can connect to the server by calling the Create method of the server's CoClass, and assign the interface reference to the variable PassDataServer. PassDataServer is declared as a private member variable of the form, with a type of IPassData. IPassData is the interface implemented by the COM server. The second statement calls the PassData method of the IPassData interface, and passes the ClientDataSet's Data property as a parameter.

The following is the server's PassData method:

procedure TPassData.PassData(CdsData: OleVariant);
begin
with MainForm.CustCds do
begin
Data := CdsData;
Open;
end; // with
end;

This method takes a single parameter of type OleVariant that's 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 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.

Note that the ClientDataSet in the server isn't 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 the MIDAS server sends to the client can come from anywhere. It doesn't have to be stored in a database table. One of the techniques in the PassOther sample application (also available for download; see end of article for details) 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. Then 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 that 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 2 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 FieldDefs entered at design
time. Calling CreateDataSet 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 2: The server's BeforeGetRecords event handler.

The BeforeGetRecords event handler starts by creating a StringList, named FieldVals, that's 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 creates the in-memory dataset using the FieldDefs supplied at design time, and opens the ClientDataSet.

The AssignFile and Reset statements 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 the OwnerData parameter in the client application ClientDataSet's BeforeGetRecords event. Because 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. The demonstration application uses the following text, which is in a standard comma-delimited ASCII file named Text.txt:

"Sherman T. Potter","1/23/1901","MASH 4077"
"B. J. Hunnicut","4/19/29","MASH 4077"
"B. F. Pierce","6/6/1928","MASH 4077"
"Margaret Houlihan","8/8/1930","MASH 4077"

When you assign a string to CommaText, it's parsed on any commas or spaces not enclosed in quotation marks, and each substring is assigned to an element of the StringList. Next, the procedure inserts a new record into the ClientDataSet, and assigns the values from the StringList to the fields in the new record. Finally, the new record is posted. Once the end of the text file is reached, a call to CloseFile closes the ASCII file.

A call to the First method 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 on the last record, the last record is the only one that will be sent to the MIDAS client. Finally (literally finally), a call to the StringList's Free method destroys it.

On the client side, things are even easier. When you open the ClientDataSet in the MIDAS client application, its BeforeGetRecords event fires. The code for the client's BeforeGetRecords event handler follows:

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;

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've seen, it appears as a parameter to the DataSetProvider's BeforeGetRecords event. The result is shown in Figure 3.


Figure 3: The text-file data as it appears in the demonstration client at run time.

Sending a File You Don't Want to Display

Using ClientDataSet is great for data you want to display on a form. Suppose, however, that you need to send a file from a COM server to its client, but you don't want the file's contents displayed in a ClientDataSet.

It's easy - even if you need to send a file that's too large to fit in memory. The File tab of the sample application, which contains Button and Memo components, demonstrates this. Figure 4 shows the code from the Copy File button's OnClick event handler. This procedure begins by declaring a constant, ArraySize, that determines the size of the variant array used to transfer the file from the COM server to the client.

procedure TMainForm.CopyFileBtnClick(Sender: TObject);
const
ArraySize = 20;
var
VData: Variant;
PData: PByteArray;
S: string;
ByteCount: Integer;
begin
with MainDm.Conn do
begin
{ Create the variant array of bytes that will hold the
data read from the text file by the server
application. }
VData := VarArrayCreate([0, ArraySize - 1], varByte);
{ 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 number of bytes read. The parameter is
a variant array of bytes passed by reference. }
ByteCount := AppServer.GetFileData(VData);
{ 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 4: The Copy File button's OnClick event handler.

This sample program displays the blocks of data read from the server in the Memo component on the form (see Figure 5). In an application where you are transferring a large amount of data, and storing it in memory or writing it to a file, you would use a much larger array (e.g. 4KB or 16KB) to transfer more data on each call to the server.


Figure 5: The File tab of the sample application.

The first statement creates a variant array, VData, with a lower bound of zero and an upper bound of ArraySize-1, making it the same size as ArraySize. The array is of type varByte, so it can hold anything. Because we want to put the data into the Memo component, the string of bytes returned from the server must be put into 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, and the Memo is emptied.

Transferring the file is accomplished by three custom methods, which are 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 array, VData, as a var parameter, and returns the number of bytes 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 as 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.

The ^ at the end of the pointer variable PData dereferences the pointer. In other words, PData^ means "the location in memory which PData points to." Note that Move performs no error checking of any kind, so be careful to use the correct parameters. Strange things will happen at run time 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.

On the server side, the methods OpenFile, GetFileData, and CloseFile were added to the IAppServer interface using the Type Library editor. The following shows the code from the remote data module's unit for the OpenFile method:

procedure TPassOther.OpenFile(FileName: OleVariant);
begin
{ Create the TFileStream object in read mode. Allow other
applications to read the text file, but not to write
to it. }
Fs := TFileStream.Create(FileName,
fmOpenRead or fmShareDenyWrite);
end;

OpenFile contains a single statement, 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.

Figure 6 shows the GetFileData method. This method expects a single var parameter, which is the variant array of bytes passed by the client. GetFileData locks the variant array for fast access, then 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. It also passes 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 read is assigned to Result, and returned by the function. Finally, a call to VarArrayUnlock unlocks the variant array.

{ Reads a block of data from the TFileStream, Fs, into the
parameter Data, which is a variant array of bytes.
Returns the number of bytes read into the array. }

function TPassOther.GetFileData(var Data: OleVariant):
Integer;
var
PData: PByteArray;
begin
{ 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). This
function returns the number of bytes read. }
Result :=
Fs.Read(PData^, VarArrayHighBound(Data, 1) + 1);
finally
VarArrayUnlock(Data);
end; // try
end;
Figure 6: The GetFileData method.

The following shows the CloseFile method, which frees the FileStream object and sets its instance variable to nil:

procedure TPassOther.CloseFile;
begin
if Assigned(Fs) then
begin
Fs.Free;
Fs := nil;
end;
end;

The OnDestroy event handler for the remote data module also frees the FileStream if Fs is not nil, just in case the client program doesn't call CloseFile.

Although this example uses a MIDAS client and server, you can use exactly the same technique to transfer a file from a COM server to its client.

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 7 shows the GetArray and LoadVariantArray methods of the sample MIDAS server. GetArray declares a 10-element integer array and loads it with the numbers 1 through 10. The client application passes a variant, VData, as a var parameter. GetArray calls LoadVariantArray and passes it three parameters.

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;
{ Load the integer array into the variant array. }
LoadVariantArray(@IntArray, SizeOf(IntArray), VData);
end;

procedure TPassOther.LoadVariantArray(PData: Pointer;
NumBytes: Integer; var VData: OleVariant);
var
PVData: PByteArray;
begin
{ 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, NumBytes - 1], varByte);
{ Lock the variant array for faster access. Then copy the
array to the variant array, and unlock the variant
array. }
PVData := VarArrayLock(Vdata);
try
{ Move the bytes at the location in memory that PData
points to into the location in memory that PVData
points to. PData points to the integer array and
PVData points to the variant array of bytes. }
Move(PData^, PVData^, NumBytes);
finally
VarArrayUnlock(VData);
end; // try
end;
Figure 7: The GetArray and LoadVariantArray methods.

The first parameter value, @IntArray, is the memory address of the IntArray array. The "at" sign is the "Address of" operator in Pascal, so you can read @IntArray as "the address of IntArray." This provides LoadVariantArray with a pointer to the location in memory where the integer array values are stored. The second parameter value, SizeOf(IntArray), passes the size of IntArray in bytes. The third and final parameter is the variant variable into which the integer array will be loaded.

LoadVariantArray begins by calling VarArrayCreate, which creates a variant array of bytes that's same size as 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. Notice that the first parameter of LoadVariantArray, PData, is of type Pointer. Using the generic Pointer data type means that you can pass the address of any type of variable, array, Pascal record, or any other memory structure to this method. This makes LoadVariantArray completely generic. It can be used to load anything stored in memory into a variant array.

Figure 8 shows the OnClick event handler for the Copy Array button in the PassOther sample application, and the UnloadVariantArray method that is called by the button's OnClick event handler. 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.

procedure TMainForm.CopyArrayBtnClick(Sender: TObject);
var
IntArray: array[1..10] of Integer;
VData: Variant;
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);
{ Get the data out of the variant array. }
UnloadVariantArray(VData, @IntArray, SizeOf(IntArray));
{ Display the array values in the memo. }
for I := 1 to 10 do
ArrayMemo.Lines.Add(IntToStr(IntArray[I]));
end;

procedure TMainForm.UnloadVariantArray(
var VData: OleVariant; PData: Pointer;
NumBytes: Integer);
var
PVData: PByteArray;
begin
{ Lock the variant array, copy the data to the array, and
unlock the variant array. }
PVData := VarArrayLock(VData);
try
{ Move the data in memory that PVData points to (the
variant array data), to the location in memory that
PData points to (the integer array). }
Move(PVData^, PData^, NumBytes);
finally
VarArrayUnlock(VData);
end; // try
end;
Figure 8: The CopyArrayBtnClick and UnloadVariantArray methods.

Next, the OnClick event handler calls UnloadVariantArray, passing it three parameters. The first, VData, is the variant array that contains the values. The second, @IntArray, passes a pointer to the location in memory where you want to place the bytes from the variant array. In this case, the second parameter is the address of the integer array IntArray. The third parameter value is the size of IntArray in bytes.

UnloadVariantArray locks the variant array, and obtains a pointer to its data. It then moves the data from the variant array to the memory location pointed to by PData, the address of the IntArray array, in this case. Finally, the variant array is unlocked and the integers are displayed in the Memo component on the form (again, see Figure 5).

Conclusion

The ability to pass a variant array as a parameter to a COM method call lets you pass any kind of data between a COM server and its client. While the examples in this article used a text file and a variant array, the same methods will work for any kind of file or data stored in memory. The methods used to transfer the text file will work without change for any type of file, including binary files, such as image files. The same is true of the code that transferred the integer array. It will work equally well with a Pascal record, a multi-dimensional array of doubles, or anything else stored in memory. In short, you can put anything into a variant and transfer it to another application using COM.


Component Download: http://www.baltsoft.com/files/dkb/attachment/Moving_Data_via_COM.ziphttp://www.baltsoft.com/files/dkb/attachment/Moving_Data_via_COM.zip


<< Back to main page