Exposing Events From Non-VSTO Add-in Automation Objects

I posted a while back about exposing an automation object from an add-in that fires events. That post was couched in terms of VSTO add-ins. A customer asked recently how the same technique could be used in a non-VSTO add-in. So, that’s the topic of this post.

First, here’s my automation object and the interfaces it implements. IAddInUtilities is a regular incoming interface that defines one method, CreateCustomTaskPane. The AddInUtilities class implements this method: because the AddInUtilities class is little more than an API into the add-in’s functionality, this implementation simply invokes a corresponding method on the add-in class itself to do the actual work of creating the task pane.

IAddInEvents is the outgoing event interface, which defines one method, SomeEvent. The AddInUtilities class does not implement this interface – like any other event interface, this will be implemented by the client. Instead, the AddInUtilities class declares its support for this interface by using the ComSourceInterfaces attribute. The class then exposes a method, FireEvent, to allow the add-in class to cause the event to fire.

// The custom interface that our COMAddIn.Object implements.

[ComVisible(true)]

[InterfaceType(ComInterfaceType.InterfaceIsDual)]

[Guid("C2743EFC-AD90-47a6-B1DC-12E52C6E2FE7")]

public interface IAddInUtilities

{

    void CreateCustomTaskPane();

}

// The delegate type for our custom event.

[ComVisible(false)]

public delegate void SomeEventHandler(object sender, EventArgs e);

// Outgoing (source/event) interface.

[ComVisible(true)]

[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]

public interface IAddInEvents

{

    [DispId(1)]

    void SomeEvent(object sender, EventArgs e);

}

// The object we will expose through the COMAddIns collection.

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.None)]

[Guid("F743C9A0-DDEF-49d5-AEAA-2E6798814C23")]

[ComSourceInterfaces(typeof(IAddInEvents))]

public class AddInUtilities :

    StandardOleMarshalObject,

    IAddInUtilities

{

    private Connect addInObject;

    // Event field: this is what a COM client will hook up

    // their sink to.

    public event SomeEventHandler SomeEvent;

    internal AddInUtilities(SharedAddInEvents.Connect o)

    {

        addInObject = o;

    }

    // This is the interface method we expose to potential clients.

    public void CreateCustomTaskPane()

    {

        addInObject.CreateCustomTaskPane();

    }

    internal void FireEvent(object sender, EventArgs e)

    {

        if (SomeEvent != null)

        {

            SomeEvent(sender, e);

        }

    }

}

Next, we define a custom UserControl, which will form the basis of our custom task pane. As this is a shared add-in, not a VSTO add-in, we can’t take advantage of VSTO’s wrappers for custom task panes. So, we have to make our custom UserControl visible to COM, effectively registering it as an ActiveX control. At runtime, Office will instantiate this control via normal COM registry lookup and the CoCreateInstance mechanism. Our control will have just one button, and when the user clicks this button, we sink the Click event and fire the custom event exposed from our IAddInUtilities object.

[ComVisible(true)]

[Guid("6017B040-87CE-4bfc-88E8-18009A8EC403")]
public partial class SimpleUserControl : UserControl

{

    public Connect AddInObject;

    public SimpleUserControl()

    {

        InitializeComponent();

    }

    private void btnHello_Click(object sender, EventArgs e)

    {

        AddInObject.FireClickEvent(sender, e);

    }

}

Finally, the main add-in class – which, in a shared add-in project, is typically named Connect. This implements IDTExtensibility2 as normal, and also ICustomTaskPaneConsumer – because we want to advertise to Office that we want to create custom task panes. In the OnConnection method, we instantiate our AddInUtilities object and expose it out through the COMAddIns collection.

[Guid("CA2575D4-E119-4257-B180-2121720CF773")]

[ProgId("SharedAddInEvents.Connect")]

public class Connect :

    Object, IDTExtensibility2,

    ICustomTaskPaneConsumer

{

    private ICTPFactory factory;

    private AddInUtilities addInUtilities;

    public void OnDisconnection(

        ext_DisconnectMode disconnectMode, ref System.Array custom) {}

    public void OnAddInsUpdate(ref System.Array custom) {}

    public void OnStartupComplete(ref System.Array custom) {}

    public void OnBeginShutdown(ref System.Array custom) {}

    public void OnConnection(

        object application, ext_ConnectMode connectMode,

        object addInInst, ref System.Array custom)

    {

        addInUtilities = new AddInUtilities(this);

        COMAddIn comAddIn = (COMAddIn)addInInst;

        comAddIn.Object = addInUtilities;

    }

    public void CTPFactoryAvailable(ICTPFactory CTPFactoryInst)

    {

        factory = CTPFactoryInst;

    }

Recall that the AddInUtilities class makes a call to the CreateCustomTaskPane method in the add-in class. Here, we create a custom task pane based on our custom UserControl. When Office has created the task pane, we can extract the custom UserControl and set its add-in object property. We do this so that the control has a way to call back into the add-in class. When the user clicks the button in our task pane, the event is propagated from the UserControl to the add-in object, and we propagate it out to any clients that are listening for the event on the exposed COMAddIn.Object object (that is, in our case, the AddInUtilities object).

    internal void CreateCustomTaskPane()

    {

        try

        {

            CustomTaskPane taskPane =

                factory.CreateCTP(

                "SharedAddInEvents.SimpleUserControl",

                "My Caption", Type.Missing);

            SimpleUserControl sc =

                (SimpleUserControl)taskPane.ContentControl;

            sc.AddInObject = this;

            taskPane.Visible = true;

        }

        catch (Exception ex)

        {

            MessageBox.Show(ex.ToString());

        }

    }

    internal void FireClickEvent(object sender, EventArgs e)

    {

       addInUtilities.FireEvent(sender, e);

    }

}

When the add-in is built, and the host Office app is running, we can invoke the AddInUtilities method to create a custom task pane from any suitable automation client. When the user clicks the button in the task pane, we fire an event, which the client can sink. For example, this is how you could connect from VBA:

Public WithEvents addInUtils As SharedAddInEvents.AddinUtilities

 Private Sub CommandButton1_Click()

    Dim addin As Office.COMAddIn

    Set addin = Application.COMAddIns("SharedAddInEvents.Connect")

    Set addInUtils = addin.Object

    addInUtils.CreateCustomTaskPane

End Sub

Private Sub addInUtils_SomeEvent( _

ByVal sender As Variant, ByVal e As mscorlib.EventArgs)

    MsgBox "Got SomeEvent"

End Sub

Note, when you build the project, you should have the Register for COM Interop option checked. This registers all ComVisible types, as well as building and registering a typelib for these types. Unlike VSTO projects, the ComVisible types are not associated in the registry with the typelib, so you need to add this extra registry key for the typelib under the CLSID for the AddInUtilities object:

[HKEY_CLASSES_ROOT\<Wow6432Node>\CLSID\{F743C9A0-DDEF-49D5-AEAA-2E6798814C23}\TypeLib]
@="{45C1778F-EFB9-4d93-81EF-EE14011C133B}"

Note also that I’ve carefully added explicit GUIDs for both interfaces, the AddInUtilities class, the custom UserControl, the main add-in Connect class, and the assembly itself (in the assemblyinfo.cs). If you don’t do this, VS will give you arbitrary GUIDs when it builds the project – and you won’t have any easy way to figure out what they are.

SharedAddInEvents.zip