Link to home
Start Free TrialLog in
Avatar of doug_stephens
doug_stephensFlag for Canada

asked on

Array parameters to SOAP Server

I am trying to write an invocable function in a Delphi 2005 SOAP Server which updates an array parameter. I can make it work if the array is the returned value but not if it is a parameter. The function is:

function TRFMain.TestArray(_test: array of string; var _message:String): Boolean;
begin
   _test[0] := 'One';
   _test[1] := 'And';
   _test[2] := 'Tree';
   _message := 'One And Tree';
   result := True;
end;

I call it from a button in the client:

procedure TForm1.Button4Click(Sender: TObject);
var p : array of string;
    w : IRFMainservice;
    b : Boolean;
    m : String;
begin
   w := IRFMainservice.Create;
   SetLength(p,3);
   b := w.TestArray(p,m);
   w.Dispose
end;

The array is never changed after the call to TestArray. The string parameter is passed OK.
Avatar of BlackTigerX
BlackTigerX

try something like:

TMyString = string[100];

function TRFMain.TestArray(_test: array of TMyString; var _message:TMyString): Boolean;

didn't test it, just suspect that's the problem
Avatar of doug_stephens

ASKER

Wow, you're quick Btx. But I'm looking to pass an array of strings. Your type is just a 100 character string. Simple strings work fine defined as String, as I mentioned in my post. I also tried something like your idea by using

TMyString = array of string
or
TMyString = array[0..x] of string

instead of just declaring an open array parameter, but same result. My server function can see the values passed from the client. But can't send 'em back in a parameter, just as a result.
ASKER CERTIFIED SOLUTION
Avatar of BlackTigerX
BlackTigerX

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
now, the function is declared as stdcall right?
Yes it is stdcall.
I'll test TStringDynArray also.

Also tried putting the array into a remotable class, as:
   TTestArray = class(TRemotable)
   private
    Fdata: TArray;
    procedure Setdata(const Value: TArray);
   published
      property data : TArray read Fdata write Setdata;
   end;

Same result.
and you have tried var...
function TRFMain.TestArray(var _test: array of string; var _message:String): Boolean;
ah... gets translated into WideString, I'll play some more and come back
I think I've got it finally. On the server, you need to use:
    function ServerFunc(var x: TStringDynArray)

On the client you use something like:
    Function ClientFunc;
    var a : TArrayOfString;
    begin
       ....
       ServerFunc(a);  

If you declare as TStringDynArray in client it will not compile if you have var on the parameter in ServerFunc. If you leave out var, you don't get the modified array returned. It's a bit confusing and none of it is documented.
i use dynamic arrays in SOAP in next way

types definition:

TItem = class(TRemotable)
.....
end;

TItemsArray = array of TItem;

TMainType = class(TRemotable)
  private
    ....
    FItems: TItemsArray;
  published
    ...
   property Items: TItemsArray read FItems write FItems;
end;


server side:


function ISomeInterfaceImpl.GetMainTypeData(out Data: TMainType ): Integer;
var
  i, n: Integer;
begin
  Data := nil;
  result := 0;
  begin
          Data := TMainType.Create;
          ....
          for i:=1 to 3 do begin
            n := Length(Data.FItems);
            SetLength(Data.FItems, n+1);
            Data.FItems[n] := TItem.Create;
            Data.FItems[n].field1  := ....;
            .....
          end;
   end
end;


client side:


procedure getData(Sender: TObject);
var
  intf: ISomeInterface;
  Data: TMainType ;
  i: Integer;
begin
  intf:=nil; Data := nil;
  try
    intf := GetISomeInterface(false, '', nil);
    intf.GetMainTypeData(Data);
    .....
      for i:=0 to Length(Data.Items) - 1 do begin
        .....
     end;
  finally
    intf:=nil;
    if Assigned(Data) then begin
      for I := 0 to Length(FItems)-1 do
        if Assigned(FItems[I]) then
           FItems[I].Free;
        SetLength(FItems, 0);
        Data.Free;
    end;
  end
end;


that would be the correct way of implementing them (like vadim_ti showed), I remember back in the days when SOAP came out it was recommended from Borland that all the complex types were descendant from TRemotable
But with Delphi 2005 I believe that all TDynArrayxxxxxx types are pre-defined as TRemotable. My problem was that in the client you had to define them as TArrayOfxxxx type.

