4

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:

object browser looks right

...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:

all handlers are null in the C# code

What am I doing wrong?

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • The article you linked to says "We need the ClassInterface attribute and we need to set it to None." Does changing the attribute on `ParserState` to `[[ClassInterface(ClassInterfaceType.None)]` help? – Bradley Grainger Mar 28 '16 at 07:08
  • @RomanR. that `Handles` syntax is VB.NET... – Mathieu Guindon Mar 28 '16 at 07:17
  • 3
    I copy/pasted your declarations into new project and it worked well - Excel VBA does see the event interface and events reach back VBA. What I can think of is that somehow you have bad threading in your case, esp. if you use worker threads and pass pointers between them, and then between COM apartments making them unavailable on one threads while they are OK on other. The code snippet itself does not show supposed threading problems though. – Roman R. Mar 28 '16 at 08:42
  • @Roman the parser state is indeed being updated from a background thread. If you can write an answer explaining that COM events need to be invoked from the UI thread (I'll edit the code in the question to include the `_dispatcher` field), you get an easy rep boost! Thanks a million! – Mathieu Guindon Mar 28 '16 at 16:24

1 Answers1

2

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
Roman R.
  • 68,205
  • 6
  • 94
  • 158
  • So.. confirmed, the "simple case" works indeed. The hard part will be to raise the parser events on the UI/main thread then. – Mathieu Guindon Mar 28 '16 at 18:50
  • Wtih C++ development I normally create a worker window in original STA thread, then worker enqueues event information into internal collection and does `PostMessage` to the worker window. Message handler receives the message and processes the list to raise actual events. I am not sure what is most appropriate and/or elegant equivalent for C# (window based timer to poll instead of worker window perhaps?). What I would definitely avoid is any blocking calls from the worker thread. – Roman R. Mar 28 '16 at 18:58