Desaware Home
Products    Purchase    Publishing    Articles   Support    Company    Contact    
Articles
.NET
COM

 

 

bluebar
Contact Desaware and order today

bluebar
Sign up for Desaware's Newsletter for the latest news and tech tips.

Implementing indirect method and property calls on ActiveX/COM objects

by Daniel Appleman
Copyright © 1997 by Daniel Appleman. All Rights Reserved.

In this article you'll learn about the IDispatch interface, and its role in calling an object's methods and properties. You'll find out how you can use this interface to make indirect calls on an object. One possible use of this technique is to dynamically set properties of controls based on data from a database.

This is an intermediate to advanced level article that presumes some familiarity with both COM and API techniques. If you find this article too complex for you at this time, you'll find the necessary introductory material in my two other books: "Dan Appleman's Developing ActiveX Components with Visual Basic 6.0: A Guide to the Perplexed" ISBN 1-56276-570-6 and "Dan Appleman's Visual Basic Programmer's Guide to the Win32 API" ISBN 0-672-31590-4.

-Note: While the subject matter and technology discussed in this article should prove of interest to all Visual Basic programmers, the implementation techniques shown here do require Desaware's SpyWorks Professional or Standard edition.

Contents:

Objects and Interfaces

The Windows Common Object Model (or COM for short) forms the basis for the operation of Visual Basic, many other applications and increasingly large parts of the operating system itself. COM allows applications and dynamic link libraries to expose "objects". An object, in this context, has the following characteristics:

  • It may contain data, but if it does, you cannot access that data directly.
  • It exposes functionality through one or more sets of methods and properties called "interfaces".
  • It has one or more associated code modules (or servers) which contain code that implements the functionality of the object.

I know that when I see lists such as this, I often pay it only cursory attention. But in this case I encourage you to think carefully about the way these characteristics differ from each other. Many Visual Basic programmers may never see them as different. If you add a class module to your application, the data is managed by your application, you rarely use more than one interface, and the code is part of the application itself.

If you create an ActiveX DLL or EXE server, it becomes more clear that the data in the component is hidden from clients that use the component. If you've ever upgraded a component, you know that the code that implements the component differs from the object itself. As long as the code is backwards compatible, the object will continue to work correctly.

The focus of this article is in the second item in the list - the idea that an object can have more than one interface. It is an idea that is new to many Visual Basic programmers. Why should an object have more than one interface?

There are several reasons for this.

  • One reason is to add new functionality to an object. If you modify a function, by adding or changing a parameter, you must modify all of the code that uses that function. In the case of ActiveX components, this means modifying every program that may use that object. And you have to change all of the programs at once, because any obsolete program will fail the instant you install an updated component. This is clearly an intolerable situation. The COM approach is to add a new interface to the object which contains a new set of functions, including the modified one. Older applications will continue to use the original interface which contains the original unchanged function. Newer applications can be designed to call the new interface with the new function. Your object continues to be fully backward compatible, while at the same time supporting new functionality and new applications.

  • Another reason for supporting multiple interfaces is to allow different objects to provide common functionality. For example: if you define a standard interface that can be used to print an object, you can create a client application that uses this interface to print any object that supports that interface. If you want your component to be printable, you simply implement the this standard interface. This is how it is possible for any ActiveX container to display any ActiveX control. An ActiveX control implements a set of interfaces that are designed especially for ActiveX containers to display controls.

This latter reason is the one that concerns us here.

There are two interfaces that every Visual Basic programmer should know about. The first is called IUnknown. This interface is supported by every COM object (including those you create in Visual Basic, even though you will never see it because it is handled for you entirely behind the scenes). The IUnknown interface is responsible for the reference counting that an object uses to keep track of when it is in use and when it can be freed. It also contains a function that allows you to retrieve a new interface for the object.

Of more interest to us is the IDispatch interface, also known as the automation interface. Any object that supports this interface is also known as an OLE automation, or ActiveX automation object.

IDispatch and Indirection

