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

Comments (8)

  1. I’ve posted a few times on the best way to expose methods from an add-in to automation clients – for

  2. kevinsbennett says:

    Andrew, I have tried what this article suggests (but in VB.NET).  I have an Excel add-in which exposes a couple string properties and an event through an interface.  I am trying to communicate with this add-in from a VB.NET application.  Everything works great, except for adding a handler to the exposed event from the add-in.

    Namely, I say…

    AddHandler addInObject.SomeEvent, AddressOf addInObject_SomeEvent

    I get a "specified cast is not valid" error on the AddHandler line.  Any ideas what could be going wrong?  Thanks for any help!

  3. andreww says:

    kevin – sorry for the delay in replying, I wasn’t getting email alerts on comments. Anyway, I’m not a VB expert, but I’d say the runtime is trying to cast your addInObject to a type that implements the SomeEvent event, and failing. Did you declare addInObject as a COMAddIn.Object or as a strongly-typed object that implements the event interface?

  4. I posted a while back about exposing an automation object from an add-in that fires events . That post

  5. Jay says:

    Hi Andrew

    Our client is working on both the versions Office 2007 and office 2003.

    Can we marshal user control from Application level addin to Document level addin.

    Some thinig like this.

    public interface IAddInEvents

    {

       UserControl GetControl();

    }

    Once I get the user control in Office 2003 application I will add that user control to action pane.

    Please let me know if this is possible.

    thanks

    Jay

  6. andreww says:

    Jay – I don’t really understand what you’re trying to do. UserControl does derive ultimately from MarshalByRefObject, so I guess you could marshal it, but I seriously question the design here. Why would you want to do this? If you need to instantiate a control on a doc-level ActionsPane, why don’t you do so directly from your doc-level solution? If you need to pass property values from your add-in to your doc-level solution, you can do this without trying to marshal the entire control.

  7. dannn30 says:

    Hi Andrew.

    Thanks for this useful post.

    Just one thing, If the client is a C# app, you need to declare AddInUtilities  as ClassInterfaceType.AutoDual to support event referring.

  8. dannn30 says:

    I have some problems with the Add-In event on the client side (C#, VSTO Template project)

    I tried

    to add handler to the eventDelegate event:

     object sAddinEventName = "ExcelAddIn1";

     COMAddIn pAddin = Application.COMAddIns.Item(ref sAddinEventName);

     object objAddin = pAddin.Object;

     if (null != objAddin)

     {

        objAddin.GetType().InvokeMember("MyFunc", BindingFlags.InvokeMethod,

    null, objAddin, invokeArgs);

         EventInfo[] pAllEvents =  objAddin.GetType().GetEvents();

         EventInfo peInfo = objAddin.GetType().GetEvent("eventDelegate");

    MyEventDelegate pAddSiteEvent = new MyEventDelegate(EventCode); 
    

         //peInfo.AddEventHandler(objAddin, pAddSiteEvent);

         object[] invokeArgs = {"123"};

        objAddin.GetType().InvokeMember("MyFunc", BindingFlags.InvokeMethod,

    null, objAddin, invokeArgs);

    }

    objAddIn receive the instance to the object. InvokeMethod works!!! But

    adding event handler to the event delegate failes.

    pAllEvents displays 0 events and obviously has null value after GetEvent was

    called.

    Any idea what I am doing wrong?