//
you're reading...
COM, Programming Issues/Tips

Setting a Property by IDispatch Invoke.

1. Introduction.

1.1 When setting the property of a COM object via IDispatch::Invoke() using DISPATCH_PROPERTYPUT, it is important to ensure the following :

  • DISPPARAMS.rgdispidNamedArgs must point to a DISPID whose value is
    DISPID_PROPERTYPUT (-3).
  • DISPPARAMS.cNamedArgs must be equal to 1.

1.2 If the above settings are not done, IDispatch::Invoke() will return DISP_E_PARAMNOTFOUND.

1.3 This blog will demonstrate this problem.

2. Example Code.

2.1 To demonstrate the problem, I have developed a simple COM class MyATLClass written in ATL. The following is a sample IDL definition for such a COM class :

[
	object,
	uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
	dual,
	nonextensible,
	helpstring("IMyATLClass Interface"),
	pointer_default(unique)
]
interface IMyATLClass : IDispatch
{
	[propget, id(1), helpstring("property long_property")] HRESULT long_property([out, retval] LONG* pVal);
	[propput, id(1), helpstring("property long_property")] HRESULT long_property([in] LONG newVal);
};

[
	uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx),
	helpstring("MyATLClass Class")
]
coclass MyATLClass
{
	[default] interface IMyATLClass;
};

The MyATLClass exposes a single interface IMyATLClass which exposes only one property “long_property” of type long.

2.3 For simplification purposes, let’s assume that the ATL project compiles to a DLL named ATLCOMServer.dll with type library ATLCOMServer.tlb. Let’s further assume that the MyATLClass COM class has progID “ATLCOMServer.MyATLClass”.

2.4 The following is a code listing of a client application written in C++ :

#include "stdafx.h"
#import "ATLCOMServer.tlb" raw_interfaces_only no_implementation
using namespace ATLCOMServerLib;

void SetIntegerProperty(IDispatch* dispatch, LONG lValue);

void GetIntegerProperty(IDispatch* dispatch, LONG& lValueReceiver);

int _tmain(int argc, _TCHAR* argv[])
{
	::CoInitialize(NULL);

	if (1)
	{
		IMyATLClassPtr spIMyATLClass = NULL;

		spIMyATLClass.CreateInstance(__uuidof(MyATLClass));

		SetIntegerProperty(spIMyATLClass, 100);

		LONG lValueReceiver = 0;

		GetIntegerProperty(spIMyATLClass, lValueReceiver);
	}

	::CoUninitialize();

	return 0;
}

The following is a summary of the code above :

  • An instance of the MyATLClass COM class is instantiated and a pointer to its IMyATLClass interface is obtained.
  • The SetIntegerProperty() helper function is used to set the “long_property” property to a value of 100.
  • The GetIntegerProperty() helper function is then used to get the “long_property” property in order to confirm that its value is indeed 100.

2.5 The following is a full code listing of the SetIntegerProperty() helper function :

void SetIntegerProperty(IDispatch* dispatch, LONG lValue)
{
	OLECHAR FAR* rgszNames = L"long_property";
	DISPID dispid = 0;

	dispatch -> GetIDsOfNames
	(
		IID_NULL,
		&rgszNames,
		1,
		LOCALE_SYSTEM_DEFAULT,
		&dispid
	);

	DISPPARAMS dp;
	DISPID dPutID;
	VARIANT varValue;

	VariantInit(&varValue);
	V_VT(&varValue) = VT_I4;
	V_I4(&varValue) = lValue;	

	memset(&dp, 0, sizeof(DISPPARAMS));
	dp.cArgs = 1;   // Number of arguments to Invoke() remains 1.
	dp.rgvarg = &varValue;

	// This is important.
	// Comment out the following 3 lines to
	// see the effects : the Invoke()
	// call later will return a value of
	// 0x80020004 (DISP_E_PARAMNOTFOUND).
	dPutID = DISPID_PROPERTYPUT;
	dp.rgdispidNamedArgs = &dPutID;
	dp.cNamedArgs = 1;

	VARIANT vr;
	VariantInit(&vr);

	//Finally make the call
	UINT nErrArg = 0;
	HRESULT hr = S_OK;

	hr = dispatch -> Invoke
	(
	  dispid,
	  IID_NULL,
	  LOCALE_SYSTEM_DEFAULT,
	  DISPATCH_PROPERTYPUT,
	  &dp,
	  &vr,
	  NULL,
	  &nErrArg
	);

	VariantClear(&varValue);
	VariantClear(&vr);

	return;
}