I don't think you can put non-scalar types into a Remotable class, unless they are also Remotable classes. I tried, for example:

 TTestArray = class(TRemotable)
   private
    Fdata: array of string;
    procedure Setdata(const Value: TArray);
   published
      property data : TArray read Fdata write Setdata;
   end;

and it didn't work.

I'd like to exchange a variety of VCL classes such as TList or TStringList in this same way. BTW, is use of OUT parameters important here? I've never used them.
you have no problem to use non-scalar types in a remotable, you only need to register them and free them on client side after use. OUT is not so important, OUT only say function will ignore parameter value, without OUT you can "read" from parameter and modify it.
to put those types you would have to serialize them in a TByteDinArray (or whatever is called)
So, suppose I wanted to pass an object containing a TStringList. I can't have something like this (or can I?):

MyClass = class(TRemotable)
private
     Flist: TStringlist;
published
     property List : TStringlist read Flist write Flist
end;

I think rather, as Btx says, you would have to use a TStringDynArray to serialize the list into an array and mimic all the Stringlist members, basically re-writing the class. Seems wrong.

Is there some easy way to register VCL classes? If not, then I don't quite see the value of remoting.
from Delphi manual:

Remotable object example
This example shows how to create a remotable object for a parameter on an invokable
interface where you would otherwise use an existing class. In this example, the
existing class is a string list (TStringList). To keep the example small, it does not
reproduce the Objects property of the string list.

Because the new class is not scalar, it descends from TRemotable rather than
TRemotableXS. It includes a published property for every property of the string list
you want to communicate between the client and server. Each of these remotable
properties corresponds to a remotable type. In addition, the new remotable class
includes methods to convert to and from a string list.
TRemotableStringList = class(TRemotable)
private
FCaseSensitive: Boolean;
FSorted: Boolean;
FDuplicates: TDuplicates;
FStrings: TStringDynArray;
public
procedure Assign(SourceList: TStringList);
procedure AssignTo(DestList: TStringList);
published
property CaseSensitive: Boolean read FCaseSensitive write FCaseSensitive;
property Sorted: Boolean read FSorted write FSorted;
property Duplicates: TDuplicates read FDuplicates write FDuplicates;
property Strings: TStringDynArray read FStrings write FStrings;
end;
Note that TRemotableStringList exists only as a transport class. Thus, although it has a
Sorted property (to transport the value of a string list’s Sorted property), it does not
need to sort the strings it stores, it only needs to record whether the strings should be
sorted. This keeps the implementation very simple. You only need to implement the
Assign and AssignTo methods, which convert to and from a string list:
procedure TRemotableStringList.Assign(SourceList: TStrings);
var I: Integer;
begin
SetLength(Strings, SourceList.Count);
for I := 0 to SourceList.Count - 1 do
Strings[I] := SourceList[I];
CaseSensitive := SourceList.CaseSensitive;
Sorted := SourceList.Sorted;
Duplicates := SourceList.Duplicates;
end;
procedure TRemotableStringList.AssignTo(DestList: TStrings);
var I: Integer;
begin
DestList.Clear;
DestList.Capacity := Length(Strings);
DestList.CaseSensitive := CaseSensitive;
DestList.Sorted := Sorted;
DestList.Duplicates := Duplicates;
for I := 0 to Length(Strings) - 1 do
DestList.Add(Strings[I]);
end;

Optionally, you may want to register the new remotable class so that you can specify
its class name. If you do not register the class, it is registered automatically when you
register the interface that uses it. Similarly, if you register the class but not the
TDuplicates and TStringDynArray types that it uses, they are registered automatically.
This code shows how to register the TRemotableStringList class and the TDuplicates
type. TStringDynArray is registered automatically because it is one of the built-in
dynamic array types declared in the Types unit.
This registration code goes in the initialization section of the unit where you define
the remotable class:
RemClassRegistry.RegisterXSInfo(TypeInfo(TDuplicates), MyNameSpace, 'duplicateFlag');
RemClassRegistry.RegisterXSClass(TRemotableStringList, MyNameSpace, 'stringList', '',False);
Yes, I've read that. A few times in fact! It seems to me that it says that you CANNOT include VCL classes (such as StringList) in a remotable object. Instead you have to reproduce all the members and use a TxxxDynArray. This makes it very inconvenient for the client app since it won't have access to the public functions to convert a real Stringlist object. Or you duplicate the Assign methods in both apps.

