The complete answer is as follows:
Problem: Class that writes data to excel unable to handle 'busy/reject' response messages from excel.
Solution: Implement IMessageFilter
interface as described here
IMessageFilter
Definition (from link):
namespace ExcelAddinMessageFilter
{
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct INTERFACEINFO
{
[MarshalAs(UnmanagedType.IUnknown)]
public object punk;
public Guid iid;
public ushort wMethod;
}
[ComImport, ComConversionLoss, InterfaceType((short)1),
Guid("00000016-0000-0000-C000-000000000046")]
public interface IMessageFilter
{
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int HandleInComingCall([In] uint dwCallType, [In] IntPtr htaskCaller,
[In] uint dwTickCount,
[In, MarshalAs(UnmanagedType.LPArray)] INTERFACEINFO[]
lpInterfaceInfo);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int RetryRejectedCall([In] IntPtr htaskCallee, [In] uint dwTickCount,
[In] uint dwRejectType);
[PreserveSig, MethodImpl(MethodImplOptions.InternalCall,
MethodCodeType = MethodCodeType.Runtime)]
int MessagePending([In] IntPtr htaskCallee, [In] uint dwTickCount,
[In] uint dwPendingType);
}
}
IMessageFilter
Implementation part of my class (see link):
#region IMessageFilter Members
int ExcelAddinMessageFilter.IMessageFilter.
HandleInComingCall(uint dwCallType, IntPtr htaskCaller, uint dwTickCount, ExcelAddinMessageFilter.INTERFACEINFO[] lpInterfaceInfo)
{
// We're the client, so we won't get HandleInComingCall calls.
return 1;
}
int ExcelAddinMessageFilter.IMessageFilter.
RetryRejectedCall(IntPtr htaskCallee, uint dwTickCount, uint dwRejectType)
{
// The client will get RetryRejectedCall calls when the main Excel
// thread is blocked. We can handle this by attempting to retry
// the operation. This will continue to fail so long as Excel is
// blocked.
// As an alternative to simply retrying, we could put up
// a dialog telling the user to close the other dialog (and the
// new one) in order to continue - or to tell us if they want to
// abandon this call
// Expected return values:
// -1: The call should be canceled. COM then returns RPC_E_CALL_REJECTED from the original method call.
// Value >= 0 and <100: The call is to be retried immediately.
// Value >= 100: COM will wait for this many milliseconds and then retry the call.
return 1;
}
int ExcelAddinMessageFilter.IMessageFilter.
MessagePending(IntPtr htaskCallee, uint dwTickCount, uint dwPendingType)
{
return 1;
}
#endregion
With the IMessageFilter
interface defined and implemented, I setup a STA Thread
and a Timers.Timer
as follows:
Thread:
thread = new Thread(WriteToExcel);
thread.SetApartmentState(ApartmentState.STA);
Timer:
timer = new System.Timers.Timer();
timer.Interval = 2000;
timer.Elapsed += new ElapsedEventHandler(StartSTAThread_Handler);
where StartSTAThread_Handler
is defined as:
void StartSTAThread_Handler(object source, ElapsedEventArgs e)
{
thread.Start();
thread.Join();
thread = null;
}
This thread calls the method I use to write to Excel and handles rejected messages with the IMessageFilter
interface described above. The last thing I had to do was to fully qualify excel OM references, that is; instead of:
Excel.Range rng = app.ActiveSheet.Range["range_name"];
rng.Copy(); // ERRROR: message filter's RetryRejectedCall is NOT called
I had to use fully qualified referencing:
app.ActiveSheet.Range["range_name"].Copy // OK: calls RetryRejectedCall when excel dialog etc is showing
While this seems to work for my needs, it does seem to contradict the "two dot rule" described by another poster here ...