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:
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.
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 ...
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? 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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With