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
- Anonymous
December 01, 2008
I’ve posted a few times on the best way to expose methods from an add-in to automation clients – for - Anonymous
January 21, 2009
The comment has been removed - Anonymous
February 26, 2009
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? - Anonymous
March 01, 2009
I posted a while back about exposing an automation object from an add-in that fires events . That post - Anonymous
May 17, 2009
Hi AndrewOur 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.thanksJay - Anonymous
May 20, 2009
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. - Anonymous
July 30, 2009
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. - Anonymous
August 08, 2009
I have some problems with the Add-In event on the client side (C#, VSTO Template project)I triedto 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");
//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!!! Butadding event handler to the event delegate failes.pAllEvents displays 0 events and obviously has null value after GetEvent wascalled.Any idea what I am doing wrong?MyEventDelegate pAddSiteEvent = new MyEventDelegate(EventCode);