Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Events raised in .net code is does not seem to occur in COM code when deployed with Side by side manifests

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>
like image 890
Vidar Avatar asked Jun 03 '13 08:06

Vidar


2 Answers

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...

like image 166
Vidar Avatar answered Oct 31 '22 01:10

Vidar


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

like image 23
Herman Avatar answered Oct 31 '22 01:10

Herman