A huge thanks to Cyrille Fauvel, who manages DevTech's Media & Entertainment team but still finds the time to dip into the odd deeply-technical, AutoCAD-related issue. Cyrille provided the original article on this topic late last year, but it's taken me time to get around to editing and publishing it. A quick tip... if you're not interested in the technical details of how Cyrille has exposed the various Properties Palette interfaces to .NET, you can safely skip this post and join us again when we go ahead and make use of the implementation to add dynamic properties to core AutoCAD objects using C#. Most people won't want to know all the details included in this post, but are just interested in the end results: next time I'll be providing a pre-built ObjectARX module, along with full source-code, that can be loaded into AutoCAD 2007-2009 to enable the use of the Properties Palette from .NET.
AutoCAD's Properties Palette - once known as the Object Properties Manager (OPM) - is a very handy way to display properties inside your application, whether those properties are associated with individual objects or with the application itself. The Properties Palette uses COM to communicate with the object(s) in question, and has always required the use of C++ to expose particular interfaces that control the display of the properties in the palette, so its functionality has not been available to developers using managed .NET languages such as C# and VB.NET.
There is some portion of the Properties Palette functionality exposed via the Autodesk.AutoCAD.Windows.ToolPalette namespace, such as the IAcPiPropertyDisplay interface allowing objects and commands to customize the display of properties in the property inspector window, but this is far from complete. This post looks at exposing more of the standard Properties Palette functionality to .NET languages, and next time we'll look at some specific examples of using it from C#.
Our first step is to expose some "OPM" interfaces to .NET. There are two ways to do this: we can expose them from our .NET application sung standard C# or VB.NET code, or we can use a small ObjectARX module to implement them.
Well, we have to use ObjectARX to access some OPM functionality that is currently only exposed via C++, so we're going to go down that path. What we're going to do is expose the interface(s) we want from our ObjectARX module and marshal the various parameters to be useable from .NET. Simple! :-)
To get started, we're going to look at AutoCAD's IPropertyManager2 interface, which is contained in dynprops.h on the ObjectARX SDK:
//--------------------------
// IPropertyManager2 interface
// This is the main property manager class. Use this to add your
// property classes for a given type of IUnknown object.
// You can get this interface using
// CreateOPMIUnknownProtocol(ppUnk)->GetPropertyManager2().
//--------------------------
// {FABC1C70-1044-4aa0-BF8D-91FFF9052715}
DEFINE_GUID(IID_IPropertyManager2, 0xfabc1c70, 0x1044, 0x4aa0, 0xbf, 0x8d, 0x91, 0xff, 0xf9, 0x5, 0x27, 0x15);
interface DECLSPEC_UUID("FABC1C70-1044-4aa0-BF8D-91FFF9052715")
IPropertyManager2 : public IUnknown
{
BEGIN_INTERFACE
// *** IUnknown methods ****
STDMETHOD(QueryInterface)(THIS_ REFIID riid, LPVOID FAR* ppvObj) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
// *** IPropertyManager2 methods ***
STDMETHOD(AddProperty)(THIS_ IUnknown FAR* pDynPropObj) PURE;
STDMETHOD(RemoveProperty)(THIS_ IUnknown FAR* pDynPropObj) PURE;
STDMETHOD(GetDynamicProperty)(THIS_ /*[in]*/LONG index,
/*[out]*/IUnknown ** pDynPropObj) PURE;
STDMETHOD(GetDynamicPropertyByName)(THIS_ /*[in]*/BSTR propName,
/*[out]*/IUnknown ** pDynPropObj) PURE;
STDMETHOD(GetDynamicPropertyCountEx)(THIS_ /*[out]*/LONG* count) PURE;
//For COM Wrappers to generate dynamic property typeinfo
STDMETHOD(GetDynamicClassInfo)(THIS_ /*[in]*/IUnknown* pObj,
/*[out]*/ITypeInfo** pptiDynamic,
/*[out]*/DWORD* dwCookie) PURE;
};
Now in our ObjectARX application we're going to expose the IPropertyManager2 interface to .NET.
At line 41 in the IPropertyManager2.h file in the provided project [which will actually be provided with the final post of the series], we declare the interface using the same Global Unique Identifier (GUID):
[InteropServices::Guid("FABC1C70-1044-4aa0-BF8D-91FFF9052715")]
Then we need to indicate that our interface is derived from the standard, root COM interface, IUnknown:
[InteropServices::InterfaceTypeAttribute(InteropServices::ComInterfaceType::InterfaceIsIUnknown)]
We make it visible to COM, of course:
[InteropServices::ComVisible(true)]
Now for the interface itself. We declare that it's a "public interface class" along with its various members, specifying how to marshal all the types. We're not going to return values from our methods (which usually indicate success or failure): we'll leave it to the .NET code that implements the interfaces (which we'll see in the final post of the series) to throw exceptions instead.
Here's the full class declaration, edited for display on this blog:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
[InteropServices::Guid(
"FABC1C70-1044-4aa0-BF8D-91FFF9052715"
)]
[InteropServices::InterfaceTypeAttribute(
InteropServices::ComInterfaceType::InterfaceIsIUnknown
)]
[InteropServices::ComVisible(true)]
public interface class IPropertyManager2
{
void AddProperty(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pDynPropObj
);
void RemoveProperty(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pDynPropObj
);
void GetDynamicProperty(
[InteropServices::In] long index,
[InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] interior_ptr<Object^> value
);
void GetDynamicPropertyByName(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::BStr
)
] System::String^ name,
[InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] interior_ptr<Object^> value
);
void GetDynamicPropertyCountEx(
[InteropServices::Out] long* count
);
void GetDynamicClassInfo(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pDynPropObj,
[InteropServices::Out,
InteropServices::MarshalAs(
/*InteropServices::UnmanagedType::ITypeInfo*/
InteropServices::UnmanagedType::IUnknown
)
] interior_ptr<Object^> typeInfo,
[InteropServices::Out] ulong* dwCookie
);
};
}
}
}
}
This one class is really at the core of our effort to expose the OPM to .NET. To let us access and use this class, we're going to expose a "property extension factory". We might have opted to P/Invoke an ObjectARX API for this, but it's ultimately cleaner to implement this from C++ and expose it via a managed wrapper.
So how do we know we need a property extension factory? Typically to access the IPropertyManager2 instance from ObjectARX we use the GET_OPMPROPERTY_MANAGER() macro. From dynprops.h, we know that this macro expands into GET_OPMEXTENSION_CREATE_PROTOCOL()->CreateOPMObjectProtocol(pAcRxClass)->GetPropertyManager() and the GET_OPMEXTENSION_CREATE_PROTOCOL() macro expands, in turn, into OPMPropertyExtensionFactory::cast(AcDbDatabase::desc()->queryX(OPMPropertyExtensionFactory::desc())). This is how we know we need to expose the OPMPropertyExtensionFactory class.
So let's take a look at the declaration in the ObjectARX SDK of OPMPropertyExtensionFactory, once again from dynprops.h:
//--------------------------
// OPMPropertyExtension interface
// This class is implemented by AutoCAD and available through
// GET_OPMEXTENSION_CREATE_PROTOCOL. You can add property classes
// by calling GET_OPMPROPERTY_MANAGER for a particular AcRxClass
// to get the property manager for that class.
// You can also enumerate the dynamic properties which have
// been added to that class as well as its base class(es) via
// GetPropertyCount and GetPropertyClassArray
//--------------------------
class OPMPropertyExtensionFactory: public AcRxObject
{
public:
ACRX_DECLARE_MEMBERS(OPMPropertyExtensionFactory);
virtual ~OPMPropertyExtensionFactory(){}
//Retrieves the OPMPropertyExtension for the specified class, if the
//extension has not been added before, it creates it. Note: the implementation
//of this class manages the lifetime of OPMPropertyExtension, as such you don't
//need to delete them.
virtual OPMPropertyExtension* CreateOPMObjectProtocol(AcRxClass* pClass,
LONG lReserved = NULL) = 0;
...
}
To expose this class through .NET we're going to use the Autodesk::AutoCAD::Runtime::Wrapper attribute class to tell AutoCAD to manage this class as an internal class object wrapper. We then derive our wrapper class from RXObject (as the base class of the corresponding ObjectARX class is AcRxObject), and implement a constructor, a GetImpObj() method to provide access to the unmanaged object and our factory function itself, CreateOPMObjectProtocol(). There's no need for a destructor as this is handled by the wrapper.
Here's the complete class declaration from OPMPropertyExtensionFactory.h in the provided sample:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
[Autodesk::AutoCAD::Runtime::Wrapper(
"OPMPropertyExtensionFactory"
)]
public ref class AcMgdOPMPropertyExtensionFactory
: public RXObject
{
public protected:
AcMgdOPMPropertyExtensionFactory(
System::IntPtr unmanagedPointer,
bool bAutoDelete
)
: RXObject(unmanagedPointer, bAutoDelete) {}
internal:
//- Returns the unmanaged ARX Object
inline OPMPropertyExtensionFactory* GetImpObj()
{
return(
static_cast<OPMPropertyExtensionFactory *>(
UnmanagedObject.ToPointer()
)
);
}
public:
virtual AcMgdOPMPropertyExtension^ CreateOPMObjectProtocol(
RXClass^ runtimeClass,
long lReserved
);
} ;
}
}
}
}
There is one last class that needs to be exposed: OPMPropertyExtension, which is what is created by the OPMPropertyExtensionFactory (logically enough). We'll use the same technique as for the OPMPropertyExtensionFactory.
Here's the class declaration from OPMPropertyExtension.h:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
[Autodesk::AutoCAD::Runtime::Wrapper(
"OPMPropertyExtension"
)]
public ref class AcMgdOPMPropertyExtension : public RXObject
{
public protected:
AcMgdOPMPropertyExtension(
System::IntPtr unmanagedPointer,
bool bAutoDelete
)
: RXObject (unmanagedPointer, bAutoDelete) {}
internal:
//- Returns the unmanaged ARX Object
inline OPMPropertyExtension* GetImpObj()
{
return (
static_cast<OPMPropertyExtension *>(
UnmanagedObject.ToPointer()
)
);
}
public:
virtual Object^ GetPropertyManager();
virtual void SetPropertyManager(Object^ pPropManager);
} ;
}
}
}
}
Those are all the declarations (yes, in C++ you need declare your classes separately from their definitions... yawn... :-) so let's go ahead and implement the wrapper classes.
First the OPMPropertyExtensionFactory class (from OPMPropertyExtensionFactory.cpp), with it's main method to create and return a new OPMPropertyExtension:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
AcMgdOPMPropertyExtension^
AcMgdOPMPropertyExtensionFactory::CreateOPMObjectProtocol(
RXClass^ runtimeClass, long lReserved
)
{
return (
gcnew AcMgdOPMPropertyExtension(
System::IntPtr(
GetImpObj()->CreateOPMObjectProtocol(
static_cast<AcRxClass*>(
//runtimeClass->GetImpObj()
runtimeClass->UnmanagedObject.ToPointer()
),
lReserved
)
),
false
)
);
}
}
}
}
}
Here's the implementation of the OPMPropertyExtension class (from OPMPropertyExtension.cpp), which gets us access to the OPMPropertyManager:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
Object^ AcMgdOPMPropertyExtension::GetPropertyManager()
{
IUnknown *pUnk =
GetImpObj()->GetPropertyManager();
return (
System::Runtime::InteropServices::Marshal::
GetObjectForIUnknown(System::IntPtr(pUnk))
);
}
void AcMgdOPMPropertyExtension::SetPropertyManager(
Object^ pPropManager
)
{
IPropertyManager *pPropMgr =
reinterpret_cast<IPropertyManager *>(
System::Runtime::InteropServices::Marshal::
GetIUnknownForObject(pPropManager).ToPointer()
);
GetImpObj()->SetPropertyManager(pPropMgr);
}
}
}
}
}
To finish off our implementation, we're going to re-implement the two handy macros we have in unmanaged C++ (i.e. ObjectARX), to give us the same capabilities when in a managed environment.
Here's our implementation from xOPM.cpp:
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
AcMgdOPMPropertyExtensionFactory^
xOPM::xGET_OPMEXTENSION_CREATE_PROTOCOL()
{
Dictionary^ classDict =
SystemObjects::ClassDictionary;
RXClass^ opmFactoryClass =
(RXClass^)classDict->At("OPMPropertyExtensionFactory");
RXClass^ dbClass =
(RXClass^)classDict->At("AcDbDatabase");
return(
gcnew AcMgdOPMPropertyExtensionFactory(
dbClass->QueryX (opmFactoryClass),
false
)
);
}
Object^
xOPM::xGET_OPMPROPERTY_MANAGER(RXClass^ pAcRxClass)
{
AcMgdOPMPropertyExtensionFactory^ opmFactory =
xOPM::xGET_OPMEXTENSION_CREATE_PROTOCOL();
return(
opmFactory->
CreateOPMObjectProtocol(pAcRxClass, 0)->
GetPropertyManager());
}
}
}
}
}
There's just one more interface that needs exposing to .NET, and that's IDynamicProperty2. We're going to expose this in much the same way as we did IPropertyManager2.
Here's the original declaration from dynprops.h:
//--------------------------
// IDynamicProperty2 interface
// Implement this class to create dynamic properties for the PropertyPalette
// it defines the base set of property attributes as well as
// the name/type/data tuples.
//--------------------------
// {9CAF41C2-CA86-4ffb-B05A-AC43C424D076}
DEFINE_GUID(IID_IDynamicProperty2, 0x9caf41c2, 0xca86, 0x4ffb, 0xb0, 0x5a, 0xac, 0x43, 0xc4, 0x24, 0xd0, 0x76);
interface DECLSPEC_UUID("9CAF41C2-CA86-4ffb-B05A-AC43C424D076")
IDynamicProperty2 : public IUnknown
{
BEGIN_INTERFACE
// *** IUnknown methods ****
STDMETHOD(QueryInterface)(THIS_ REFIID riid, LPVOID FAR* ppvObj) PURE;
STDMETHOD_(ULONG, AddRef)(THIS) PURE;
STDMETHOD_(ULONG, Release)(THIS) PURE;
// *** IDynamicProperty2 methods ***
//Unique property ID
STDMETHOD(GetGUID)(THIS_ /*[out]*/GUID* propGUID) PURE;
// Property display name
STDMETHOD(GetDisplayName)(THIS_ /*[out]*/BSTR* bstrName) PURE;
// Show/Hide property in the OPM, for this object instance
STDMETHOD(IsPropertyEnabled)(THIS_ /*[in]*/IUnknown *pUnk,
/*[out]*/BOOL* pbEnabled) PURE;
// Is property showing but disabled
STDMETHOD(IsPropertyReadOnly)(THIS_ /*[out]*/BOOL* pbReadonly) PURE;
// Get the property description string
STDMETHOD(GetDescription)(THIS_ /*[out]*/BSTR* bstrName) PURE;
// *** Basic property value information ***
// OPM will typically display these in an edit field
// optional: meta data representing property type name, ex. ACAD_ANGLE
STDMETHOD(GetCurrentValueName)(THIS_ /*[out]*/BSTR* pbstrName) PURE;
// What is the property type, ex. VT_R8
STDMETHOD(GetCurrentValueType)(THIS_ /*[out]*/VARTYPE* pVarType) PURE;
// Get the property value, passes the specific object we need the property
// value for.
STDMETHOD(GetCurrentValueData)(THIS_ /*in*/IUnknown *pUnk,
/*[out]*/VARIANT* pvarData) PURE;
// Set the property value, passes the specific object we want to set the
// property value for
STDMETHOD(SetCurrentValueData)(THIS_ /*[in]*/IUnknown *pUnk,
/*[in]*/const VARIANT varData) PURE;
//*** Notifications ***
//OPM passes its implementation of IDynamicPropertyNotify, you
//cache it and call it to inform OPM your property has changed
STDMETHOD(Connect)(THIS_ /*[in]*/IDynamicPropertyNotify2* pSink) PURE;
STDMETHOD(Disconnect)(THIS_ ) PURE;
};
And here's our own exposure of this interface to .NET (from IDynamicProperty2.h):
namespace Autodesk
{
namespace AutoCAD
{
namespace Windows
{
namespace OPM
{
[InteropServices::Guid(
"9CAF41C2-CA86-4ffb-B05A-AC43C424D076"
)]
[InteropServices::InterfaceTypeAttribute(
InteropServices::ComInterfaceType::InterfaceIsIUnknown
)]
[InteropServices::ComVisible(true)]
public interface class IDynamicProperty2
{
void GetGUID(
[InteropServices::Out] System::Guid% propGUID
);
void GetDisplayName(
[InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::BStr
)
] interior_ptr<System::String^> name);
void IsPropertyEnabled(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pUnk,
[InteropServices::Out] System::Int32% bEnabled
);
void IsPropertyReadOnly(
[InteropServices::Out] System::Int32% bReadonly
);
void GetDescription(
[InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::BStr
)
] interior_ptr<System::String^> description
);
void GetCurrentValueName(
[InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::BStr
)
] interior_ptr<System::String^> name
);
void GetCurrentValueType(
[InteropServices::Out] ushort% pVarType
);
void GetCurrentValueData(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pUnk,
[InteropServices::In,
InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::Struct
)
] interior_ptr<Object^> varData
);
void SetCurrentValueData(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pUnk,
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::Struct
)
] Object^ varData
);
void Connect(
[InteropServices::In,
InteropServices::MarshalAs(
/*IDynamicPropertyNotify2*/
InteropServices::UnmanagedType::IUnknown
)
] Object^ pSink
);
void Disconnect();
};
[InteropServices::Guid(
"975112B5-5403-4197-AFB8-90C6CA73B9E1"
)]
[InteropServices::InterfaceTypeAttribute(
InteropServices::ComInterfaceType::InterfaceIsIUnknown
)]
[InteropServices::ComVisible(true)]
public interface class IDynamicPropertyNotify2
{
void OnChanged(
[InteropServices::In,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::IUnknown
)
] Object^ pDynamicProperty
);
void GetCurrentSelectionSet(
[InteropServices::In,
InteropServices::Out,
InteropServices::MarshalAs(
InteropServices::UnmanagedType::Struct
)
] interior_ptr<Object^> pSelection
);
};
}
}
}
}
OK, that's it for the hardcore technical details on exposing AutoCAD's Properties Palette to .NET. In the next post we'll start the real fun with some real examples of using these interfaces from C# (and you can trust me when I say that's going to be a whole lot simpler than this post, honestly! :-).