C# COM 服务器事件 “lost” 在 Excel 处于编辑模式时引发事件

标签 c# excel com vba

我有一个用 C# 编写的进程内 COM 服务器(使用 .NET Framework 3.5),它基于此示例引发 COM 事件: http://msdn.microsoft.com/en-us/library/dd8bf0x3(v=vs.90).aspx

Excel VBA 是我的 COM 服务器最常用的客户端。我发现当我在 Excel 处于编辑模式时引发 COM 事件(例如,正在编辑一个单元格)时,事件“丢失”了。这意味着,永远不会调用 VBA 事件处理程序(即使在 Excel 编辑模式完成后),并且对 C# 事件委托(delegate)的调用会通过并静默失败,不会引发任何异常。有谁知道如何在我的 COM 服务器上检测到这种情况?或者更好的办法是让事件委托(delegate)调用阻塞,直到 Excel 退出编辑模式?

我试过:

  • 正在检查事件委托(delegate)的属性 – 找不到任何表明事件未能在客户端引发的属性。
  • 直接从工作线程和主线程调用事件委托(delegate) – 事件未在客户端引发,服务器未抛出异常。
  • 将事件委托(delegate)推送到工作线程的 Dispatcher并同步调用它——事件不会在客户端引发,服务器不会抛出异常。
  • 将事件委托(delegate)推送到主线程的 Dispatcher 并以同步和异步方式调用它 - 事件不会在客户端引发,服务器不会抛出异常。
  • 检查 Dispatcher.BeginInvoke 调用的状态代码(使用 DispatcherOperation.Status)——状态总是以“已完成”结束,永远不会处于“中止”状态。
  • 创建一个进程外的 C# COM 服务器 exe 并测试从那里引发事件 – 结果相同,事件处理程序从未调用,没有异常(exception)。

由于没有迹象表明客户端未引发该事件,因此我无法在我的代码中处理这种情况。

这是一个简单的测试用例。 C# COM 服务器:

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 "";
        }
    }
}

我使用 regasm 注册它:

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

Excel VBA 客户端代码:

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

编辑 27/11/2014 - 我一直在对此进行更多调查。

引发 COM 事件的 C++ MFC 自动化服务器会出现此问题。如果我在 Excel 处于编辑模式时从主线程引发 COM 事件,则永远不会调用事件处理程序。服务器上不会抛出任何错误或异常,类似于我的 C# COM 服务器。 但是,如果我使用全局接口(interface)表将事件接收器接口(interface)从主线程返回编码到主线程,然后调用事件 - 它会在 Excel 运行时阻塞在编辑模式。 (我还使用 COleMessageFilter 来禁用繁忙的对话框和不响应的对话框,否则我会收到异常:RPC_E_CANTCALLOUT_INEXTERNALCALL 在消息过滤器内部调用是非法的。)

(如果您想查看 MFC 自动化代码,请告诉我,为简洁起见,我将跳过它)

知道这一点后,我尝试在我的 C# COM 服务器上执行相同的操作。我可以实例化全局接口(interface)表(使用来自 pinvoke.net 的定义)和消息过滤器(使用来自 MSDN 的 IOleMessageFilter 定义)。但是,当 Excel 处于编辑模式时,该事件仍然会“丢失”并且不会阻止。

以下是我修改 C# COM 服务器的方式:

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);
        }
    }
}

我希望我的 C# COM 服务器以类似于 MFC 自动化服务器的方式运行。这可能吗?我想我应该在 GIT 中注册 ManagerEvents 接口(interface)指针,但我不知道如何获取它?我尝试使用 Marshal.GetComInterfaceForObject(this, typeof(ManagerEvents)) 但这只会引发异常:System.InvalidCastException: Specified cast is not valid。

最佳答案

你应该使用回调方法而不是事件

1- 改变界面:

// 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- 添加一个字段来保存回调方法并更改调用者方法:

    [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- 向您的 VBA 添加一个模块(因为 AddressOf 仅适用于模块 SUB)并将此方法放入该模块

Dim g_nCounter As Integer

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

4- 将这个新创建的 SUB 的地址传递给您的托管方法:

managerObj.StartThreadWork "", AddressOf Module1.callback

关于C# COM 服务器事件 “lost” 在 Excel 处于编辑模式时引发事件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23798443/

相关文章:

c# - ConfigurationManager Save() 想要创建一个 tmp 文件

excel - 在 Excel 工作表中搜索文本并将单元格范围提取到表格中

excel - 所有空白值的平均值错误处理

com - 在它自己的 exe 容器中创建每个 COM 实例

c# - shell 扩展如何将多个选定的文件传递给我自己的程序?

delphi - 通过 COM 访问静态属性

c# - 等待 Rx 观察者完成而不使用锁

c# - 带有 List<> 项目的玻璃映射器 Editable() 不工作?

c# Directory.GetFiles 在驱动器 C 上给出 8.3 个文件名

excel - 显示最小值