An object's interface consists of the set of methods that can be called on the object. Methods, in this context, includes properties (which are accessed using method calls). An object's interface is implemented internally by an array of function addresses. If an application that uses an object knows all about an interface for that object when the application is compiled, it can compile that information into its executable and call the object's methods directly. For example: if an object has a method named "Print" which is the third method of an interface, the application can compile a direct call to the third method of the interface into the executable. This is called "early binding" - the interface's method is bound to the application code when the application is compiled.

But consider the following situation: Say an application is designed to manage a large number of objects and it doesn't know ahead of time which objects it will be accessing? Or you want to choose which methods to call while the application is running? In these cases, you need the ability to find out at runtime what methods an object supports, and access those methods by their names. This approach is called "late binding", because the binding of code to the calling application is made at runtime.

Late binding is typically used when you have declared an object variable "As Object". If you want to call the "Print" method on an unknown object, Visual Basic must go to the object itself and first ask it if it supports a method called "Print". If it does, it must ask the object to execute the Print method. An object provides this kind of capability to client applications using the IDispatch interface.

The IDispatch interface has four methods, two of which concern us now: The GetIDsOfNames method is used to determine if an object supports a particular method. The Invoke method is used to call the method.

The key thing to remember here is that there are two distinct ways to call an object's methods. An object can expose an interface containing a set of methods. An object can also implement the IDispatch interface which allows you to call a set of methods. Components that you create with Visual Basic take both approaches simultaneously - implementing what is known as a "dual interface". This means that you can access an object's methods using the direct interface (early binding) or the automation interface (late binding). Many objects choose to implement only one of these approaches.

The following figure illustrates this scenario. The object exposes two interfaces. The lower interface supports a large number of methods and properties and is unique to the class. These can be called directly by an application, or indirectly by way of the IDispatch interface shown above.

If what you've read so far in this section seems confusing, keep in mind that I've just tried to summarize nearly a full chapter in my "Developing ActiveX Components" book into a few paragraphs - I think you'll find it a good source for gaining an in depth understanding of the subject.

The operation of the IDispatch interface, like the IUnknown interface, is hidden from Visual Basic programmers. This means that you really don't know or care whether Visual Basic is using early or late binding. Well, actually, you can determine this, and you may care because it does impact performance, but in either case the details actual method calls are hidden from you in either case.

Unfortunately, by hiding the details from you Visual Basic precludes you from taking advantage of one of the coolest features of IDispatch - the ability to indirectly call an object's methods. Indirection means that you can take an object reference and pass it a string variable containing a method name and it's parameters and have the method called. Something like this:

A$ = "Move(0,0,500,500)"
Form1.A$ or Call(Form1, A$)

One often requested application of this technique is to allow you to dynamically define forms. You could have a database which contains property names and values and set an object's property values at runtime based on the records in the database. You could also create your own scripting tool which calls an object's methods based on user input. Without indirection, you are limited to those methods and properties that you know about when the application is created, and you are forced into using awkward conditional statements and case statements to simulate indirection. This is a shame, because as you have seen, the IDispatch interface supports indirection very nicely -- in fact, indirection is precisely why the IDispatch interface exists.

But why can't you call IDispatch functions directly from Visual Basic?

Automation compatibility and the art of using interfaces.

Methods that can be called through the IDispatch interface have a number of limitations. First of all, they are necessarily slower than those called directly. This is because all of the parameters must be placed into variants which are organized into certain data structures. Since the parameters are all passed as variants, only parameters supported by variants can be passed to these methods.

The IDispatch interface itself supports marshalling between process spaces -- the ability to create an object in one process an call it from another. Only parameter and return data types that Windows knows how to marshal can be used as parameters in methods called via IDispatch.

Finally, Visual Basic itself does not support all of the data types that are supported by IDispatch. If a method uses a parameter that Visual Basic does not recognize, the method cannot be called from Visual Basic.

As long as you are creating objects in Visual Basic, none of these represent problems. However, as a result of these restrictions a large number of interfaces cannot be called directly from Visual Basic. One of these uncallable interfaces is the IDispatch interface itself!

In some cases the incompatibilities are easy to work around -- say, when the interface uses an unsigned data type. In these cases you can create a new type library for the interface which Visual Basic can use to call methods on the interface.

Personally, I don't like this approach. Redefining a standard system interface strikes me as one of the most dangerous things a programmer can do. Interfaces are standard for a reason - and if you allow a non-standard interface to be registered on your system you risk breaking other applications that depend on the interface adhering to the system standard. Windows development is fraught with incompatibilities as it is -- adding potential new incompatibilities is something I prefer to avoid. Plus, the risks of an incompatible type library being accidentally distributed to other systems is too horrible to contemplate.

The type library approach suffers from the additional limitation that there are some interfaces that are difficult (if not impossible) to make Visual Basic compatible.

I wanted to find an approach that had the following features:

  1. Could not possibly interfere with the correct operation of Visual Basic or any other application.
  2. Allowed you to deal with individual methods of an interface without requiring that you redefine those methods that you don't need.
  3. Have no restrictions with regards to data types supported.
  4. Work with any interface - not just IDispatch.

The solution that I came up with was incorporated into SpyWorks as of the version 5.1 release. It's part of the ActiveX extension library and implemented through an object called dwGenericCall - a generic interface calling capability.

The approach (for those of you not yet acquainted with SpyWorks) is quite easy to use. After adding a reference to the Desaware ActiveX Extension Library to your project, you create a dwGenericCall object and initialize it as follows:

Private gencall As dwGenericCall
Set gencall = New dwGenericCall
' Set interface to IDispatch
Call gencall.SetInterfaceInfo _
("{00020400-0000-0000-C000-000000000046}", _
 ObjectToReference)

The SetInterfaceInfo method call serves two purposes. First, it lets you specify which object you wish to access. Second, it lets you specify which interface on the object you wish to use. The interface can be specified either as a quoted string containing a GUID (as shown above for the IDispatch interface), or the actual name of an registered interface (such as "IStorage"). Once you have called SetInterfaceInfo, you can free the ObjectToReference variable. An internal reference will keep your object loaded until the dwGenericCall object is freed.

Since this article concerns itself with providing indirect method access to objects, we'll focus on using this generic calling capability to call the methods of the IDispatch interface. But keep in mind that the techniques shown here can be applied to any interface. In fact, the sample program for this article (which you can download from ftp.desaware.com/SampleCode/Articles/Indirect.zip ) also demonstrates how to call a number of methods of the ITypeInfo interface, which is used to obtain information about the methods supported by an object. The sample code can only be run if you have the latest version of SpyWorks (5.0 standard or 5.1 professional or later), but I believe that you'll find the sample code interesting even if you don't have SpyWorks.

The real trick for calling interfaces is to make them Visual Basic compatible. Fortunately, Visual Basic already provides a well known mechanism for calling incompatible functions. The Declare statement has proven flexible enough to allow Visual Basic programmers to call any API or DLL function. True, it is sometimes necessary to apply some interesting API techniques to handle some of the odder parameter types, but these techniques are fairly well known and well documented (and I'm working on some additional material beyond my API book to make them even better known and better documented -- but I'm getting way ahead of myself).

Since the Declare statement is capable of handling the compatibly issue, the question is: how can we take advantage of that statement in order to call object methods (which are not exported and use a completely different calling mechanism).

The trick is to use a DLL entry point called "dwGenericCall" provided in the Desaware extension object library.

Let's take a look at the IDispatch interface as it is defined in the file oaidl.h:

