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.