Implementing IDTExtensibility2 in an Automation Add-in
The OZ blog engine had a minor mishap the other day, so I'm reposting a couple of entries here (with the help of Neville van der Merwe, who helped me resurrect the old posts).
Neville raised a good point about using IDTExtensibility2 in an automation add-in. You can indeed use the default Visual Studio Shared Add-in project as the basis for an automation add-in. You can then either strip out all the IDTExtensibility2 code as per my previous blog. Alternatively, you can leave it in and attribute your class with [ClassInterface(ClassInterfaceType.AutoDual)]. This is the approach I took in my book.
If we want to implement more than one interface (in this case, the custom interface that defines our UDFs and IDTExtensibility2, then we have to use ClassInterfaceType.AutoDual - this munges the 2 interfaces into a new one, which is also set to be the default interface for the coclass. If your class is called "Connect", the AutoDual class interface will be named "_Connect". Excel caches the function-name list based on the typeinfo for the default interface.
AutoDual interfaces are generally a bad idea. The generated class interface exposes type information for all the public COM-visible members of the class and its base classes. This is irrelevant to late-binding clients, but a timebomb for early-binding COM clients. COM clients can vtable bind. If the managed class - or any of the COM-visible classes in its inheritance - is updated with more/fewer methods, this will break vtable-bound clients. Moreover, the designer of a base class might not have exposed an AutoDual interface and therefore may rightly consider it reasonable to add new methods arbitrarily to the class. Any new method in a base class will offset the vtable for the derived class, instantly breaking any early-bound client.
So, in general, you should never ship a class that exposes an AutoDual class interface unless all of its base classes also expose an AutoDual class interface, and unless all of the classes are under your control. This is why I prefer to avoid them. However, in the specific case of automation add-ins, the client is late-bound, so this is not really an issue. This is one of the very few scenarios where you can strategically use AutoDual without risk of it coming back to bite you somewhere down the road. The insidious nature of AutoDual makes me strongly recommend against its use in general, but in the case of Automation Add-ins, it's OK.
If you do chose to implement IDTExtensibility2 in your automation add-in, you can use some of the IDTExtensibility2 methods in the normal way. That is, you can implement OnConnection and/or OnStartupComplete for initialization, and OnDisconnection and/or OnBeginShutdown for cleanup. These 4 methods will be called at the expected times. For an automation add-in (as opposed to a COM add-in), the OnAddinsUpdate method will not be called. OnAddinsUpdate is called specifically when COM add-ins are added to or removed from the collection. OnDisconnection is potentially misleading, because you cannot actually disconnect an automation add-in. Even unchecking the checkbox in the (automation) Add-ins dialog does not disconnect the add-in. There is no way to disconnect/unload an automation add-in once it has been loaded (short of unregistering it and shutting down Excel).
When you add an automation add-in in the Add-ins dialog, this adds an entry in HKCU\Software\Microsoft\Office\11.0\Excel\Options named OPEN, OPEN1, OPEN2 etc. This has the value: /A "XXX", where XXX is your ProgID. If you unregister your add-in and then uncheck the box in the Add-ins dialog, Excel will throw up an error dialog: "Cannot find add-in XXX. Delete from list?". Replying Yes to this prompt will delete the registry key when Excel next shuts down.
When you come to shim your add-in, your shim class might expose both IDTExtensibility2 and your custom interface. Even though you implement 2 interfaces, you rely on the AutoDual class interface to represent them as one interface to Excel. Internally, your shim class could switch between the interfaces using QI (because, of course, within your shim you know that the various methods you expose are really defined in separate interfaces. The shim class might look like this:
class ATL_NO_VTABLE CConnectProxy :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CConnectProxy, &CLSID_ConnectProxy>,
public IDispatchImpl<AddInDesignerObjects::_IDTExtensibility2,
&AddInDesignerObjects::IID__IDTExtensibility2,
&AddInDesignerObjects::LIBID_AddInDesignerObjects, 1, 0>,
public IDispatchImpl<AutomationAddin::ITemperatureConversion,
&AutomationAddin::IID_ITemperatureConversion,
&AutomationAddin::LIBID_AutomationAddin, 1, 0>
{
public:
CConnectProxy() : m_pConnect(NULL), m_pTempConv(NULL)
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_CONNECTPROXY)
BEGIN_COM_MAP(CConnectProxy)
COM_INTERFACE_ENTRY2(IDispatch, AutomationAddin::ITemperatureConversion)
COM_INTERFACE_ENTRY(AddInDesignerObjects::IDTExtensibility2)
COM_INTERFACE_ENTRY(AutomationAddin::ITemperatureConversion)
END_COM_MAP()
DECLARE_PROTECT_FINAL_CONSTRUCT()
HRESULT FinalConstruct();
void FinalRelease();
public:
//IDTExtensibility2 implementation:
STDMETHOD(OnConnection)(IDispatch * Application,
AddInDesignerObjects::ext_ConnectMode ConnectMode,
IDispatch *AddInInst, SAFEARRAY **custom)
{
return m_pConnect->OnConnection(
Application, ConnectMode, AddInInst, custom);
}
STDMETHOD(OnDisconnection)(
AddInDesignerObjects::ext_DisconnectMode RemoveMode, SAFEARRAY **custom )
{
return m_pConnect->OnDisconnection(RemoveMode, custom);
}
STDMETHOD(OnAddInsUpdate)(SAFEARRAY **custom )
{
return m_pConnect->OnAddInsUpdate(custom);
}
STDMETHOD(OnStartupComplete)(SAFEARRAY **custom )
{
return m_pConnect->OnStartupComplete(custom);
}
STDMETHOD(OnBeginShutdown)(SAFEARRAY **custom )
{
return m_pConnect->OnBeginShutdown(custom);
}
//ITemperatureConversion implementation:
STDMETHOD(Fahr2Cel)(double val, double* pRetVal)
{
return m_pTempConv->Fahr2Cel(val, pRetVal);
}
STDMETHOD(Cel2Fahr)(double val, double* pRetVal)
{
return m_pTempConv->Cel2Fahr(val, pRetVal);
}
protected:
AddInDesignerObjects::IDTExtensibility2 *m_pConnect;
AutomationAddin::ITemperatureConversion *m_pTempConv;
};
Then, you could QI to setup the correct interface pointers, probably in your FinalConstruct, like this:
HRESULT CConnectProxy::FinalConstruct()
{
HRESULT hr = S_OK;
CCLRLoader *pCLRLoader = CCLRLoader::TheInstance();
IfNullGo(pCLRLoader);
IfFailGo(pCLRLoader->CreateInstance(
AssemblyName(), ConnectClassName(),
__uuidof(IDTExtensibility2),
// __uuidof(AutomationAddin::ITemperatureConversion),
(void **)&m_pConnect));
IfFailGo(m_pConnect->QueryInterface(
__uuidof(AutomationAddin::ITemperatureConversion),
(void **)&m_pTempConv));
Error:
return hr;
}