Also, why do the two assignment methods not match their interfaces (TStrings vs TStringList)?
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
>> Btx: ah... gets translated into WideString, I'll play some more and come back

How did you determine that it got translated into a WideString? Did you use some kind of tool to examine the data type?
Vadim_ti, that makes sense. I think this is coming together for me now.

One last supplemental question, really the heart of my post. I was originally having problems modifying an array parameter, although I could get an array returned. This is solved by using TDynStringArray as the parameter type. However, I still have the same problem with the remotable class. In the server I have:

  TMyObject = class(TRemotable)
     private
         FMyData: String;
    procedure SetMyData(const Value: String);
    published
         property MyData : String read FMyData write SetMyData;
  end;


If my client does something like:
    o := TMyObject.Create;
    o.MyData := 'Set by client';
    myServer.TestThis(o);

And my server does something like this with the parameter:
    Function TMyServer.TestThis(o:TMyObject) : Boolean;
       ...
    o.Mydata := 'Set by server';
    return True;

o.MyData is unchanged in the client. If I make it a VAR parameter, I get this error: "referenced object with ID '1' was not found in the document".  I have updated the web reference in the client. It is stdcall. I did not explicity register TMyObject (but I don't think you need to).

If I change the server so that it returns the object, like:
   Function TMyServer.TestThis(o: TMyObject) : TMyObject;
      ...
   o.Mydata := 'Set by Server';
   result := o;

then I get the changed object. Can't I change remotable object properties and pass them back as parameters?
>>> I did not explicity register TMyObject (but I don't think you need to).

YOU NEED TO
and you need to use
var or out to return value
>>> YOU NEED TO
Delphi manual says these are automatically registered when you register the interface which uses the remotable classes, unless you want to use a custom namespace. But anyway I tried adding this to my server interface unit:
RemClassRegistry.RegisterXSClass(TMyObject,'', 'TMyObject', '',False);

And added VAR to the parameter, as
Function TMyServer.TestThis(var o:TMyObject) : Boolean

AND IT WORKS!

Still, I dont' understand why Delphi manual says: "Optionally, you may want to register the new remotable class so that you can specify its class name. If you do not register the class, it is registered automatically when you register the interface that uses it."
Sorry! I pushed to wrong button. Too eager for a solution maybe. It DOES NOT work with the manual registration and VAR parameter as above. Same error message "referenced object with ID '1' was not found in the document".  That error message goes away if I remove VAR, but still don't get the changed object back to the client.

Is my registration correct? Still not sure why I would have to register it.
Optionally, you may want to register the new remotable class so that you can specify its class name

and you want to specify its name

registration is looking good


may be you register it on server side only, and client side does not see your registration?
(uses unit????)

add the same line

RemClassRegistry.RegisterXSClass(TMyObject,'', 'TMyObject', '',False);

in your client side unit
Yes, I did only register in server. I guess I figuered that by adding the web reference all this registry stuff would become resolved automatically.

I tried putting the above into my client's main initialization section but doesn't compile. RegClassRegistry is undefined. I then tried adding InvokeRegistry to my uses clause but that cannot be resolved. (Windows VCL client).
could you post your client side unit where TMyObject class is defined?
No sure what you mean. I only have TMyObject defined in the server interface unit. Here is the code, with some parts removed for readability.

**********

CLIENT main unit looks like:

unit Main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, Borland.Vcl.StdCtrls, Borland.Vcl.ComCtrls, System.ComponentModel,
  localhost.IRFMainservice;

type
  TForm1 = class(TForm)
          ... a bunch of form objects ...
    procedure ChangeObjectClick(Sender: TObject);
  private
  public
  end;

implementation

uses Types, Lib;

{$R *.nfm}

...

procedure TForm1.ChangeObjectClick(Sender: TObject);
var w : IRFMainservice;
    mo : TMyObject;
    m : String;
begin
   w := IRFMainservice.Create;
   mo := TMyObject.Create;
   mo.MyData := 'MyData Set by Client';
   m := 'Original MyData='+mo.MyData;
   m := m + chr(13) + 'Return='+w.TestChangeObject(mo);
   m := m + chr(13) +'After call MyData='+mo.MyData;
   ShowMessage(m);
   mo.Free;
   w.Free;
end;

...

initialization
   RemClassRegistry.RegisterXSClass(TMyObject,'', 'TMyObject', '',False);

end.

***************

SERVER interface unit looks like:

unit RFMainIntf;

interface

uses InvokeRegistry, Types, XSBuiltIns;

type
...
  TMyObject = class(TRemotable)
     private
         FMyData: String;
    procedure SetMyData(const Value: String);
    published
         property MyData : String read FMyData write SetMyData;
  end;
  ....  
    IRFMain = interface(IInvokable)
    ['{E7FAFA7D-E245-4323-BBDD-DF785CE02E52}']
      ... various other functions
      function GetTestChangeObject(_o : TMyObject) : TMyObject; stdcall;
  end;
 
 implementation
 ....
 procedure TMyObject.SetMyData(const Value: String);
 begin
   FMyData := Value;
 end;
 
 initialization
   InvRegistry.RegisterInterface(TypeInfo(IRFMain));
   RemClassRegistry.RegisterXSClass(TMyObject, '', 'TMyObject', '',False);
   RemClassRegistry.RegisterXSClass(TRFClient, '', 'TRFClient', '',False);
 
end.

*************

SERVER implementation unit:

unit RFMainImpl;

interface

uses InvokeRegistry, Types, XSBuiltIns, RFMainIntf, ADODB;

type

  TRFMain = class(TInvokableClass, IRFMain)
  private
     ....
  public
      ...
    function GetTestChangeObject(_o : TMyObject) : TMyObject; stdcall;
  end;

implementation

uses Main, Classes, Sysutils, Lib, SQLClasses;

....

function TRFMain.TestChangeObject(_o: TMyObject): String;
begin
   _o.MyData := 'Server set My Data to this';
   result := 'Server MyData='+_o.MyData;
end;

initialization
  InvRegistry.RegisterInvokableClass(TRFMain);
end.
ok

i do not know waht is localhost.IRFMainservice in uses clause

but if you will place instead it

RFMainIntf

i think all will work

sure you will need to change
       function GetTestChangeObject(_o : TMyObject) : TMyObject; stdcall;
to
       function GetTestChangeObject(var _o : TMyObject) : TMyObject; stdcall;


In D2005 when you add a web reference, you get localhost.IRFMainservice added to your project. I just did File/Use Unit and it was there. That's how I am able to get the definition of TMyObject in my client app.

Are you suggesting that I add the RFMainIntf.pas to my client project? That won't work cause it uses InvokeRegistry to get TRemotable which I cannot compile in my client, as mentioned above.

Regarding var, I had it there during tests and forgot to take it out in my example. If I have var I get the error on my client call.

Also, I still can't do RemClassRegistry.RegisterXSClass on the client side. Do I really need that?
i do not have 2005, so i do not know
I think it would be the same thing - basically provides the client with the interface to the remotable classes.
what you mean in this message?

I tried putting the above into my client's main initialization section but doesn't compile. RegClassRegistry is undefined. I then tried adding InvokeRegistry to my uses clause but that cannot be resolved. (Windows VCL client).

what compilation error you get?
As I said, the client is not able to resolve the names during compile.

I just cannot seem to pass back a changed object via a parameter from a SOAP server to a VCL client, except as a returned value, var parameters or not. Maybe this is not possible.

I think I can work with this, with some minor design changes. I'll just make sure I don't make any server functions that need to modify multiple objects. It's only a minor inconvenience.

Thanks very much to both of you for the assistance. I'm splitting the points.
I FINALLY SOLVED MY PROBLEM!

I had to set 'soRootRefNodeToBody'  to True in my server's pascal invoker, then everything above worked great. Found that in a 2002 post in Google Groups from Bruneau. From the sound of that thread, 'soRootRefNodeToBody'  should be True by default, but is always False in my version (update2 applied). Why? Well, I do usually get a script error when D2005 loads. Hmmmmm.

Anyway, I'm happy as a pig in, well, jello. Think I'll give myself the rest of the day off!