当Excel在编辑模式下引发事件时,c# COM服务器事件“丢失”
本文关键字:事件 COM 服务器 丢失 编辑 模式 Excel | 更新日期: 2023-09-27 18:04:37
我有一个用c#编写的进程内COM服务器(使用。net Framework 3.5),基于以下示例引发COM事件:http://msdn.microsoft.com/en-us/library/dd8bf0x3 (v =应用程序). aspx
Excel VBA是我的COM服务器最常用的客户端。我发现,当我提出COM事件,而Excel是在编辑模式(例如一个单元格正在编辑)的事件是"丢失"。意思是,VBA事件处理程序从未被调用(即使在Excel编辑模式完成后),并且对c#事件委托的调用通过并无声地失败,没有抛出异常。有人知道我如何在我的COM服务器上检测这种情况吗?或者更好的仍然使事件委托调用块,直到Excel是编辑模式?I have try:
- 检查事件委托的属性-找不到任何属性表明事件在客户端引发失败。
- 直接从工作线程和主线程调用事件委托——客户端不引发事件,服务器端不抛出异常。
- 将事件委托推送到工作线程的Dispatcher并同步调用它-客户端不引发事件,服务器端不抛出异常。
- 将事件委托推入主线程的Dispatcher并同步和异步调用它-客户端不引发事件,服务器端不抛出异常。
- 检查Dispatcher的状态码。BeginInvoke调用(使用DispatcherOperation.Status) -状态总是以" Completed "结束,并且永远不会处于" Aborted "状态。
- 创建一个进程外的c# COM服务器exe并测试从那里引发事件-相同的结果,事件处理程序从未被调用,没有异常。
由于我没有得到事件没有在客户端引发的指示,我无法在我的代码中处理这种情况。
下面是一个简单的测试用例。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服务器。然而,如果我使用全局接口表将事件接收接口从主线程回到主线程,然后调用事件-它将在Excel处于编辑模式时阻塞。(我还使用COleMessageFilter来禁用繁忙对话框和不响应对话框,否则我会收到异常: rpce_cantcallout_inexternalcall这是非法的呼叫,而内部消息过滤器。)
(如果你想看MFC自动化代码,请告诉我,为了简洁,我跳过它)
知道了这一点,我试着在我的c# COM服务器上做同样的事情。我可以实例化全局接口表(使用来自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接口指针,但我不知道如何得到它?我试过用Marshal。GetComInterfaceForObject(this, typeof(ManagerEvents)),但这只是抛出一个异常:System。
你应该使用回调方法而不是Event
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