2.6 The above code is essentially a typical IDispatch::Invoke() call to set a property of a COM object. However, note the lines headed by the comment “This is important.” :

// This is important.
// Comment out the following 3 lines to
// see the effects : the Invoke()
// call later will return a value of
// 0x80020004 (DISP_E_PARAMNOTFOUND).
dPutID = DISPID_PROPERTYPUT;
dp.rgdispidNamedArgs = &dPutID;
dp.cNamedArgs = 1;

These 3 lines are vital to the success of the ensuing IDispatch::Invoke() call with DISPATCH_PROPERTYPUT being the context of the Invoke() call.

2.7 If these 3 lines are commented out, the call to Invoke() will result in a return value of 0x80020004 (DISP_E_PARAMNOTFOUND).

2.8 For completion, I have also added the code listing for the GetIntegerProperty() helper function :

void GetIntegerProperty(IDispatch* dispatch, LONG& lValueReceiver)
{
	OLECHAR FAR* rgszNames = L"long_property";
	DISPID dispid = 0;

	dispatch -> GetIDsOfNames
	(
		IID_NULL,
		&rgszNames,
		1,
		LOCALE_SYSTEM_DEFAULT,
		&dispid
	);

	DISPPARAMS dp;
	memset(&dp, 0, sizeof(DISPPARAMS));

	VARIANT vr;
	VariantInit(&vr);

	//Finally make the call
	UINT nErrArg = 0;
	HRESULT hr = S_OK;

	hr = dispatch -> Invoke
	(
	  dispid,
	  IID_NULL,
	  LOCALE_SYSTEM_DEFAULT,
	  DISPATCH_PROPERTYGET,
	  &dp,
	  &vr,
	  NULL,
	  &nErrArg
	);

	if (V_VT(&vr) == VT_I4)
	{
	  lValueReceiver = V_I4(&vr);
	}

	VariantClear(&vr);

	return;
}

3. Setting the Property From Managed Code.

3.1 In managed code, if the property setting is done using Type.InvokeMember(), the setting will go through without any special settings.

3.2 The following is an example :

private static void TestUsingTypeInvokeMember(ref object COMObj, Int32 iValue)
{
    // The property set will be successful.
    COMObj.GetType().InvokeMember("long_property", BindingFlags.SetProperty, null, COMObj, new object[] { iValue });

    // The property get will return the correct value
    // which was set previously.
    object objRet = COMObj.GetType().InvokeMember("long_property", BindingFlags.GetProperty, null, COMObj, null);
}

static void Main(string[] args)
{
    object COMObj = Activator.CreateInstance(Type.GetTypeFromProgID("ATLCOMServer.MyATLClass"));

    TestUsingTypeInvokeMember(ref COMObj, 100);
}

3.3 However, when the property is set using the IDispatch COM interface, performed using COM Interop, the same requirement as described in point 1.1 must be observed. Otherwise, the same DISP_E_PARAMNOTFOUND error code will be returned.

3.4 The following is a full listing of a property setting procedure in C# performed using IDispatch::Invoke() :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Reflection;

