1

I'm creating a .NET component that I want to use to simplify working with an application that supports a COM Automation interface. One of the features of the component is that it will display dialogs to collect information from the user. However, in the workflow that I need to use, the dialog is partially displayed and then it freezes.

I've created a simplified version of what I'm doing using Excel. I'm actually using another application with a COM Automation interface but the problem is a general problem and is reproducible using Excel, which most people should have. Here's a brief description of the various pieces. I'm writing this in Visual Basic but assume the same problem will occurr with C# although I haven't tested that.

.NET Class Library project "MyCmd"

Class MyDef

When an instance of the MyDef class is created, it gets and saves the currently active Excel sheet. It also listens to the SelectionChange event of the Excel Sheet object. In response to the SelectionChange event, it creates an instance of the "MyCmd" class (described below) and fires a custom CommandCreated event, passing the created MyCmd object.

It also supports a public method called ForceEvent which does the same thing as the SelectionChange event. This is used to test the functionality using a different code path.

Imports Microsoft.Office.Interop.Excel

Public Class MyDef
    Private WithEvents _sheet As Microsoft.Office.Interop.Excel.Worksheet
    Public Event CommandCreated(ByVal command As MyCmd)

    Public Sub New()
        _sheet = g_app.ActiveSheet
    End Sub

    Public Sub ForceEvent()
        Dim cmd As New MyCmd()
        RaiseEvent CommandCreated(cmd)
    End Sub

    Private Sub _sheet_SelectionChange(Target As Range) Handles _sheet.SelectionChange
        Me.ForceEvent()
    End Sub
End Class

Class MyCmd

A class that contains a single method that creates a new instance of a dialog and displays it.

Public Class MyCmd
    Private _form As Form = Nothing

    Public Sub ShowDialog()
        If _form Is Nothing Then
            _form = New CommandDialog
        End If

        _form.Visible = True
    End Sub
End Class

The dialog I'm testing with is a form (named "CommandDialog") with a couple of buttons and no code behind them. I'm just testing to see if the form is displayed as expected.

Globals Module

I'm also using the code below to connect to Excel and get the Excel Application object. It works as expected.

Public Module Globals
    Private _app As Microsoft.Office.Interop.Excel.Application = Nothing

    Public Function g_app() As Object
        If Not _app Is Nothing Then
            Return _app
        Else
            Try
                _app = GetObject(, "Excel.Application")
            Catch ex As Exception
                MsgBox("Excel must be running")
                Return Nothing
            End Try
            Return _app
        End If
    End Function
End Module

.NET Class Library project "MyCmd"

I also have another project called "CommandFramework" that is using the MyCmd class library. This second project is a Windows Form App that contains two buttons and an event handler for the CommandCreated event supported by MyDef. The entire code is below.

When the first button is clicked, it creates an instance of the MyDef class, which because of the WithEvent declaration, also sets up an event handler for the CommandCreated event. Clicking the second button calls the ForceEvent method which results in firing the CommandCreated event and displays the dialog. I can also click in a cell in the active Excel sheet and that causes the SelectionChange event to be fired and results in the ForceEvent method to be called. However, in this case, the dialog is only partially displayed and is frozen. The code flow is identical except for the trigger which is either clicking the second button or receiving the Excel SelectionChange event. The second case causes the freeze.

Public Class CommandFramework
    Private WithEvents _cmdDef As MyCommand.MyDef = Nothing

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        _cmdDef = New MyCommand.MyDef
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        _cmdDef.ForceEvent()
    End Sub

    Private Sub cmdDef_CommandCreated(command As MyCommand.MyCmd) Handles _cmdDef.CommandCreated
        command.ShowDialog()
    End Sub
End Class

Does anyone have any ideas what the cause of the problem is and what a solution might be? My guess is it's some kind of threading issue and there's a deadlock but I don't know enough about that area to even begin diagnosing or trying something else.

Brian Ekins
  • 137
  • 9
  • 1
    The Excel application events arrive on a secondary MTA thread. I suspect the issue is due to creating a new UI element on this secondary thread that does not have a message pump. – TnTinMn Jul 16 '18 at 22:16
  • 1
    You can't get anywhere until you can debug it. From your .NET project, use Project > Properties > Debug, select "Start external program" and pick Excel.exe. Set a breakpoint on Sub New to ensure the debugger is doing its job. When it hangs use Debug > Break All and Debug > Windows > Threads to pick the right thread and worry sufficiently about your code running on the wrong one. The stack trace is essential info to get help. – Hans Passant Jul 16 '18 at 22:18
  • @HansPassant Thank you for the input. I didn't know about the "Break All" and then debugging threads. When I go through the code path that works I end up with just the single main thread. However with the code path where the form freezes there is an additional "Worker Thread". For that thread, the only information it gives me is the ID, Managed ID and Category. For the name is says "" and for the Location says "". This seems to confirm that it is a thread issue. Any suggestions on what to try next? – Brian Ekins Jul 16 '18 at 22:33
  • @TnTinMn That sounds like a reasonable answer. Do you have any suggestions of how to somehow end up handling that event on the main thread? – Brian Ekins Jul 16 '18 at 22:33
  • No, you are just seeing a thread that is no longer executing code, common in .NET. You are ignoring my recommendation to show us the call stack.at your peril, good luck with it. – Hans Passant Jul 16 '18 at 22:41
  • 1
    @BrianEkins, If the threading is the issue, see: [Making Thread-Safe Calls to Windows Forms Controls](https://learn.microsoft.com/en-us/dotnet/framework/winforms/controls/how-to-make-thread-safe-calls-to-windows-forms-controls#making-thread-safe-calls-to-windows-forms-controls) for information on the invoke pattern. You may also want to read about the [ISynchronizeInvoke Interface](https://msdn.microsoft.com/en-us/library/system.componentmodel.isynchronizeinvoke(v=vs.110).aspx) that is implemented by WinForm controls. – TnTinMn Jul 16 '18 at 23:28
  • @HansPassant, I didn't mean to ignore any advice and appreciate any help. My Call Stack is empty. – Brian Ekins Jul 17 '18 at 00:36
  • @TnTinMn, thank you for the link. I'll take a look. – Brian Ekins Jul 17 '18 at 00:37

1 Answers1

1

Try inheriting all your classes which handle COM event from StandardOleMarshalObject:

Public Class MyDef
    Inherits System.Runtime.InteropServices.StandardOleMarshalObject

...

Public Class CommandFramework
    Inherits System.Runtime.InteropServices.StandardOleMarshalObject

That will make sure your receive event callbacks on the same thread your .NET objects have been initially created on (presumably, the main STA thread). See if that helps.

Note it still might be prone to deadlocks or re-entrancy issues which are typical with COM marshalling, particularly if you call another Excel API from your Excel event handler. Ideally, you should return from the event handler as soon as possible, and perhaps handle asynchronously whatever needs to be done, with something like SynchronizationContext.Current.Post() or await Task.Yield(). With asynchronous scenarios though, you should still care about possible reentrancy, e.g. when your event handler gets invoked again while you're still handling the previous invocation.

I have some related q/a:

noseratio
  • 59,932
  • 34
  • 208
  • 486