简体   繁体   中英

Why are COM event handlers always null?

Using this article , I've set up this COM-visible interface to define my events:

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

The events are meant to be fired by a class that implements this interface:

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

    void Parse();
    void BeginParse();

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

Here's the implementation:

[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();
        }
    }
    //...

The _state_StateChanged handler is responding to events raised from a background worker thread.


The COM client code is a VBA class looking like this:

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

While everything looks right from the Object Browser :

对象浏览器看起来正确

...when the VBA code calls BeginParse , breakpoints get hit in the C# code, but all handlers are null , and so the VBA handlers don't run:

所有处理程序在C#代码中都为空

What am I doing wrong?

Your COM/VBA integration is about right, however you need to keep in mind COM threading model and rules of using your COM class in single threaded apartment.

You have your instance of Rubberduck.ParserState created on STA thread. VBA immediately sees WithEvents specifier and does its best connecting event handlers to the connection point implemented by COM class. Specifically, COM class receives COM interface pointers to accept event calls on the same thread and stores the pointer to use it later at event invocation time.

When you raise the event, both server (C#) and client (VBA) might or might not check if execution takes place on proper thread (rather, proper apartment). With C++ development you might have a chance to ignore threading mismatch (it is not a good thing but let's assume you know what you're doing), and environments like VBA and .NET COM interop are stricter trying to take care of integrity of environment overall and they are likely to fail if threading is wrong. That is, you have to raise your event on the right thread! If you have a background worker thread, you cannot raise event from it directly and you need to pass it first to the apartment thread where the call is actually expected.

If your threading issue were limited to invocation from worker thread, the problem would rather be non-null event sinks calling which you get an exception or otheriwse the call not reaching your VBA. You have then null however, so it is likely that threading affects in another way (instantiation from certain callback on a worker thread etc.) Either way, once you violate COM rule of not passing interface pointer between apartments, the pointers become unusable, causing failures on calls or being unable to provide expected cast and so on). Having it fixed you will have the events working.

Bonus code: minimal C# project and XLS file proving the events work fine in simplest form ( Subversion / Trac ).

Event is raised right from Initialize call:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM