How to hook up a COM event dispatcher?

2019-07-02 11:58发布

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?

2条回答
爷的心禁止访问
2楼-- · 2019-07-02 12:23

Am I on the right track?

Yes. What you have in an event sink - you're missing a bit of code to register the sink with the COM servers.

The VBProjects and VBComponents interfaces implement (somewhere very deep) the IConnectionPointContainer interface - you need to use that to collect IConnectionPoint instances. And to un-register the sink, you'll need a data structure to remember the int cookie that the registration step gives you.

Here's a rough example - say you have an App class with these fields:

private readonly IConnectionPoint _projectsEventsConnectionPoint;
private readonly int _projectsEventsCookie;

private readonly IDictionary<VBComponents, Tuple<IConnectionPoint, int>>  _componentsEventsConnectionPoints = 
    new Dictionary<VBComponents, Tuple<IConnectionPoint, int>>(); 

Somewhere in the constructor, you'll register the sink using IConnectionPoint.Advise, and register your custom event handlers:

var sink = new VBProjectsEventsSink();
var connectionPointContainer = (IConnectionPointContainer)_vbe.VBProjects;
Guid interfaceId = typeof (_dispVBProjectsEvents).GUID;
connectionPointContainer.FindConnectionPoint(ref interfaceId, out _projectsEventsConnectionPoint);

sink.ProjectAdded += sink_ProjectAdded;
sink.ProjectRemoved += sink_ProjectRemoved;
sink.ProjectActivated += sink_ProjectActivated;
sink.ProjectRenamed += sink_ProjectRenamed;

_projectsEventsConnectionPoint.Advise(sink, out _projectsEventsCookie);

Then, when a project is added, you'll register a sink for each component using IConnectionPoint.Advise, then you can register your custom event handlers, and add an entry to your dictionary:

void sink_ProjectAdded(object sender, DispatcherEventArgs<VBProject> e)
{
    var connectionPointContainer = (IConnectionPointContainer)e.Item.VBComponents;
    Guid interfaceId = typeof(_dispVBComponentsEvents).GUID;

    IConnectionPoint connectionPoint;
    connectionPointContainer.FindConnectionPoint(ref interfaceId, out connectionPoint);

    var sink = new VBComponentsEventsSink();
    sink.ComponentActivated += sink_ComponentActivated;
    sink.ComponentAdded += sink_ComponentAdded;
    sink.ComponentReloaded += sink_ComponentReloaded;
    sink.ComponentRemoved += sink_ComponentRemoved;
    sink.ComponentRenamed += sink_ComponentRenamed;
    sink.ComponentSelected += sink_ComponentSelected;

    int cookie;
    connectionPoint.Advise(sink, out cookie);

    _componentsEventsConnectionPoints.Add(e.Item.VBComponents, Tuple.Create(connectionPoint, cookie));
}

When a project is removed, you un-register the sinks using IConnectionPoint.Unadvise, and remove the dictionary entry:

void sink_ProjectRemoved(object sender, DispatcherEventArgs<VBProject> e)
{
    Tuple<IConnectionPoint, int> value;
    if (_componentsEventsConnectionPoints.TryGetValue(e.Item.VBComponents, out value))
    {
        value.Item1.Unadvise(value.Item2);
        _componentsEventsConnectionPoints.Remove(e.Item.VBComponents);
    }
}

And then you can run any code you want in your handler:

void sink_ComponentAdded(object sender, DispatcherEventArgs<VBComponent> e)
{
    _parser.State.OnParseRequested(e.Item);
}

If you have a Dispose method in your App class, that would be a good place to clean up any remnants:

public void Dispose()
{
    _projectsEventsConnectionPoint.Unadvise(_projectsEventsCookie);
    foreach (var item in _componentsEventsConnectionPoints)
    {
        item.Value.Item1.Unadvise(item.Value.Item2);
    }
}
查看更多
来,给爷笑一个
3楼-- · 2019-07-02 12:25

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.

查看更多
登录 后发表回答