Here is a really simple .net <-> COM interop example using events.
This example works just fine as long as i either use regasm or the register for com interop option in Visual studio build options for the .net library. But I need to deploy using registration free interop enabled side-by-side manifests.
The application runs just fine in side-by-side mode, it's just that the events seems to disappear. I suspect it's some thread marshalling issue, but I can't seem to find the correct solution.
This is of course an attempt to replicate an issue I have with a slightly more complicated interop integration. There is one difference between the issues I'm having here compared to the real issues:
Both solutions fail to properly sink events raised in the .net code while running on reg-free deployment, and both solutions works as expected when the .net dlls are registered in the registry. However: on the "real" project I get a runtime error when it fails from System.Reflection.Target. On this simplified example it just fails silently.
I'm thoroughly stuck on this one, so any and all suggestions and solutions will be very much welcomed.
I've put the complete code on github if anyone needs to play around with it before answering: https://github.com/Vidarls/InteropEventTest
The .net part
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace InteropEventTest
{
[Guid("E1BC643E-0CCF-4A91-8499-71BC48CAC01D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface ITheEvents
{
void OnHappened(string theMessage);
}
[Guid("77F1EEBA-A952-4995-9384-7228F6182C32")]
[ComVisible(true)]
public interface IInteropConnection
{
void DoEvent(string theMessage);
}
[Guid("2EE25BBD-1849-4CA8-8369-D65BF47886A5")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITheEvents))]
[ComVisible(true)]
public class InteropConnection : IInteropConnection
{
[ComVisible(false)]
public delegate void Happened(string theMessage);
public event Happened OnHappened;
public void DoEvent(string theMessage)
{
if (OnHappened != null)
{
Task.Factory.StartNew(() => OnHappened(theMessage));
}
}
}
}
The COM (VB6) part
Private WithEvents tester As InteropEventTest.InteropConnection
Private Sub Command1_Click()
Call tester.DoEvent(Text1.Text)
End Sub
Private Sub Form_Load()
Set tester = New InteropConnection
End Sub
Private Sub tester_OnHappened(ByVal theMessage As String)
Text2.Text = theMessage
End Sub
I currently have the following files / folder structure for deploy:
Root
|-> [D] Interop.Event.Tester
|-> Interop.Event.Tester.manifest
|-> [D] InteropEventTest
|-> InteropEventTest.dll
|-> InteropEventTest.manifest
|-> InteropEventTest.tlb
|-> tester.exe
|-> tester.exe.manifest
Content of manifest files:
Interop.Event.Test.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="Interop.Event.Tester" version="1.0.0.0" type="win32" processorArchitecture="x86"/>
</assembly>
InteropEventTest.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="InteropEventTest" version="1.0.0.0" type="win32"/>
<clrClass
name="InteropEventTest.InteropConnection"
clsid="{2EE25BBD-1849-4CA8-8369-D65BF47886A5}"
progid="InteropEventTest.InteropConnection"
runtimeVersion="v4.0.30319"
threadingModel="Both"/>
<file name="InteropEventTest.tlb">
<typelib
tlbid="{5CD6C635-503F-4103-93B0-3EBEFB91E500}"
version="1.0"
helpdir=""
flags="hasdiskimage"/>
</file>
<comInterfaceExternalProxyStub
name="ITheEvents"
iid="{E1BC643E-0CCF-4A91-8499-71BC48CAC01D}"
proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
baseInterface="{00000000-0000-0000-C000-000000000046}"
tlbid="{5CD6C635-503F-4103-93B0-3EBEFB91E500}" />
</assembly>
tester.exe.manifest
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity name="tester.exe" version="1.0.0.0" type="win32" processorArchitecture="x86"/>
<dependency>
<dependentAssembly>
<assemblyIdentity name="InteropEventTest" version="1.0.0.0" type="win32"/>
</dependentAssembly>
</dependency>
<dependency>
<dependentAssembly>
<assemblyIdentity name="Interop.Event.Tester" version="1.0.0.0" type="win32" processorArchitecture="x86"/>
</dependentAssembly>
</dependency>
</assembly>
After a long time (and several failed attempts) It turned out I could make this work by making one tiny change:
Make the VB6 code compile to P-Code instead of native code.
I'm pretty sure this somehow affects how marshalling between threads is handles, but I've been unable to find any information confirming that theory.
At least it works...
Or Not! (24. October 2013)
It turned out that in real life compiling to P-Code was not enough. In another implementation of this pattern we ended up with the event just disappearing into nowhere, with no exceptions (we thought) and no traces. So more investigation was due:
1. The real issue
Wrapping the event triggering in a try-catch clause revealed that there was in fact an exception being thrown, it just never surfaced anywhere
if (OnHappened != null)
{
try
{
OnHappened(theMessage));
}
catch (Exception e)
{
Messagebox.Show(e.GetType().Name + " : " + e.message)
}
}
The exception was a TargetException (the object does not match the target type)
. Some research revealed that this was most probably a threading issue (as I had suspected earlier.)
2. The solution
Most of the stuff written about this seemed to solve it by using an Invoke method. It turned out that most other people trying to solve this was building winforms application, and thus had a handy Ìnvoke(Delegate)
method available on all forms and controls.
As Winforms also does quite a bit of COM interop behind the scenes (according to now forgotten articles on the google result list) The invoke method is used to ensure that a method call is executed on the thread that created the given component and thus ensure that it happens on the message-pumped UI thread.
I figured this could be relevant for my case aswell, so I cheated.
I made my interop class inherit from the winforms control
public class InteropConnection : Control, IInteropConnection
Now I wrapped my call in the Invoke method
if (OnHappened != null)
{
try
{
Invoke(OnHappened, theMessage);
}
catch (Exception e)
{
Messagebox.Show(e.GetType().Name + " : " + e.message)
}
}
Now I got an exception because the Control had no WindowHandle assigned.
As it turned out the Control class has a handy CreateHandle()
method that can be called and solves this particular issue. (I do not know what possible consequences this has, as the documentation does not recommend calling this method directly.
Now all seems to be working all the time, though I would not be surprised if something new jumps up and bites me now...
I have run into the same issue. COM can marshal the event/call to the correct thread but it needs to have a proxy-stub. These are added to the registry if you use the /tlb
option with regasm, and the equivalent in the manifest file are the elements typelib
and comInterfaceExternalProxyStub
. The VB6 executable can be compiled to a native binary.
For more info see my SO topic: Regfree COM event fails from other thread
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