1

I was given a VSTO Outlook add-in written in VBA, and have been tasked to reduce the start-up time. I'm rather new to the whole add-in and COM objects thing so I need some help.

The add-in takes anywhere from 0.2s to 2.0s to startup, and Outlook disables the plugin if the average startup time is >1000ms. Using the registry hack to force enable the add-in is unfortunately not an option. I've also tested it with an empty add-in, which also can take up to 1.8s to startup. I've searched SO and other such sites for a solution, and across one involving writing a "stub" in an un-managed language such as Delphi or C++, which does nothing but load the actual add-in. The interface that's supposed to do this is IManagedAddin.

My issue is with implementing this interface. I've created a simple add-in in VC++ The class that implements _IDTExtensibility2 is in Connect.h, shown below. However, I don't have a clue how to implement IManagedAddin::Load to load my add-in into Outlook, and there doesn't seem to be a lot of documentation on this. Any help would be much appreciated!

EDIT: Updated code below

// Connect.h : Declaration of the CConnect

#pragma once
#include "resource.h"       // main symbols
#include "NativeAddin_i.h"
#include "IManagedAddin.h"
#include <Windows.h>
#include <iostream>



#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA)
#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms."
#endif

using namespace ATL;

// CConnect

class ATL_NO_VTABLE CConnect :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CConnect, &CLSID_Connect>,
    public IDispatchImpl<IConnect, &IID_IConnect, &LIBID_PixelLLib, /*wMajor =*/ 1, /*wMinor =*/ 0>,
    public IDispatchImpl<_IDTExtensibility2, &__uuidof(_IDTExtensibility2), &LIBID_AddInDesignerObjects, /* wMajor = */ 1, /* wMinor = */ 0>
{
public:
    static Outlook::_Application* outlookApp;
    static ext_ConnectMode connectMode;
    static LPDISPATCH addInInst;
    static SAFEARRAY** customArr;
    static UINT_PTR timerId;

    CConnect()
    {
    }

DECLARE_REGISTRY_RESOURCEID(106)


BEGIN_COM_MAP(CConnect)
    COM_INTERFACE_ENTRY(IConnect)
    COM_INTERFACE_ENTRY2(IDispatch, _IDTExtensibility2)
    COM_INTERFACE_ENTRY(_IDTExtensibility2)
END_COM_MAP()



    DECLARE_PROTECT_FINAL_CONSTRUCT()

    HRESULT FinalConstruct()
    {
        return S_OK;
    }

    void FinalRelease()
    {
    }

    static VOID CALLBACK TimerCallback(HWND hWnd, UINT nMsg, UINT timerId, DWORD dwTime)
    {
        KillTimer(NULL, timerId);

        HRESULT hr;
        BSTR manifestUrl = SysAllocString(L"file://path/to/manifest.vsto");

        // load add-in
        IID clsid;
        hr = IIDFromString(OLESTR("{99D651D7-5F7C-470E-8A3B-774D5D9536AC}"), &clsid); // VSTOAddinLoader CLSID

        IID iid;
        hr = IIDFromString(OLESTR("{B9CEAB65-331C-4713-8410-DDDAF8EC191A}"), &iid);
        IManagedAddin* loader;
        hr = CoCreateInstance(clsid, NULL, CLSCTX_ALL, iid, (void**)&loader);

        hr = loader->Load(manifestUrl, outlookApp);

        _IDTExtensibility2* ext;
        hr = loader->QueryInterface(IID__IDTExtensibility2, (void**)&ext);

        hr = ext->OnConnection(outlookApp, connectMode, addInInst, customArr);

        MSO::IRibbonExtensibility* ribbon;
        hr = (???)->QueryInterface(MSO::IID_IRibbonExtensibility, (void**)&ribbon);
    }


    STDMETHOD(OnConnection)(LPDISPATCH App, ext_ConnectMode ConnectMode, LPDISPATCH AddInInst, SAFEARRAY * * custom)
    {
        HRESULT hr;
        UINT time = 200;

        Outlook::_Application* app;
        hr = App->QueryInterface(__uuidof(Outlook::_Application), (void**)&app);

        // init static members
        outlookApp = app;
        connectMode = ConnectMode;
        addInInst = AddInInst;
        customArr = custom;

        timerId = SetTimer(NULL, 0, time, (TIMERPROC)&TimerCallback);
        return S_OK;
    }

    STDMETHOD(OnDisconnection)(ext_DisconnectMode RemoveMode, SAFEARRAY * * custom)
    {
        return S_OK;
    }

    STDMETHOD(OnAddInsUpdate)(SAFEARRAY * * custom)
    {
        return S_OK;
    }

    STDMETHOD(OnStartupComplete)(SAFEARRAY * * custom)
    {
        return S_OK;
    }

    STDMETHOD(OnBeginShutdown)(SAFEARRAY * * custom)
    {
        return S_OK;
    }
};

OBJECT_ENTRY_AUTO(__uuidof(Connect), CConnect)
Outlook::_Application* CConnect::outlookApp = NULL;
ext_ConnectMode CConnect::connectMode = ext_cm_AfterStartup;
LPDISPATCH CConnect::addInInst = NULL;
SAFEARRAY** CConnect::customArr = NULL;