interface IDispatch : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( 
/* [out] */ UINT __RPC_FAR *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( 
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ ITypeInfo __RPC_FAR *__RPC_FAR *ppTInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( 
/* [in] */ REFIID riid,
/* [size_is][in] */ LPOLESTR __RPC_FAR *rgszNames,
/* [in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ DISPID __RPC_FAR *rgDispId) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( 
/* [in] */ DISPID dispIdMember,
/* [in] */ REFIID riid,
/* [in] */ LCID lcid,
/* [in] */ WORD wFlags,
/* [out][in] */ DISPPARAMS __RPC_FAR *pDispParams,
/* [out] */ VARIANT __RPC_FAR *pVarResult,
/* [out] */ EXCEPINFO __RPC_FAR *pExcepInfo,
/* [out] */ UINT __RPC_FAR *puArgErr) = 0;
};

What a mess!

But don't panic -- it's really not that bad.

The first thing to be aware of is the exact order of the functions in the interface. IDispatch contains four functions that we will number zero through two. Or does it?

See the line: interface IDispatch : public IUnknown at the start of the declaration?

This says that the interface declaration inherits functions from the IUnknown interface. Inheritance is not a very well known concept to Visual Basic programmers, but trust me - it just means that the three methods that are part of IUnknown are also part of IDispatch. So the four methods shown here: GetTypeInfoCount, GetTypeInfo, GetIDsOfNames and Invoke are actually functions three through six (starting our count from zero).

Why the fuss over the number? Because the position of a method within an interface is a critical part of the interface definition. If the position were allowed to change, it would be impossible for early binding to take place because an application would never know where a given function would be in the function list.

In our case, we are only concerned with two of these functions, GetIDsOfNames and Invoke. GetIDsOfNames is used to obtain the dispatch identifiers for the method and the method's parameters given their names in string form. Dispatch identifiers are unique numbers that identify a method that can be called by IDispatch, and are used in the Invoke method to determine which methods to call. The IDispatch Invoke method is used to actually call the methods.

For our purposes we need to create Declare statements for each of these methods. Don’t worry for the time being about the parameter types. I'll cover them in more detail later.

Private Declare Function intGetIDsOfNames Lib "dwAxExtn.dll" _
Alias "dwGenericCall" (ByVal ObjectReference As Long, _
riid As guid, ByVal rgszNames As Long, ByVal cNames As Long, _
ByVal lcid As Long, rgDispID As Long) As Long '5
Private Declare Function intInvoke Lib "dwAxExtn.dll" Alias _
"dwGenericCall" (ByVal ObjectReference As Long, ByVal _
dispIdMember As Long, riid As guid, ByVal lcid As Long, _
ByVal wFlags As Integer, pDispParams As tagDISPPARAMS, _
ByVal pVarResult As Long, ByVal pExcepInfo As Long, _
puArgErr As Long) As Long '6

There are two interesting things to note. First, I changed the name by which the functions will be called in Visual Basic, adding an "int" prefix (short for internal). This is because, while the IDispatch interface cannot be called directly from Visual Basic, the interface does exist. Thus each object already has an Invoke and GetIDsOfNames methods, even though you can't call them. Adding the prefix prevents error messages due to this conflict.

The other thing to note is that both functions are aliased to call the same DLL entry point! Yet they have different numbers and types of parameters and ultimately need to call different methods! How can this be?

The secret is in the first parameter, called "ObjectReference" which is not present in the C++ interface declarations that you saw earlier. To give you an idea of what it does, take a look at the way the intGetIDsOfNames function is actually called:

hres = intGetIDsOfNames(gencall.GenericCallReference(5), _
IID_NULL, VarPtr(names(0)), NameCount, GetUserDefaultLCID(), _
dispids(0))

The gencall object is the dwGenericCall object that you created earlier and attached to the object that you want to call. The ObjectReference parameter is always passed the result of the GenericCallReference function of this object. This function takes as its only parameter the position of the method on the interface (five, for GetIDsOfNames). This provides the SpyWorks ActiveX extension component all of the information that it needs to correctly call the method that you desire using the exact parameters that you passed to the dwGenericCall entry point in the ActiveX extension component's DLL.

Let's review:

  • First you create a dwGenericCall object and associate it with an object and desired interface to call.
  • Next you create some VB declarations for that method.
  • Finally you call the functions that you have declared.

Clearly, the problem of calling methods on an interface has now become somewhat easier -- all you need to do is create the correct Declare statement for each method. Now, this can be a tricky problem, but it's a clearly defined problem. As you can see, there is no need to create, compile and register a type library. Nothing here modifies the registry or risks changing standard interface declarations. And you only need to come up with declarations for those methods that you want to use.

Well, I don't know about you -- but I think that's pretty cool.

Better yet, while I can't promise you the same results, I will note that during the entire development of the IndirectCall class, I did not experience a single memory exception. And that I KNOW is pretty cool.

Inside the IndirectCall Class Object

Now that the problem has been defined as one of creating correct Declare statements, there remain a few specific parameter types to deal with that are unique to OLE (and thus are not discussed in my Win32 API book).

The REFIID parameter type is actually a pointer to a GUID (globally unique identifier) which is 16 bytes that uniquely identify an object or interface. The structure is defined in the IndirectCall class as follows:

Private Type guid
	Data1 As Long
	Data2 As Integer
	Data3 As Integer
	Data4(7) As Byte
End Type

Fortunately, both of the functions that we are concerned with don't actually use this parameter. Instead they require that you pass an empty GUID (all fields set to zero). The parameter type in VB is simply defined 'As GUID', just as you would pass any user defined type to an API or DLL call.

The LCID parameter type is a 32 bit long containing a locale identifier. Locales are described in detail in my Win32 API book. For most applications you can simply return the result of the GetUserDefaultLCID function.

The LPOLESTR __RPC_FAR *rgszNames parameter poses an interesting challenge. The * in the parameter type indicates that it is a pointer. An LPOLESTR is a pointer to an OLE string. What's an OLE string? It's the same type of string used internally within Visual Basic - specifically - it's a Unicode string! In fact, the rgszNames parameter is a pointer to an array of these strings. For our purposes we define the parameter as a Long that is passed ByVal. That way all you need to do is obtain a pointer to the first entry in an array of these strings and pass that pointer. This can be done using the VarPtr operation, which obtains a pointer to an item of data. If the names() array contains the strings, then names(0) is the first string in the array and VarPtr(names(0)) is the address (pointer) to the start of the array - exactly the value that we need to pass.

The other parameters are straightforward once you know that a DISPID is a 32 bit long value. Remember that a UINT (unsigned integer) is 32 bits - Visual Basic integers may be 16 bits, but Win32 integers are 32 bits.

The GetDispIDs method of the IndirectCall class performs the dispatch ID lookup using the intGetIDsOfNames method using the following code:

' Obtain identifiers for function names
' Names(0) is the name of the function. Others are parameter names
Public Function GetDispIDs(names() As String, dispids() _
As Long) As Long
Dim IID_NULL As guid ' Create null GUID
Dim hres As Long, NameCount As Long
VerifyObject
If LBound(names()) <> 0 Then Exit Function
NameCount = UBound(names()) + 1
ReDim dispids(NameCount - 1)
hres = intGetIDsOfNames(gencall.GenericCallReference(5), _
IID_NULL, VarPtr(names(0)), NameCount, _
GetUserDefaultLCID(), dispids(0))
If (hres = &H80020006) And (dispids(0) <> -1) Then _
' Problem with named parameters
	GetDispIDs = -1 ' Not all were converted
	Exit Function
End If
If hres Then
	Err.Raise hres
End If
End Function

You pass the method an array of strings, where the first string (position zero) contains the name of the method or property for which you want to obtain dispatch identifiers. The remaining entries in the array optionally contain names of the method's parameters. The function returns zero on success, -1 if one or more of the parameter names cannot be converted successfully. Any other situation causes an error to be raised. This function takes advantage of the fact that error values under OLE are standardized. The return value from the method is an HRESULT - an OLE error value that we can directly raise to report to the client which error occurred.

The Invoke declaration is longer, but no more complex than the GetIDsOfNames declaration. What makes it tricky is that Invoke is one of the most complex functions found in any OLE interface. It has to handle not just method calls but also properties. Invoke defines four types of calls defined in the ExecutionType enumeration:

Public Enum ExecutionType
   DISPATCH_METHOD = 1
   DISPATCH_PROPERTYGET = 2
   DISPATCH_PROPERTYPUT = 4
   DISPATCH_PROPERTYPUTREF = 8
End Enum       

DISPATCH_METHOD is a function or subroutine call. DISPATCH_PROPERTYGET and DISPATCH_PROPERTYPUT refer to the Property Get and Property Let operations. DISPATCH_PROPERTYPUTREF refers to the Property Set operation.

Parameters are passed to the Invoke method using the DISPPARAMS structure which is defined as follows:

Private Type tagDISPPARAMS
   rgvarg As Long ' Pointer to variants
   rgdispidNamedArgs As Long ' Pointer to named argument dispids
   cArgs As Long
   cNamedArgs As Long
End Type       

The rgvarg field will contain a pointer to an array of variants containing the parameters. The rgdispidNamedArgs filed contains dispatch identifiers for any named parameters. The array of variants first contains the named parameters that correspond to those in the rgdispidNamedArgs array, next it contains the actual parameters in reverse order. The cArgs field contains the total number of parameters. The cNamedArgs field contains the number of named parameters.

The InternalInvoke function shown here is used to provide a fairly low level way of calling the Invoke method.

' See ExecutionType enumeration for the InvokeType parameter
' Parameters() is all of the parameter values
' Named parameters is empty, if there are no named parameters
' Otherwise it is the names of parameters. If there are N _
' items in this array, the first N parameters in the _
' Parameters array are the values for the named 
' parameters. If the Named parameter element is a long or _
' integer, it is the dispid. If a string, the function _
' obtains the dispid for the string. 
' All other values are invalid
' Both arrays are zero based. If the first element in the _
' array is empty ("" in the case of names), the array is _
' considered empty.
' Parameters are in reverse order (thus the first parameter _
' in a function list is the last entry in the _
' Parameters() array
Public Function InternalInvoke(InvokeType As Integer, _
FunctionName As String, parameters() As Variant, _
NamedParameters() As Variant, Optional vres As Variant) As Long
Dim NamesNeedingIDs() As String
Dim dispids() As Long
Dim tdispids() As Long
Dim NameCount As Integer
Dim UnnamedCount As Integer
Dim x&, nameentry&
Dim res&
Dim hres&
Dim dispparam As tagDISPPARAMS
Dim IID_NULL As guid ' Create null GUID
VerifyObject ' Make sure the object is set correctly
' Arrays must be zero based in this design.
If LBound(NamedParameters()) <> 0 Or LBound(parameters()) _
    <> 0 Then
	' Raise invalid parameter error
	RaiseParameterError
End If
' Get the number of names
NameCount = UBound(NamedParameters()) + 1
' Find out the number of named and unnamed parameters
If IsEmpty(NamedParameters(0)) Or NamedParameters(0) = "" _
	Or NamedParameters(0) = "0" Then NameCount = 0
UnnamedCount = UBound(parameters()) + 1
If IsEmpty(parameters(0)) Then UnnamedCount = 0
UnnamedCount = UnnamedCount - NameCount
If UnnamedCount < 0 Then
	' Raise invalid parameter error
	RaiseParameterError
End If
ReDim NamesNeedingIDs(NameCount)
' We always need the dispid for the function
NamesNeedingIDs(0) = FunctionName
nameentry = 0
' Create an array containing the names for which we need _
' dispids.
For x = 0 To NameCount - 1
	If VarType(NamedParameters(x)) = vbString Then
		NamesNeedingIDs(nameentry + 1) = _
                NamedParameters(x)
		nameentry = nameentry + 1
	End If
Next x
' Get the correct number of names
ReDim Preserve NamesNeedingIDs(nameentry)
' Get the dispatch IDs
res = GetDispIDs(NamesNeedingIDs(), tdispids())
If res < 0 Then
	' Raise invalid parameter error
	RaiseParameterError
End If
' Now build the real list
ReDim dispids(NameCount)
dispids(0) = tdispids(0) ' The function name
nameentry = 0
For x = 0 To NameCount - 1
	If VarType(NamedParameters(x)) = vbString Then
		' Copy from the named parameter list
		dispids(x + 1) = tdispids(nameentry + 1)
		nameentry = nameentry + 1
	Else
		' Copy the actual dispid that was passed
		dispids(x + 1) = CLng(NamedParameters(x))
	End If
Next x
' Load the dispparam structure
With dispparam
	.cArgs = UnnamedCount + NameCount
	.cNamedArgs = NameCount
	If NameCount > 0 Then .rgdispidNamedArgs = _
        VarPtr(dispids(1)) _
	' dispids(0) is function name
	If .cArgs > 0 Then .rgvarg = VarPtr(parameters(0))
End With
If Not IsMissing(vres) Then
	hres = intInvoke(gencall.GenericCallReference(6), _
        dispids(0), IID_NULL, GetUserDefaultLCID(), _
	InvokeType, dispparam, VarPtr(vres), 0, _
        intLastBadParameter)
Else
	' Try with a void return
	hres = intInvoke(gencall.GenericCallReference(6), _
        dispids(0), IID_NULL, GetUserDefaultLCID(), _
	InvokeType, dispparam, 0, 0, intLastBadParameter)
End If
If hres = 0 Then
	Exit Function
End If
Err.Raise hres
End Function

The function first figures out how many parameters and named are in the Parameters and NamedParameters arrays. It uses the GetDispIDs function to convert the function name and named parameters into dispatch identifiers. The NamedParameters array consists of variants instead of strings in order to allow you to pass it dispatch identifiers as long variables if you already know them. This is important for performing property assignments which use a special dispatch ID to pass the new value of the property. After loading the internal parameters and dispids arrays with the variants and dispatch identifiers, the tagDISPPARMS structure is initialized with the correct parameter counts and the addresses of the parameter arrays.

The actual intInvoke call is almost anticlimactic at this point. There are two calls, one where a return value is specified, the other where it is a subroutine call. You do need to distinguish between them because OLE automation will raise an error if you request a result to a method that does not provide one, or vice versa.

To Learn More

The InternalInvoke function forms the basis of the IndirectCall class. In most cases you may never call this method. The full class implementation contains additional methods for property and function access that are easier to use. It even includes functions that can parse method name and parameters. For example: the entire function to call the "Move" method to move a form to the upper left corner of the screen is as follows:

Private Sub cmdInvoke_Click()
	Dim a As New IndirectCall
	Set a.ReferencedObject = Me
	Call a.CallIndirect("Move(0,0)")
End Sub

The IndirectCall class also demonstrates use of the ITypeInfo interface to obtain information about the methods and properties supported by an object. Detailed information on interfaces and their methods can be found in the Win32 online documentation. Even this article has only offered a cursory introduction to the Invoke method.

I hope you have found this article worthwhile. At over 5000 words, it counts as a feature article by the standard of any magazine. It is provided at no charge both as a way to provide useful information to the Visual Basic community (especially Desaware's customers), and (of course) in the hope that some readers will find the technology intriguing enough to take a look at SpyWorks, of which this technology is but a small part.

References:

Microsoft Developer's Network Library

"Dan Appleman's Developing ActiveX Components with Visual Basic 6.0: A Guide to the Perplexed

"Dan Appleman's Visual Basic Programmer's Guide to the Win32 API" ISBN 1-56276-446-2

Desaware's SpyWorks Professional 

Sample code at: ftp.desaware.com/SampleCode/Articles/Indirect.zip

For notification when new articles are available, sign up for Desaware's Newsletter.

articles
Related Products:
 
Products    Purchase    Articles    Support    Company    Contact
Copyright© 2012 Desaware, Inc. All Rights Reserved.    Privacy Policy