Author: Alessandro Federici
Streaming COM Objects as XML
Answer:
Introduction
When I was making my first COM objects I used to think that I could give my COM
classes the same shape of regular Delphi ones. I am specifically talking about
properties and a "stateful life-style" in which you keep using its methods
similarily to what you'd do with a DataSet (i.e. Open, Next, Next, Close). Well
yes, COM allows you to do that and if the objects lives on the client's computer
everything works fast and efficiently. The problem araises if and when you move
your object to another machine. In that situation then your application suddenly
starts to slow down and your client becomes dependent on the network condition.
Each time you access a property you are invoking a method that executes on another
machine. If your code makes such calls continuosly you will be in big trouble. The
ideal solution would be to redesign such objects making them "stateless". With
"stateless object" I mean those whose methods do all they say they will do (i.e.
ExecuteMoneyTransfer) and don't depend on other methods (i.e. ExecuteMoneyTransfer
doesn't expect a Commit or Rollback method to be called by the client after it is
completion). But often the amount of legacy code makes a resedign not practical. Is
it possible then to do anything to improve performance and makes stateful objects
stateless? As you would expect it is possible (no point in writing this article
otherwise ;-) ). By persisting the object's state in some kind of intermediary
format (I choose XML) and streaming it in one shot you can achieve the goal.
Remember that this is not an optimal solution and the code I am presenting here is
not optimized either. If you are starting from scratch you should design stateless
objects.
TlbInf32.dll
Included in Visual Studio 6 and Visual Basic 6.0 CDs you can find a very handy DLL
called TlbInf32. You can download the documentation for this file on the MSDN
websidehttp://www.msdn.microsoft.com/downloads/default.asp?url=/downloads/sample.asp
?url=/msdn-files/027/000/255/msdncompositedoc.xml. If you don't have Visual Studio
you can download the DLL from the website
Compiled.orghttp://www.compiled.org/showcontent.php?cid=118
TlbInf32 includes a set of classes that can help you reading type information for
both type libaries and COM objects. Now, download my example by clicking
herehttp://www.msdelphi.com/Articles/COM/Streaming_COM_Objects_to_XML/Streaming_COM_
Objects_to_XML.ZIP.
The code
The example is very straightforward. I won't spend too much time on it. There's a
simple COM library with an object which implements the following interface:
1 ISimpleObject = interface(IDispatch)
2 function Name: WideString[propget, dispid $00000001]; safecall;
3 procedure Name(Value: WideString)[propput, dispid $00000001]; safecall;
4 function Age: Integer[propget, dispid $00000002]; safecall;
5 procedure Age(Value: Integer)[propput, dispid $00000002]; safecall;
6 end;
7
8 //Then there's a client application that is able to persist and restore the state
9 of it by using an auxiliary class called TTypeInfoStreamer which is defined as:
10
11 TTypeInfoStreamer = class
12 private
13 fTLI: _TLIApplication;
14 public
15 constructor Create;
16
17 function GetObjectAsXML(const anObject: IDispatch): widestring;
18 procedure SetObjectAsXML(const anObject: IDispatch; aString: widestring);
19 end;
20
21 //The two methods GetObjectAsXML and SetObjectAsXMl are what interest us. Please,
22 don't make an example of the code. I put this sample togheter in 10 minutes to
23 answer a question of a guy in a newsgroup. Take a look at the lines I highlighted:
24
25 function TTypeInfoStreamer.GetObjectAsXML(const anObject: IDispatch): widestring;
26 var
27 xml: DOMDocument30;
28 intfinfo: InterfaceInfo;
29 root,
30 node: IXMLDOMNode;
31 i: integer;
32 val: OleVariant;
33 p: PSafeArray;
34 begin
35 p := MakeEmptyParmsArray;
36
37 try
38 intfinfo := fTLI.InterfaceInfoFromObject(anObject);
39
40 xml := CoDOMDocument30.Create;
41 xml.async := FALSE;
42
43 root := xml.createNode(' '');
44 xml.appendChild(root);
45
46 with intfinfo do
47 for i := 1 to (Members.Count) do
48 begin
49 if not (Members[i].InvokeKind = INVOKE_PROPERTYGET) then
50 Continue;
51
52 val := fTLI.InvokeHook(anObject, Members[i].Get_MemberId,
53 INVOKE_PROPERTYGET,
54 p);
55
56 node := xml.createNode('element', Members[i].Name, '');
57 node.text := VarToStr(val);
58 root.appendChild(node);
59 end;
60
61 finally
62 result := root.xml;
63 SafeArrayDestroy(p);
64 end;
65 end;
As you can see we created an instance of the TLIApplication object (included in
TlbInf32.dll), passed a pointer to the object we want to stream and then looped
tough its Members collections. The members collection is the list of methods
implemented by the object. What we want to read is the value of the object's
properties so we will only stop on the methods that return the value of a property
(Members[i].InvokeKind=INVOKE_PROPERTYGET) . In order to invoke the method we need
to call the method TLIApplication.InvokeHook which is defined as:
function InvokeHook(const Object_: IDispatch; ID: OleVariant; InvokeKind:
InvokeKinds;
var ReverseArgList: PSafeArray): OleVariant; safecall;
It's interesting to note how the ID parameter could be either the name of the
method or its DispID. So, in case you have the DispID already, you wouldn't need to
use late bound calls (which first invoke the IDispatch.GetIDOfNames method slowing
things down a *lot*). InvokeKind tells the TLIApplication *how* to invoke it and
finally the ReverseArgList is a safe array that in our case only contains no
values. See the rest of the code to find out how I build one. The result is
something like this:
Alessandro Federici
25
Et voila'! We have our COM object persisted into XML! Now we need to set back these
values. See the code below.
66 procedure TTypeInfoStreamer.SetObjectAsXML(const anObject: IDispatch;
67 aString: widestring);
68 var
69 xml: DOMDocument30;
70 intfinfo: InterfaceInfo;
71 root,
72 node: IXMLDOMNode;
73 i: integer;
74 val: OleVariant;
75 p: PSafeArray;
76 s: string;
77 begin
78 p := MakeOneElementArray;
79
80 try
81 intfinfo := fTLI.InterfaceInfoFromObject(anObject);
82
83 xml := CoDOMDocument30.Create;
84 xml.async := FALSE;
85 xml.loadXML(aString);
86 root := xml.documentElement;
87
88 with root do
89 for i := 0 to (childNodes.length - 1) do
90 begin
91 s := childNodes[i].nodeName;
92 SetOneElementArray(p, childNodes[i].Text);
93 fTLI.InvokeHook(anObject, s, INVOKE_PROPERTYPUT, p);
94 end;
95
96 finally
97 SafeArrayDestroy(p);
98 end;
99 end;
As you can see we did the exact opposite of what the had done before except that in this case we invoked the method as a property writer. I hope this will demistify a little how to read COM type information and stream its contents in an arbitraty format. The TTypeInfoStreamer class is far from being a complete class but feel free to use the code as a start. Happy coding!
|