XML to COM Object Property Mapping
I'm copying this entry that I posted on my old personal blog so I don't lose it. Note that if you read it, convert it to past tense since I wrote it while working for HP. If I remember correctly, the code was the result of browsing through ITypeInfo docs all over the net. Enjoy. One more thing, this code will make you appreciate the Reflection API in .NET even more.
I have various function nuggets that have followed me around like a lost sick puppy over the years and this is one of my most recent. Since I work with a UI architecture that is very dynamic in nature and extremely plugin-oriented, I need to have a set of functions to explore the type information of objects and allow me to dynamically call methods and properties. Here is an example of such a function. Let's say you had some XML that looked something like:
<Object type='Object.ProgID' prop1='XYZ' prop2='true' prop3='43' prop4='3.14159'/>
This is just an example but just by looking you can see that it defines a COM object (with the progID in the type attribute) with several different properties of varying data types (string, boolean, integer, float). The first thing you need to do is obviously parse the relavent data out. For instance, the parser finds a new Object tag, grabs the type attribute and does a CoCreateInstance to create the COM object (I'll leave that as an exercise for the reader...unless someone really doesn't know how to do it). The parser then enumerates through each attribute grabbing 2 strings, the attribute name and the attribute value. The parser doesn't need to worry about doing any type coercion (the method that follows does it for you).
So, at this point a COM object has been created and the parser is enumerating each attribute which corresponds to an actual property name in the COM object. Here's the method that given a instance of a COM object, the property name (string) and the property value (string), calls the correct property setter on the object (criticism is expected but keep in mind the code is still rough since I'm still in implementation phase of the UI engine):
HRESULT CSomeClass::SetObjectProperty( IDispatch* pObject, BSTR propertyName, BSTR propertyValue )
{
USES_CONVERSION;
HRESULT hr = S_OK;
DISPID dispID = 0;
TYPEATTR* typeAttr;
UINT puArgError;
// get ITypeInfo from object
ITypeInfo* pTypeInfo;
hr = pObject->GetTypeInfo( 0, 0, &pTypeInfo );
if( FAILED(hr) )
{
LogMsg( 1, (1, "ERROR:Unable to get type info for pipeline object. Ensure that COM_MAP contains: COM_INTERFACE_ENTRY2( IDispatch, IMainInterfaceName )"));
return FALSE;
}
// get type attributes (# functions e.g.)
pTypeInfo->GetTypeAttr(&typeAttr);
for( WORD iFunction = 0; iFunction < typeAttr->cFuncs; iFunction++ )
{
FUNCDESC* funcDesc;
CComBSTR methodName;
CComVariant vPropValue;
// get function description info
hr = pTypeInfo->GetFuncDesc( iFunction, &funcDesc );
// make sure its a propput function
if( funcDesc->invkind != INVOKE_PROPERTYPUT && funcDesc->invkind != INVOKE_PROPERTYPUTREF )
{
pTypeInfo->ReleaseFuncDesc( funcDesc );
continue;
}
// get method name
hr = pTypeInfo->GetDocumentation(funcDesc->memid, &methodName, 0, 0, 0);
if( FAILED(hr) )
{
pTypeInfo->ReleaseFuncDesc( funcDesc );
continue;
}
// check to make sure we have the right property
if( CString(methodName).CompareNoCase(CString(propertyName )) != 0 )
{
continue;
}
// the dispid
dispID = funcDesc->memid;
// at this point we found the correct property function to call
// now we need to build the VARIANT arg
vPropValue = propertyValue;
vPropValue.ChangeType( funcDesc->lprgelemdescParam[0].tdesc.vt );
// build dispparams
// NOTE: PRB: Error 0x80020004 When Setting a Property (https://support.microsoft.com/support/kb/articles/q175/6/18.asp)
DISPPARAMS dispParams;
DISPID dispidNamed = DISPID_PROPERTYPUT;
dispParams.rgvarg = NULL;
dispParams.rgdispidNamedArgs = &dispidNamed;
dispParams.rgvarg = &vPropValue;
dispParams.cArgs = 1;
dispParams.cNamedArgs = 1;
// invoke function (which is the propput method)
HRESULT hr = pTypeInfo->Invoke( pObject, dispID, funcDesc->invkind, &dispParams, NULL, NULL, &puArgError );
if( FAILED(hr) )
{
// error setting property
LogMsg( 1, (1, "Cannot set property \"%s\". hr=0x%x", OLE2T(methodName), hr ));
pTypeInfo->ReleaseFuncDesc(funcDesc);
}
else
{
pTypeInfo->ReleaseFuncDesc(funcDesc);
}
}
pTypeInfo->Release();
return TRUE;
}
I commented the different sections of the method so it should all be pretty clear. I have several variations that exist but essentially do the same thing. For instance, one variation uses a hashtable of property name -> property values so I don't have to repeatedly call this method over and over again for each property.
There you have it. My first code contribution. I actually have another method that is way cooler than this one. It basically accepts a COM object and a list of property name/property value pairs and calls a method. Those name/value pairs are the parameters to the COM object's method. The cool thing is that the list of pairs can be a) all strings and the method will change the types correctly and b) in any order (i.e. the name/value pairs do not have to match the order of the parameter list for the method. The dynamic method call method will order them and convert types for you).
Comments
- Anonymous
August 24, 2004
I believe it would be easier to use IDispatch::GetIDsOfNames() to get dispid given the name of the property. You don't have to call ChangeType() before calling Invoke(). Invoke() will happily accept a variant of VT_BSTR type and do the change for you.
You'll still need ITypeInfo, TYPEATTR, and friends to serailize the properties to an XML file.
Of course, there is a problem with this if you have decimal properties (VT_R4, VT_R8), AND you're running it under locale that doesn't use the same decimal separator as the one used in your XML file.
In the above example:
<Object type='Object.ProgID' prop1='XYZ' prop2='true' prop3='43' prop4='3.14159'/>
setting prop4 would fail on a swedish locale, because it uses comma as decimal separator. An obvious solution is to always use the same locale in the XML file, independent of the default system locale. If you do this, you should use VariantChangeTypeEx() to change the variant type, as it allows you to specify the LCID.