Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# COM server events “lost” when raising events while Excel is in edit mode

Tags:

c#

excel

vba

com

I have an in-proc COM server written in C# (using .NET Framework 3.5) that raises COM events based on this example: http://msdn.microsoft.com/en-us/library/dd8bf0x3(v=vs.90).aspx

Excel VBA is the most common client of my COM server. I’ve found that when I raise COM events while Excel is in edit mode (e.g. a cell is being edited) the event is “lost”. Meaning, the VBA event handler is never called (even after the Excel edit mode is finished) and the call to the C# event delegate passes through and fails silently with no exceptions being thrown. Does anyone know how I can detect this situation on my COM server? Or better still make the event delegate call block until Excel is out of edit mode?

I have tried:

  • Inspecting the properties of the event delegate – could not find any property to indicate that the event failed to be raised on the client.
  • Calling the event delegate directly from a worker thread and from the main thread – event not raised on the client, no exceptions thrown on the server.
  • Pushing the event delegate onto a worker thread’s Dispatcher and invoking it synchronously – event not raised on the client, no exceptions thrown on the server.
  • Pushing the event delegate onto the main thread’s Dispatcher and invoking it synchronously and asynchronously – event not raised on the client, no exceptions thrown on the server.
  • Checking the status code of the Dispatcher.BeginInvoke call (using DispatcherOperation.Status) – the status is always ends with “Completed”, and is never in “Aborted” state.
  • Creating an out-of-proc C# COM server exe and tested raising the events from there – same result, event handler never called, no exceptions.

Since I get no indication that the event was not raised on the client, I cannot handle this situation in my code.

Here is a simple test case. The C# COM server:

namespace ComServerTest
{
    public delegate void EventOneDelegate();

    // Interface
    [Guid("2B2C1A74-248D-48B0-ACB0-3EE94223BDD3"), Description("ManagerClass interface")]
    [InterfaceType(ComInterfaceType.InterfaceIsDual)]
    [ComVisible(true)]
    public interface IManagerClass
    {
        [DispId(1), Description("Describes MethodAAA")]
        String MethodAAA(String strValue);

        [DispId(2), Description("Start thread work")]
        String StartThreadWork(String strIn);

        [DispId(3), Description("Stop thread work")]
        String StopThreadWork(String strIn);
    }

    [Guid("596AEB63-33C1-4CFD-8C9F-5BEF17D4C7AC"), Description("Manager events interface")]
    [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    [ComVisible(true)]
    public interface ManagerEvents
    {
        [DispId(1), Description("Event one")]
        void EventOne();
    }

    [Guid("4D0A42CB-A950-4422-A8F0-3A714EBA3EC7"), Description("ManagerClass implementation")]
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    [ComSourceInterfaces(typeof(ManagerEvents))]
    public class ManagerClass : IManagerClass
    {
        private event EventOneDelegate EventOne;

        private System.Threading.Thread m_workerThread;
        private bool m_doWork;
        private System.Windows.Threading.Dispatcher MainThreadDispatcher = null;

        public ManagerClass()
        {
            // Assumes this is created on the main thread
            MainThreadDispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;

            m_doWork = false;
            m_workerThread = new System.Threading.Thread(DoThreadWork);
        }

        // Simple thread that raises an event every few seconds
        private void DoThreadWork()
        {
            DateTime dtStart = DateTime.Now;
            TimeSpan fiveSecs = new TimeSpan(0, 0, 5);
            while (m_doWork)
            {
                if ((DateTime.Now - dtStart) > fiveSecs)
                {
                    System.Diagnostics.Debug.Print("Raising event...");
                    try
                    {
                        if (EventOne != null)
                        {
                            // Tried calling the event delegate directly
                            EventOne();

                            // Tried synchronously invoking the event delegate from the main thread's dispatcher
                            MainThreadDispatcher.Invoke(EventOne, new object[] { });

                            // Tried asynchronously invoking the event delegate from the main thread's dispatcher
                            System.Windows.Threading.DispatcherOperation dispOp = MainThreadDispatcher.BeginInvoke(EventOne, new object[] { });

                            // Tried synchronously invoking the event delegate from the worker thread's dispatcher.
                            // Asynchronously invoking the event delegate from the worker thread's dispatcher did not work regardless of whether Excel is in edit mode or not.
                            System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke(EventOne, new object[] { });
                        }
                    }
                    catch (System.Exception ex)
                    {
                        // No exceptions were thrown when attempting to raise the event when Excel is in edit mode
                        System.Diagnostics.Debug.Print(ex.ToString());
                    }

                    dtStart = DateTime.Now;
                }
            }
        }

        // Method should be called from the main thread
        [ComVisible(true), Description("Implements MethodAAA")]
        public String MethodAAA(String strValue)
        {
            if (EventOne != null)
            {
                try
                {
                    // Tried calling the event delegate directly
                    EventOne();

                    // Tried asynchronously invoking the event delegate from the main thread's dispatcher
                    System.Windows.Threading.DispatcherOperation dispOp = System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(EventOne, new object[] { });

                    // Tried synchronously invoking the event delegate from the main thread's dispatcher
                    System.Windows.Threading.Dispatcher.CurrentDispatcher.Invoke(EventOne, new object[] { });
                }
                catch (System.Exception ex)
                {
                    // No exceptions were thrown when attempting to raise the event when Excel is in edit mode
                    System.Diagnostics.Debug.Print(ex.ToString());
                }
                return "";
            }

            return "";
        }

        [ComVisible(true), Description("Start thread work")]
        public String StartThreadWork(String strIn)
        {
            m_doWork = true;
            m_workerThread.Start();
            return "";
        }

        [ComVisible(true), Description("Stop thread work")]
        public String StopThreadWork(String strIn)
        {
            m_doWork = false;
            m_workerThread.Join();
            return "";
        }
    }
}

I register it using regasm:

%SystemRoot%\Microsoft.NET\Framework\v2.0.50727\regasm /codebase ComServerTest.dll /tlb:ComServerTest.tlb

Excel VBA client code:

Public WithEvents managerObj As ComServerTest.ManagerClass
Public g_nCounter As Long

Sub TestEventsFromWorkerThread()
    Set managerObj = New ComServerTest.ManagerClass