namespace CSConsoleClient01
{
    // The COM IDispatch interface must be included in the C# source codes.
    [ComImport()]
    [Guid("00020400-0000-0000-C000-000000000046")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IDispatch
    {
        [PreserveSig]
        int GetTypeInfoCount(out int Count);

        [PreserveSig]
        int GetTypeInfo
            (
                [MarshalAs(UnmanagedType.U4)] int iTInfo,
                [MarshalAs(UnmanagedType.U4)] int lcid,
                out System.Runtime.InteropServices.ComTypes.ITypeInfo typeInfo
            );

        [PreserveSig]
        int GetIDsOfNames
            (
                ref Guid riid,
                [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)]
                string[] rgsNames,
                int cNames,
                int lcid,
                [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId
            );

        [PreserveSig]
        int Invoke
            (
                int dispIdMember,
                ref Guid riid,
                uint lcid,
                ushort wFlags,
                ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams,
                out object pVarResult,
                ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo,
                out UInt32 pArgErr
            );
    } 

    class Program
    {
        const int LOCALE_SYSTEM_DEFAULT = 2048;
        const int DISPATCH_PROPERTYGET = 0x2;
        const int DISPATCH_PROPERTYPUT = 0x4;
        const int SizeOfNativeVariant = 16;
        const int DISPID_PROPERTYPUT = -3;

        [DllImport(@"oleaut32.dll", SetLastError = true, CallingConvention = CallingConvention.StdCall)]
        static extern Int32 VariantClear(IntPtr pvarg);

        private static void SetPropertyUsingIDispatchInvoke(ref object COMObj, Int32 iValue)
        {
            Guid IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
            IDispatch pIDispatch = (IDispatch)COMObj;
            string[] rgsNames = new string[1] { "long_property" };
            int[] rgDispId = new int[1] { 0 };

            int hrRet = pIDispatch.GetIDsOfNames
            (
                ref IID_NULL,
                rgsNames,
                1,
                LOCALE_SYSTEM_DEFAULT,
                rgDispId
            );

            if (hrRet == 0)
            {
                System.Runtime.InteropServices.ComTypes.DISPPARAMS DispParams
                    = new System.Runtime.InteropServices.ComTypes.DISPPARAMS();
                IntPtr pVariant = Marshal.AllocCoTaskMem(SizeOfNativeVariant);
                object varResult;
                System.Runtime.InteropServices.ComTypes.EXCEPINFO ExcepInfo
                    = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
                UInt32 pArgErr = 0;

                Marshal.GetNativeVariantForObject(iValue, pVariant);

	            DispParams.cArgs = 1;
	            DispParams.rgvarg = pVariant;
                // This is important.
                // Comment out the following 3 lines to
                // see the effects : the Invoke()
                // call later will return a value of
                // 0x80020004 (DISP_E_PARAMNOTFOUND).
                long dPutID = DISPID_PROPERTYPUT;
                GCHandle gch = GCHandle.Alloc(dPutID, GCHandleType.Pinned);
                IntPtr pdPutID = gch.AddrOfPinnedObject();

                DispParams.rgdispidNamedArgs = pdPutID;
                DispParams.cNamedArgs = 1;

                hrRet = pIDispatch.Invoke
                (
                    rgDispId[0],
                    ref IID_NULL,
                    LOCALE_SYSTEM_DEFAULT,
                    DISPATCH_PROPERTYPUT,
                    ref DispParams,
                    out varResult,
                    ref ExcepInfo,
                    out pArgErr
                );

                gch.Free();

                VariantClear(pVariant);
                Marshal.FreeCoTaskMem(pVariant);
                pVariant = IntPtr.Zero;
            }
        }

        private static void GetPropertyUsingIDispatchInvoke(ref object COMObj, out Int32 iValueReceiver)
        {
            // Set receiver to default value.
            iValueReceiver = 0;

            Guid IID_NULL = new Guid("00000000-0000-0000-0000-000000000000");
            IDispatch pIDispatch = (IDispatch)COMObj;
            string[] rgsNames = new string[1] { "long_property" };
            int[] rgDispId = new int[1] { 0 };

            int hrRet = pIDispatch.GetIDsOfNames
            (
                ref IID_NULL,
                rgsNames,
                1,
                LOCALE_SYSTEM_DEFAULT,
                rgDispId
            );

            if (hrRet == 0)
            {
                System.Runtime.InteropServices.ComTypes.DISPPARAMS DispParams
                    = new System.Runtime.InteropServices.ComTypes.DISPPARAMS();
                object varResult;
                System.Runtime.InteropServices.ComTypes.EXCEPINFO ExcepInfo
                    = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
                UInt32 pArgErr = 0;

                DispParams.cArgs = 0;
                DispParams.cNamedArgs = 0;
                DispParams.rgdispidNamedArgs = IntPtr.Zero;
                DispParams.rgvarg = IntPtr.Zero;

                hrRet = pIDispatch.Invoke
                (
                    rgDispId[0],
                    ref IID_NULL,
                    LOCALE_SYSTEM_DEFAULT,
                    DISPATCH_PROPERTYGET,
                    ref DispParams,
                    out varResult,
                    ref ExcepInfo,
                    out pArgErr
                );

                if (hrRet == 0)
                {
                    iValueReceiver = (Int32)varResult;
                }
            }
        }

        static void Main(string[] args)
        {
            object COMObj = Activator.CreateInstance(Type.GetTypeFromProgID("ATLCOMServer.MyATLClass"));

            SetPropertyUsingIDispatchInvoke(ref COMObj, 200);

            Int32 iValueReceiver = 0;

            GetPropertyUsingIDispatchInvoke(ref COMObj, out iValueReceiver);
        }
    }
}

4. COM Server Written in Managed Code.

4.1 If the COM Server is written in managed code (e.g. C#), this problem will not occur.

4.2 That is, a property set procedure will not require the DISPPARAMS.rgdispidNamedArgs and the DISPPARAMS.cNamedArgs fields to be set as described in point 1.1.

4.3 However, setting the DISPPARAMS.rgdispidNamedArgs and the DISPPARAMS.cNamedArgs fields will do no harm.

4.4 I will leave the reader with a sample COM Server written in C# for testing purposes :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace CSServer
{
    [ComVisible(true)]
    [Guid("08987702-ADC6-453d-BE09-6E52176F5A4F")]
    [ClassInterface(ClassInterfaceType.AutoDispatch)]
    [ProgId("CSServer.MyClass")]
    public class MyClass
    {
        [DispId(1)]
        public Int32 int_property
        {
            get
            {
                return m_Int32;
            }

            set
            {
                m_Int32 = value;
            }
        }

        private Int32 m_Int32;
    }
}

5. In Conclusion.

5.1 The rather strange requirement described in point 1.1 is there due to historical reasons. The fact that it is removed in servers created in managed code and handled by COM-Callable-Wrappers hints at its redundancy.

5.2 This requirement is most likely enforced in OLEAUT32.DLL (the standard Proxy/Stub DLL for Ole Automation).

5.3 Managed COM servers are managed by COM-callable-wrappers (CCW) when run in unmanaged code. Hence this requirement is most likely waivered by the CCW.

Advertisements

About Lim Bio Liong

I've been in software development for nearly 20 years specializing in C , COM and C#. It's truly an exicting time we live in, with so much resources at our disposal to gain and share knowledge. I hope my blog will serve a small part in this global knowledge sharing network. For many years now I've been deeply involved with C development work. However since circa 2010, my current work has required me to use more and more on C# with a particular focus on COM interop. I've also written several articles for CodeProject. However, in recent years I've concentrated my time more on helping others in the MSDN forums. Please feel free to leave a comment whenever you have any constructive criticism over any of my blog posts.

Discussion

One thought on “Setting a Property by IDispatch Invoke.

  1. Hi, first of all thanks for your blog: it helped me a lot. I would ask you to be so kind to explain more details about the IDispatch::Invoke() in 3.4 (from .NET). I need to use it to implement a kind of COM reflection in C++/CLI (managed) but I can’t understand how to correctly pass my .NET parameters to the call. In particular the ComTypes::DISPPARAMS structure it’a bit tricky to understand. I googled everywhere but couldn’t find any good sample except your blog.

    Posted by Daniele | December 27, 2012, 4:59 pm

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: