如何连接 COM 事件调度程序?

How to hook up a COM event dispatcher?

VBIDE API 公开了非常神秘的 _dispVBComponentsEvents 界面(以及其他),看起来像 我可以用来捕捉各种有趣事件的东西在 VBE 中。

所以我在 class 中实现了接口,旨在捕获事件并引发 "normal" .net 事件供我的应用程序的其余部分处理,如下所示:

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));
        }
    }
}

我希望像这样使用 class:

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));
}

但它不起作用,我没有得到调试输出,也没有命中断点。显然我不知道我在做什么。 MSDN 在这个问题上完全没用,找到这方面的文档比找到亨利八世第三任妻子的娘家姓更难。

我做错了什么,我该如何让它发挥作用?我在正确的轨道上吗?

Am I on the right track?

是的。您在 事件接收器 中拥有的内容 - 您缺少一些代码来 注册 COM 服务器的接收器。

VBProjectsVBComponents 接口实现(某处非常深)IConnectionPointContainer 接口 - 您需要使用它来收集 IConnectionPoint 实例。要取消注册接收器,您需要一个数据结构来记住注册步骤给您的 int cookie

这是一个粗略的例子 - 假设您有一个 App class 具有这些字段:

private readonly IConnectionPoint _projectsEventsConnectionPoint;
private readonly int _projectsEventsCookie;

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

在构造函数的某处,您将使用 IConnectionPoint.Advise 注册接收器,并注册您的自定义事件处理程序:

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);

然后,当添加一个项目时,您将使用 IConnectionPoint.Advise 为每个组件注册一个接收器,然后您可以注册您的自定义事件处理程序,并在您的字典中添加一个条目:

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));
}

删除项目后,使用 IConnectionPoint.Unadvise 取消注册接收器,并删除字典条目:

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);
    }
}

然后您可以 运行 在您的处理程序中添加任何您想要的代码:

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

如果您的 App class 中有一个 Dispose 方法,那将是清理任何残留物的好地方:

public void Dispose()
{
    _projectsEventsConnectionPoint.Unadvise(_projectsEventsCookie);
    foreach (var item in _componentsEventsConnectionPoints)
    {
        item.Value.Item1.Unadvise(item.Value.Item2);
    }
}

System.Runtime.InteropServices 命名空间公开静态 ComEventsHelper class to connect managed delegates to unmanaged dispatch sources. This basically does the same thing as the ,但连接点在运行时可调用包装器中处理,而不是必须从调用代码显式管理(因此使其更健壮).我怀疑这就是 PIA 在内部处理源接口的方式(对有问题的 Microsoft.Vbe.Interop 进行反编译,将其严重破坏以至于很难分辨)。

在这种情况下,出于某些深不可测的原因,所讨论的接口未声明为源接口,因此 PIA 构建未连接运行时包装器中的事件处理程序。所以...您可以在包装器 class 中手动连接处理程序并将它们作为包装事件转发,但仍然将处理连接点的繁重工作(和线程安全管理)留给 RCW。请注意,您需要来自引用类型库的 2 条信息 - _dispVBComponentsEvents 接口的 guid 和您有兴趣收听的非托管事件的 DispId

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
}

然后,将它们连接到 class 包装器的构造器中(为简洁起见,只显示了一个)...

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);
}

...并转发事件:

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)));
    }
}

完成后,un-register 委托人通过调用 ComEventsHelper.Remove:

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

上面的示例针对每个问题使用了包装器 class,但如果您需要在处理 COM 事件或将其传递给其他侦听器之前将附加功能附加到 COM 事件,则可以在任何地方使用相同的方法.