为什么COM事件处理程序总是为空?

4

使用这篇文章,我已经设置好了这个COM可见接口来定义我的事件:

[ComVisible(true)]
[Guid("3D8EAA28-8983-44D5-83AF-2EEC4C363079")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IParserStateEvents
{
    void OnParsed();
    void OnReady();
    void OnError();
}

这些事件的触发是由实现了此接口的类来完成的:

[ComVisible(true)]
public interface IParserState
{
    void Initialize(VBE vbe);

    void Parse();
    void BeginParse();

    Declaration[] AllDeclarations { get; }
    Declaration[] UserDeclarations { get; }
}

这是实现方式:

[ComVisible(true)]
[Guid(ClassId)]
[ProgId(ProgId)]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComDefaultInterface(typeof(IParserState))]
[ComSourceInterfaces(typeof(IParserStateEvents))]
[EditorBrowsable(EditorBrowsableState.Always)]
public class ParserState : IParserState
{
    //...
    public event Action OnParsed;
    public event Action OnReady;
    public event Action OnError;

    private void _state_StateChanged(object sender, System.EventArgs e)
    {
        var errorHandler = OnError; // always null
        if (_state.Status == Parsing.VBA.ParserState.Error && errorHandler != null)
        {
            errorHandler.Invoke();
        }

        var parsedHandler = OnParsed; // always null
        if (_state.Status == Parsing.VBA.ParserState.Parsed && parsedHandler != null)
        {
            parsedHandler.Invoke();
        }

        var readyHandler = OnReady; // always null
        if (_state.Status == Parsing.VBA.ParserState.Ready && readyHandler != null)
        {
            readyHandler.Invoke();
        }
    }
    //...

_state_StateChanged处理程序响应来自后台工作线程引发的事件。


COM客户端代码是一个类似于以下内容的VBA类:

Private WithEvents state As Rubberduck.ParserState

Public Sub Initialize()
    Set state = New Rubberduck.ParserState
    state.Initialize Application.vbe
    state.BeginParse
End Sub

Private Sub state_OnError()
    Debug.Print "error"
End Sub

Private Sub state_OnParsed()
    Debug.Print "parsed"
End Sub

Private Sub state_OnReady()
    Debug.Print "ready"
End Sub

尽管从 对象浏览器 看起来一切正常:

object browser looks right

但当 VBA 代码调用 BeginParse 时,断点会命中 C# 代码,但所有处理程序都为 null,因此 VBA 处理程序不会运行:

all handlers are null in the C# code

我做错了什么?


你链接的文章说:“我们需要ClassInterface属性,并且我们需要将其设置为None。” 将ParserState上的属性更改为[[ClassInterface(ClassInterfaceType.None)]有帮助吗? - Bradley Grainger
@RomanR. 那个 Handles 语法是 VB.NET 的... - Mathieu Guindon
3
我将您的声明复制粘贴到新项目中,它运行得很好 - Excel VBA 确实看到了事件接口,并且事件可被传回 VBA。我想的是,在您的情况下,某种方式出现了错误的线程处理,特别是如果您使用工作线程并在它们之间传递指针,然后在 COM 组件之间传递它们,使它们在一个线程上不可用而在其他线程上正常。代码片段本身并未显示出预期的线程问题。 - Roman R.
@Roman,解析器状态确实是从后台线程更新的。如果您能编写一个答案来解释COM事件需要从UI线程调用(我将编辑问题中的代码以包括“_dispatcher”字段),那么您将获得轻松的声望提升!非常感谢! - Mathieu Guindon
1个回答

2

您的COM/VBA集成基本正确,但是需要记住COM线程模型和在单线程公寓中使用COM类的规则。

您已经在STA线程上创建了Rubberduck.ParserState实例。VBA立即看到WithEvents说明符,并尽力将事件处理程序连接到由COM类实现的连接点。具体来说,COM类接收COM接口指针以接受在相同线程上的事件调用,并存储该指针以在事件调用时稍后使用。

当您引发事件时,服务器(C#)和客户端(VBA)都可能检查执行是否在适当的线程上(更准确地说,在适当的公寓中)。使用C++开发,您可能有机会忽略线程不匹配(这不是一件好事,但让我们假设您知道自己在做什么),而像VBA和.NET COM互操作这样的环境会更加严格,试图照顾整个环境的完整性,如果线程错误,则很可能失败。也就是说,您必须在正确的线程上引发事件!如果您有一个后台工作线程,则不能直接从其中引发事件,而需要先将其传递到实际期望调用的公寓线程。

如果您的线程问题仅限于从工作线程调用,则问题可能是非空事件接收器调用,您会收到异常或否则不会达到VBA。然而,您现在是null,因此很可能线程以另一种方式影响(例如,在工作线程上某个回调函数的实例化等)。无论哪种方式,一旦违反了不在公寓之间传递接口指针的COM规则,这些指针就变得无法使用,导致调用失败或无法提供预期的转换等)。修复后,您将使事件正常工作。

额外代码:最简单形式的C#项目和XLS文件,证明事件可以正常工作(Subversion/Trac)。

事件直接从Initialize调用中引发:

public void Initialize()
{
    if (OnReady != null)
        OnReady();
}

Private Sub Worksheet_Activate()
    If state Is Nothing Then Set state = New ComEvents01.ParserState
    ' Initialize below will have C# raise an event we'd receive state_OnReady
    state.Initialize
End Sub

Private Sub state_OnReady()
    ' We do reach here from Initialize and Worksheet_Activate
End Sub

所以..确认了,“简单情况”确实有效。难点将是在UI/主线程上引发解析器事件。 - Mathieu Guindon
在C++开发中,我通常会在原始STA线程中创建一个工作窗口,然后工作线程将事件信息排队到内部集合中,并使用PostMessage发送到工作窗口。消息处理程序接收消息并处理列表以引发实际事件。我不确定C#中最适当和/或优雅的等效方式是什么(也许是基于窗口的定时器来轮询?)。我绝对要避免的是从工作线程进行任何阻塞调用。 - Roman R.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接