    Dim dtStart As Date
    dtStart = DateTime.Now

    g_nCounter = 0

    Debug.Print "Start"

    ' Starts the worker thread which will raise the EventOne event every few seconds
    managerObj.StartThreadWork ""

    Do While True
        DoEvents

        ' Loop for 20 secs
        If ((DateTime.Now - dtStart) * 24 * 60 * 60) > 20 Then
            ' Stops the worker thread
            managerObj.StopThreadWork ""
            Exit Do
        End If

    Loop

    Debug.Print "Done"

End Sub

Sub TestEventFromMainThread()
    Set managerObj = New ComServerTest.ManagerClass

    Debug.Print "Start"

    ' This call will raise the EventOne event
    managerObj.MethodAAA ""

    Debug.Print "Done"
End Sub

' EventOne handler
Private Sub managerObj_EventOne()
    Debug.Print "EventOne " & g_nCounter
    g_nCounter = g_nCounter + 1
End Sub

Edit 27/11/2014 - I've been doing some more investigation on this.

This problem also occurs for a C++ MFC Automation server that raises COM events. If I raise the COM event from the main thread when Excel is in edit mode, the event handler is never called. No errors or exceptions are thrown on the server, similar to my C# COM server. However, if I use the Global Interface Table to marshal the event sink interface from the main thread back to the main thread, then invoking the event - it will block while Excel is in edit mode. (I also used COleMessageFilter to disable the busy dialog and not responding dialogs, otherwise I'd receive the exception: RPC_E_CANTCALLOUT_INEXTERNALCALL It is illegal to call out while inside message filter.)

(let me know if you'd like to see the MFC Automation code, I'm skipping it for brevity)

Knowing that, I tried to do the same on my C# COM server. I could instantiate the Global Interface Table (using the definition from pinvoke.net) and the message filter (using the IOleMessageFilter definition from MSDN). However, the event still gets "lost" and does not block while Excel is in edit mode.

Here's how I modified my C# COM server:

namespace ComServerTest
{
    // Global Interface Table definition from pinvoke.net
    [
        ComImport,
        InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
        Guid("00000146-0000-0000-C000-000000000046")
    ]
    interface IGlobalInterfaceTable
    {
        uint RegisterInterfaceInGlobal(
                [MarshalAs(UnmanagedType.IUnknown)] object pUnk,
                [In] ref Guid riid);

        void RevokeInterfaceFromGlobal(uint dwCookie);

        [return: MarshalAs(UnmanagedType.IUnknown)]
        object GetInterfaceFromGlobal(uint dwCookie, [In] ref Guid riid);
    }

    [
        ComImport,
        Guid("00000323-0000-0000-C000-000000000046") // CLSID_StdGlobalInterfaceTable
    ]
    class StdGlobalInterfaceTable /* : IGlobalInterfaceTable */
    {
    }

