Search code examples
c#comevent-handlingdispatch

How to hook up a COM event dispatcher?


The VBIDE API exposes the wonderfully cryptic _dispVBComponentsEvents interface (among others), which look like something that I could use to capture various interesting events in the VBE.

So I implemented the interface in a class that intends to capture the event and raise a "normal" .net event for the rest of my application to handle, like this:

public class VBComponentsEventDispatcher : _dispVBComponentsEvents
{
    public event EventHandler<DispatcherEventArgs<VBComponent>> ComponentAdded;
    public void ItemAdded(VBComponent VBComponent)
    {
        OnDispatch(ComponentAdded, VBComponent);
    }

    public event EventHandler<DispatcherEventArgs<VBComponent>> ComponentRemoved;
    public void ItemRemoved(VBComponent VBComponent)
    {
        OnDispatch(ComponentRemoved, VBComponent);
    }

    public event EventHandler<DispatcherRenamedEventArgs<VBComponent>> ComponentRenamed;
    public void ItemRenamed(VBComponent VBComponent, string OldName)
    {
        var handler = ComponentRenamed;
        if (handler != null)
        {
            handler.Invoke(this, new DispatcherRenamedEventArgs<VBComponent>(VBComponent, OldName));
        }
    }

    public event EventHandler<DispatcherEventArgs<VBComponent>> ComponentSelected;
    public void ItemSelected(VBComponent VBComponent)
    {
        OnDispatch(ComponentSelected, VBComponent);
    }

    public event EventHandler<DispatcherEventArgs<VBComponent>> ComponentActivated;
    public void ItemActivated(VBComponent VBComponent)
    {
        OnDispatch(ComponentActivated, VBComponent);
    }

    public event EventHandler<DispatcherEventArgs<VBComponent>> ComponentReloaded;
    public void ItemReloaded(VBComponent VBComponent)
    {
        OnDispatch(ComponentReloaded, VBComponent);
    }

    private void OnDispatch(EventHandler<DispatcherEventArgs<VBComponent>> dispatched, VBComponent component)
    {
        var handler = dispatched;
        if (handler != null)
        {
            handler.Invoke(this, new DispatcherEventArgs<VBComponent>(component));
        }
    }
}

I'm hoping to use the class like this:

var componentsEvents = new VBComponentsEventDispatcher();
componentsEvents.ComponentAdded += componentsEvents_ComponentAdded;
componentsEvents.ComponentActivated += componentsEvents_ComponentActivated;
//...
void componentsEvents_ComponentAdded(object sender, DispatcherEventArgs<VBComponent> e)
{
    Debug.WriteLine(string.Format("Component '{0}' was added.", e.Item.Name));
}

void componentsEvents_ComponentActivated(object sender, DispatcherEventArgs<VBComponent> e)
{
    Debug.WriteLine(string.Format("Component '{0}' was activated.", e.Item.Name));
}

But it doesn't work, I get no debug output and a breakpoint isn't hit. Clearly I don't know what I'm doing. MSDN is completely useless on the subject, and finding documentation about this is harder than finding the maiden name of the third wife of Henry VIII.

What am I doing wrong, and how do I get this to work? Am I on the right track?


Solution

  • The System.Runtime.InteropServices namespace exposes a static ComEventsHelper class to connect managed delegates to unmanaged dispatch sources. This basically does the same thing as the other answer, but the connection points are handled within the runtime callable wrapper instead of having to be managed explicitly from the calling code (thus making it somewhat more robust). I suspect that this is how PIAs are handling source interfaces internally (decompiling the Microsoft.Vbe.Interop in question mangled it enough that it's hard to tell).

    In this case, for some unfathomable reason the interface in question isn't declared as a source interface, so the PIA build didn't connect the event handlers in the runtime wrapper. So... you can wire up the handlers manually in the wrapper class and forward them as wrapped events, but still leave the heavy lifting (and thread safety management) of dealing with the connection points to the RCW. Note that you need 2 pieces of information from the referenced type library - the guid of the _dispVBComponentsEvents interface and the DispId's of the unmanaged events that you're interested in listening to:

    private static readonly Guid VBComponentsEventsGuid = new Guid("0002E116-0000-0000-C000-000000000046");
    
    private enum ComponentEventDispId
    {
        ItemAdded = 1,
        ItemRemoved = 2,
        ItemRenamed = 3,
        ItemSelected = 4,
        ItemActivated = 5,
        ItemReloaded = 6
    }
    

    Then, wire them up in the ctor of the class wrapper (only one shown for the sake of brevity)...

    private delegate void ItemAddedDelegate(VB.VBComponent vbComponent);
    private readonly ItemAddedDelegate _componentAdded;
    
    public VBComponents(VB.VBComponents target) 
    {
        _target = target;
        _componentAdded = OnComponentAdded;
        ComEventsHelper.Combine(_target, 
                                VBComponentsEventsGuid, 
                                (int)ComponentEventDispId.ItemAdded, 
                               _componentAdded);
    }
    

    ...and forward the events:

    public event EventHandler<DispatcherEventArgs<IVBComponent>> ComponentAdded;
    private void OnComponentAdded(VB.VBComponent vbComponent)
    {
        OnDispatch(ComponentAdded, VBComponent);
    }
    
    private void OnDispatch(EventHandler<DispatcherEventArgs<IVBComponent>> dispatched, VB.VBComponent component)
    {
        var handler = dispatched;
        if (handler != null)
        {
            handler.Invoke(this, new DispatcherEventArgs<IVBComponent>(new VBComponent(component)));
        }
    }
    

    When you're done, un-register the delegate by calling ComEventsHelper.Remove:

    ComEventsHelper.Remove(_target, 
                           VBComponentsEventsGuid,
                           (int)ComponentEventDispId.ItemAdded,
                           _componentAdded);
    

    The example above uses a wrapper class per the question, but the same method could be used from anywhere if you need to attach additional functionality to a COM event before handling it or passing it on to other listeners.