Exposing Events from Managed Add-in Objects

Following on from my recent posts on exposing add-in objects, here and here, it occurred to me that its sometimes useful to be able to expose events from these objects. Recall that you can expose your add-in through the COMAddIn.Object property in the Office OM, either directly (in a non-VSTO add-in) or through VSTO’s RequestComAddInAutomationService mechanism (documented here).

For example, let’s suppose you’re exposing an object from your add-in that provides a DoSomething method, like this:

namespace ComServiceOleMarshal

{

    [ComVisible(true)]

    [InterfaceType(ComInterfaceType.InterfaceIsDual)]

    public interface IAddInUtilities

    {

        void DoSomething();

    }

    [ComVisible(true)]

    [ClassInterface(ClassInterfaceType.None)]

    public class AddInUtilities :

        StandardOleMarshalObject,

        IAddInUtilities

    {

        public void DoSomething()

        {

            Globals.ThisAddIn.CreateNewTaskPane();

        }

    }

}

(Let’s assume the CreateNewTaskPane method in the ThisAddIn class does in fact create a new custom taskpane.) Then, in a document/workbook opened in the host application, you write some VBA macro code to consume this object, like this:

Private Sub CommandButton1_Click()

    Dim addin As Office.COMAddIn

    Dim addInUtils As ComServiceOleMarshal.AddinUtilities

    Set addin = Application.COMAddIns("ComServiceOleMarshal")

    Set addInUtils = addin.Object

    addInUtils.DoSomething

End Sub

So far, so good. Now, suppose you want to be able to fire events from your exposed add-in object. For example, say the custom taskpane has a button, and when the user clicks the button, you want to propagate the event out through the exposed add-in object. To achieve this, you would write an event interface which defines the events you want to expose. Then, specify that the add-in object class implements this event interface, using the ComSourceInterfaces attribute, documented here:

// 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);

}

[ComVisible(true)]

[ClassInterface(ClassInterfaceType.None)]

[ComSourceInterfaces(typeof(IAddInEvents))]

public class AddInUtilities :

    StandardOleMarshalObject,

    IAddInUtilities

{

    // Event field. This is what a COM client will hook up

    // their sink to.

    public event SomeEventHandler SomeEvent;

    public void DoSomething()

    {

        Globals.ThisAddIn.CreateNewTaskPane();

    }

    // We expose a method to allow the add-in class to

    // cause the event to fire.

    inernal void FireEvent(object sender, EventArgs e)

    {

        if (SomeEvent != null)

        {

            SomeEvent(sender, e);

        }

    }

}

The ThisAddIn class is enhanced to sink the Click event on the button, and re-fire it out through the exposed add-in object:

public partial class ThisAddIn

{

    private AddInUtilities addInUtilities;

    protected override object RequestComAddInAutomationService()

    {

        if (addInUtilities == null)

        {

            addInUtilities = new AddInUtilities();

        }

        return addInUtilities;

    }

    internal void CreateNewTaskPane()

    {

        UserControl uc = new UserControl();

        Button b = new Button();

        b.Text = "Click Me";

        b.Click += new EventHandler(b_Click);

        uc.Controls.Add(b);

        Microsoft.Office.Tools.CustomTaskPane taskPane =

            this.CustomTaskPanes.Add(uc, "New TaskPane");

        taskPane.Visible = true;

    }

    // When the user clicks the button on the taskpane,

    // we sink the Click event here, and fire the custom

    // event exposed from our IAddInUtilities object.

    internal void b_Click(object sender, EventArgs e)

    {

        addInUtilities.FireEvent(sender, e);

    }

}

Finally, the VBA client code is enhanced to use the WithEvents keyword when defining the add-in object, so that it can sink the propagated event:

Public WithEvents addInUtils As ComServiceOleMarshal.AddinUtilities

Private Sub CommandButton1_Click()

    Dim addin As Office.COMAddIn

    Set addin = Application.COMAddIns("ComServiceOleMarshal")

    Set addInUtils = addin.Object

    addInUtils.DoSomething

End Sub

Private Sub addInUtils_SomeEvent(ByVal sender As Variant, ByVal e As mscorlib.EventArgs)

    MsgBox "Got SomeEvent"

End Sub

Note: for this to work, in the VBE you need to add a reference to the add-in’s typelib, and to the typelib for mscorlib, which is typically in a path like this: C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorlib.tlb.

 

As an aside, note that Richard Cook, a developer in my team, has started a cool series of blog posts that contrast .NET delegate-based event handling with classic COM IConnectionPoint eventing in managed code.

ComServiceOleMarshal_ComEvent.zip