    public class ManagerClass : IManagerClass
    {
        //...skipped code already mentioned in earlier sample above...
        //...also skipped the message filter code for brevity...
        private Guid IID_IDispatch = new Guid("00020400-0000-0000-C000-000000000046");
        private IGlobalInterfaceTable m_GIT = null;

        public ManagerClass()
        {
            //...skipped code already mentioned in earlier sample above...
            m_GIT = (IGlobalInterfaceTable)new StdGlobalInterfaceTable();
        }

        public void FireEventOne()
        {
            // Using the GIT to marshal the (event?) interface from the main thread back to the main thread (like the MFC Automation server).
            // Should we be marshalling the ManagerEvents interface pointer instead?  How do we get at it?
            uint uCookie = m_GIT.RegisterInterfaceInGlobal(this, ref IID_IDispatch);
            ManagerClass mgr = (ManagerClass)m_GIT.GetInterfaceFromGlobal(uCookie, ref IID_IDispatch);
            mgr.EventOne(); // when Excel is in edit mode, event handler is never called and does not block, event is "lost"
            m_GIT.RevokeInterfaceFromGlobal(uCookie);
        }
    }
}

I’d like my C# COM server to behave in a similar way to the MFC Automation server. Is this possible? I think I should be registering the ManagerEvents interface pointer in the GIT but I don't know how to get at it? I tried using Marshal.GetComInterfaceForObject(this, typeof(ManagerEvents)) but that just throws an exception: System.InvalidCastException: Specified cast is not valid.

like image 566
JasonF Avatar asked May 22 '14 05:05

JasonF


People also ask

What C is used for?

C programming language is a machine-independent programming language that is mainly used to create many types of applications and operating systems such as Windows, and other complicated programs such as the Oracle database, Git, Python interpreter, and games and is considered a programming foundation in the process of ...

What is the full name of C?

In the real sense it has no meaning or full form. It was developed by Dennis Ritchie and Ken Thompson at AT&T bell Lab. First, they used to call it as B language then later they made some improvement into it and renamed it as C and its superscript as C++ which was invented by Dr.

What is C in C language?

What is C? C is a general-purpose programming language created by Dennis Ritchie at the Bell Laboratories in 1972. It is a very popular language, despite being old. C is strongly associated with UNIX, as it was developed to write the UNIX operating system.

Is C language easy?

C is a general-purpose language that most programmers learn before moving on to more complex languages. From Unix and Windows to Tic Tac Toe and Photoshop, several of the most commonly used applications today have been built on C. It is easy to learn because: A simple syntax with only 32 keywords.


1 Answers

You should use a callback method instead of Event

1- Change the interface:

// Interface
[Guid("2B2C1A74-248D-48B0-ACB0-3EE94223BDD3"), Description("ManagerClass interface")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
[ComVisible(true)]
public interface IManagerClass
{
    [DispId(1), Description("Describes MethodAAA")]
    String MethodAAA(String strValue);

    [DispId(2), Description("Start thread work")]
    String StartThreadWork(String strIn, [MarshalAs(UnmanagedType.FunctionPtr)] ref Action callback);

    [DispId(3), Description("Stop thread work")]
    String StopThreadWork(String strIn);
}

2- Add a field to hold the callback method and change the caller method:

    [ComVisible(false)]
    Action callBack;

    // Simple thread that raises an event every few seconds
    private void DoThreadWork()
    {
        DateTime dtStart = DateTime.Now;
        TimeSpan fiveSecs = new TimeSpan(0, 0, 5);
        while (m_doWork)
        {
            if ((DateTime.Now - dtStart) > fiveSecs)
            {
                System.Diagnostics.Debug.Print("Raising event...");
                try
                {
                    if (callBack != null)
                        callBack();
                }
                catch (System.Exception ex)
                {
                    // No exceptions were thrown when attempting to raise the event when Excel is in edit mode
                    System.Diagnostics.Debug.Print(ex.ToString());
                }

                dtStart = DateTime.Now;
            }
        }
    }

    [ComVisible(true), Description("Start thread work")]
    public String StartThreadWork(String strIn, [MarshalAs(UnmanagedType.FunctionPtr)] ref Action callback)
    {
        this.callBack = callback;
        m_doWork = true;
        m_workerThread.Start();
        return "";
    }

3- Add a module to your VBA (because AddressOf will work only on module SUBs) and place this method inside that module

Dim g_nCounter As Integer

Public Sub callback()
    Debug.Print "EventOne " & g_nCounter
    g_nCounter = g_nCounter + 1
End Sub

4- Pass the address of this newly created SUB to your managed method:

managerObj.StartThreadWork "", AddressOf Module1.callback
like image 75
Alireza Avatar answered Oct 11 '22 10:10

Alireza