UINT_PTR CConnect::timerId = 0;
// IManagedAddin.h
#pragma once
#include "resource.h"
#include "NativeAddin_i.h"

struct __declspec(uuid("B9CEAB65-331C-4713-8410-DDDAF8EC191A"))
IManagedAddin : IUnknown
{
public:
    virtual STDMETHOD(Load)(BSTR bstrManifestUrl, LPDISPATCH pdisApplication) = 0;
    virtual STDMETHOD(Unload)() = 0;
};
// pch.h
#ifndef PCH_H
#define PCH_H

// add headers that you want to pre-compile here
#include "framework.h"

#import "libid:AC0714F2-3D04-11D1-AE7D-00A0C90F26F4" raw_interfaces_only, raw_native_types, named_guids, auto_search, no_namespace
#import "libid:2DF8D04C-5BFA-101B-BDE5-00AA0044DE52" raw_interfaces_only, raw_native_types, named_guids, auto_search, rename_namespace("MSO")
#import "libid:00062FFF-0000-0000-C000-000000000046" raw_interfaces_only, raw_native_types, named_guids, auto_search, rename_namespace("Outlook")

#endif //PCH_H
TF.Ryan
  • 53
  • 1
  • 4

1 Answers1

1

What I did was to wait until OnConnection callback fired and started a timer - you might be able to use a separate thread, but in my case the processing had to be done on the main thread due to some functionality that had thread affinity. OnConnection will give you Outlook.Application object.

In the timer callback (Outlook is not looking), create an instance of the IManagedAddin COM object using CoCreateInstance(CLSID_IManagedAddin, ...). Call IManagedAddin::Load. The path must be in the form file://c:/the/folder/myaddin.vsto.

QI the IManagedAddin object for IDTExtensibility2 interface and call IDTExtensibility2::OnConnection() using the parameters saved from the native OnConnection callback.

If Outlook has already called your C++ implementation of OnStartupComplete, call OnStartupComplete on the VSTO addin. If not, you can do it later.

QI for IRibbonExtensibility and call IRibbonExtensibility::GetCustomUI. Or you can hardcode the ribbon XML in the C++ addin. Note that if Outlook calls IRibbonExtensibility::GetCustomUI on your C++ addin, and you have to delegate the call to the VSTO addin before you had a chance to run your timer code, you have no choice but to call the above code immediately rather than in a timer callback since GetCustomUI cannot be postponed.

If you are using task panes (i.e. ICustomTaskPaneConsumer interface is implemented by your VSTO addin), your C++ addin must also implement it (besides the IDTExtensibility2 and ICustomTaskPaneConsumer interfaces). QI IManagedAddin for IServiceProvider interface and use it to call IServiceProvider::QueryService(GUID_NULL, IID_ICustomTaskPaneConsumer, ...). then call ICustomTaskPaneConsumer.CTPFactoryAvailable - note that ICustomTaskPaneConsumer does not come from your VSTO addin's IDTExtensibility2 interface but rather though the IServiceProvider object off the IManagedAddin interface.

Dmitry Streblechenko
  • 62,942
  • 4
  • 53
  • 78
  • Thanks for your help! I still can't seem to get the add-in to load. I'm getting `Exception thrown at 0x00007FFE2D97B8BD (Mso20win32client.dll) in OUTLOOK.EXE: 0xC0000005: Access violation writing location 0x0000000000000000.` when calling `loader->Load(manifestUrl, app)` (code above is updated). The debugger can't load symbols in `VSTOLoader.dll` so I can't step through it either. I suspect that I'm passing the wrong application object. – TF.Ryan Oct 07 '21 at 03:14
  • Where does outlookApplication come from? – Dmitry Streblechenko Oct 07 '21 at 04:05
  • Also, try to QI it for Outlook.Application instead f just a generic IDispatch. – Dmitry Streblechenko Oct 07 '21 at 05:47
  • Okay, I think I've managed to load the `.vsto` file. In your third paragraph you've mentioned to QI the "returned object" for `IDTExtensibility`, where should this object come from? `Load` doesn't return any objects.. – TF.Ryan Oct 09 '21 at 09:15
  • Yes, sorry, you need to QI for IDTExtensibility the IManagedAddin object after calling Load. – Dmitry Streblechenko Oct 09 '21 at 14:46
  • Thanks for your help thus far, think I'm close to the solution. I'm supposed to call `IDTExtensibility2::OnConnection()` on the QI'd `IManagedAddin` object using the parameters of the native add-in `OnConnection` right? It seems a bit counter-intuitive. Also, which object should I QI for `IRibbonExtensibility`? Both the `IManagedAddin` and `IDTExtensibility2` object return `E_NOINTERFACE`. Again, thanks for helping this COM newbie! – TF.Ryan Oct 14 '21 at 03:11
  • I QI `IDTExtensibility2` for `IRibbonExtensibility`. Does you VB.Net addin implement any ribbon functionality? – Dmitry Streblechenko Oct 14 '21 at 05:58