none
Invalid value issue between COM(implemented in VB 6) and C# via event RRS feed

  • Question

  • Hi guys,

             We are meeting with a strange problem: can not get object(Interface) in COM , which troubled me for a couple of days.

            Original one is here http://social.msdn.microsoft.com/Forums/en-US/vbgeneral/thread/a3af29a4-25cb-43ac-b7d3-207b02ca5572, I posted here for intent that more experts could see it. Sorry for the inconvenience may caused.       

            Problem statement

            what we expected is that:

            1. Click Command button in COM OCX,

            2. COM OCX will raise event OnListRequest to C# form, 

            3. C# form then pass MyIList(implement a customized IList, which defined in VB) into COM via event: OnListRequest as reference variant event parameter,

            4. So that in COM, return value of count function of IList can be got.

             But when I read the MyIList.Count passed in, I found that it is something(2809428) more than 11(value of Count should return).

            The most strange point here is that if we add "MsgBox myList" statement before access Count function, it shows 11. what MsgBox(in VB) actually did so that actual value can be got?

            You are really appreciated if you would give me some help.

            I attached all related source code here

    COMLib.dll is developed in VB, defines IList, IComponent and OnListRequest Event,  all are used in both COMOcx and C# Form 

    COM OCX:

    Option Explicit
    Implements IComponent

    Private mobjEventHandler As CEComponentEvents
    Private Property Set IComponent_EventHandler(RHS As COMLib.CEComponentEvents)
       
        Set mobjEventHandler = RHS
       
    End Property

    Private Sub ShowData(myList As IList)

        If myList Is Nothing Then
            MsgBox "objIList Is Nothing! "
        Else
           
                MsgBox myList   'if added this MsgBox, myList.Count return exactly what define in C#.
                List1.AddItem myList.Count
                
        End If

    End Sub
    Private Sub Command1_Click()
       Dim objIList As IList

       On Error GoTo Command1_Click_Error
           
        mobjEventHandler.RaiseOnListRequest objIList ' raise event, and get value from C# with by ref objIList
       
        ShowData objIList
       
       Exit Sub
    Command1_Click_Error:
        MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure Command1_Click of User Control MyControl"
    End Sub

    C# form which will access to COM OCX

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    using COMLib;
    using IComponent = COMLib.IComponent;

    namespace IssueDemoApp1
    {
        public partial class MainWindow : Form
        {
            private IComponent _compoennt;
            private readonly CEComponentEvents _ceComponentEvents;
            public MainWindow()
            {
                InitializeComponent();
                _ceComponentEvents = new CEComponentEvents();
                _ceComponentEvents.OnListRequest += _ceComponentEvents_OnListRequest;
            }

            private void _ceComponentEvents_OnListRequest(ref IList listresult)
            {
                listresult = new MyIList() as IList;  //pass the MyIList instance into COM
            }

            private void MainWindow_Load(object sender, EventArgs e)
             {
                //Show the OCX in form

                Type typeOcx = Type.GetTypeFromProgID("COMOcx.MyControl");
                var ocx = new AxControl(typeOcx.GUID.ToString());
                ocx.Parent = this;
                _compoennt = (IComponent)ocx.GetOcx();
                //set event handler for OnListRequest
                 _compoennt.set_EventHandler(_ceComponentEvents);
            }
        }
    }

    MyIList in C# which implemented IList defines in VB, need to Pass this instance to COM

    namespace IssueDemoApp1
    {
        [ComVisible(true)]
        public class MyIList : IList
        {

            public MyIList()
            {

            }

            public int Count()
            {
                return 11;  //what we expected to be get in COM
            }       
        }
    }

    IList defined in VB, in COMLib.dll,
    Option Explicit

    'Number of items in the list
    Public Function Count() As Long
    End Function

    CEComponentEvents defined in VB in in COMLib.dll 

    Option Explicit
    Public Event OnListRequest(ListResult As IList)
    Public Sub RaiseOnListRequest(ByRef ListResult As IList)
        On Error GoTo RaiseOnListRequestError
        RaiseEvent OnListRequest(ListResult)
        Exit Sub
    RaiseOnListRequestError:
    End Sub

    IComponent defined in VB in in COMLib.dll 

    Option Explicit
    Public Property Set EventHandler(Handler As CEComponentEvents)
    End Property

    Thursday, September 20, 2012 4:33 AM

Answers

  • Hello Denny,

    1. I have 2 solutions for you. In this post, I shall expound my personally recommended one. I shall provide the other solution in a third post.

    2. Solution 1 : Modify The Declaration for MyIList :

    2.1 Here is a new declaration for MyIList :

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class MyIList : IListClass
    {
        public MyIList()
        {
    
        }
    
        public override int Count()
        {
            return 11;  //what we expected to be get in COM
        }
    }

    2.2 First, derive MyIList from IListClass (the RCW class for the VB IList class).

    2.3 Second, add the ClassInterfaceAttribute and use the "ClassInterfaceType.None" argument. Doing so will ensure that an instance of MyIList will expose COM interfaces implemented explicitly by the class (i.e. those of the IListClass class).

    2.4 What is returned via _ceComponentEvents_OnListRequest() will be a pointer to the _IList COM dual-interface. It will not point to an _Object interface.

    2.5 With this interface pointer, the VB code may call methods of the _IList interface directly or via IDispatch.

    2.6 A call to "List1.AddItem myList.Count" will call on MyIList's Count() method :

    public override int Count()
    {
        return 11;  //what we expected to be get in COM
    }
    

    2.7 However, in this case, the ToString() method will not be accessible and so a call to "MsgBox myList" will result in a crash.

    3. I shall post again to provide the second solution.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    • Marked as answer by Denny Song Sunday, September 23, 2012 12:21 PM
    Sunday, September 23, 2012 8:04 AM

All replies

  • Hi Denny,

    Welcome to the MSDN Forum.

    I am trying to involve some other one in this thread. Please wait it patience. Thank you.

    Best regards,


    Mike Feng
    MSDN Community Support | Feedback to us
    Please remember to mark the replies as answers if they help and unmark them if they provide no help.

    Friday, September 21, 2012 7:17 AM
    Moderator
  • Mike, thank you very much.

    Be noted that using IsObject(myList) or TypeName(myList), instead of MsgBox also works.

    Friday, September 21, 2012 10:24 AM
  • it seems that VB6 only handled automation interfaces, which means only basic datatypes and variants, no complex datatypes. We are now passing a complex datatype(a compelelty different customized IList) into COM, is that safe enough? could you please also tell me what risk will be there.

    Friday, September 21, 2012 10:37 AM
  • Hello Denny,

    1. Thank you v much for your patience. I was keen on this post right after reading it last Thursday. It took me several days to analyze it.

    2. Currently, I have emerged a practical solution. However, I have not been able to determine the actual low-level reason for the apparent error, unfortunately.

    3. The High-Level Reason For The Error.

    3.1 With the MyIList class declared as it is in the OP :

    namespace IssueDemoApp1
    {
        [ComVisible(true)]
        public class MyIList : IList
        {
    
            public MyIList()
            {
    
            }
    
            public int Count()
            {
                return 11;  //what we expected to be get in COM
            }       
        }
    }

    and the way an instance of it is created :

    private void _ceComponentEvents_OnListRequest(ref IList listresult)
    {
    	listresult = new MyIList() as IList;  //pass the MyIList instance into COM
    }
    

    what gets passed to the VB code is actually the dual interface of the COM Callable-Wrapper of an instance of a .NET managed Object.

    3.2 The definition for this interface is listed below :

    struct __declspec(uuid("65074f7f-63c0-304e-af0a-d51741cb4a8d"))
    _Object : IDispatch
    {
        //
        // Raw methods provided by interface
        //
    
          virtual HRESULT __stdcall get_ToString (
            /*[out,retval]*/ BSTR * pRetVal ) = 0;
          virtual HRESULT __stdcall Equals (
            /*[in]*/ VARIANT obj,
            /*[out,retval]*/ VARIANT_BOOL * pRetVal ) = 0;
          virtual HRESULT __stdcall GetHashCode (
            /*[out,retval]*/ long * pRetVal ) = 0;
          virtual HRESULT __stdcall GetType (
            /*[out,retval]*/ struct _Type * * pRetVal ) = 0;
    };

    3.3 In the "Command1_Click" VB code :

    Private Sub Command1_Click()
       Dim objIList As IList
    
       On Error GoTo Command1_Click_Error
           
        mobjEventHandler.RaiseOnListRequest objIList ' raise event, and get value from C# with by ref objIList
       
        ShowData objIList
       
       Exit Sub
    Command1_Click_Error:
        MsgBox "Error " & Err.Number & " (" & Err.Description & ") in procedure Command1_Click of User Control MyControl"
    End Sub

    "objIList" is treated as a dual interface pointer pointing to both IDispatch and the _IList interface (the default interface of the IList VB class).

    3.4 This being the case, in ShowData() :

    Private Sub ShowData(myList As IList)
    
        If myList Is Nothing Then
            MsgBox "objIList Is Nothing! "
        Else
           
                MsgBox myList   'if added this MsgBox, myList.Count return exactly what define in C#.
                List1.AddItem myList.Count
                
        End If
    
    End Sub

    Assuming that "MsgBox myList" is not called, when "List1.AddItem myList.Count" is called, "myList" is treated as a dual pointer to _IList.

    3.5 Now "Count" is the first method of the _IList interface and so when it is called, the very first method of the .NET _Object interface will be called. This is the "get_ToString()" method.

    3.6 This will lead to MyIList.ToString() and so a pointer to the BSTR with string value "IssueDemoApp1.MyIList" will be returned. It is this BSTR (a memory address) that gets displayed in the list box as a number.

    3.7 To verify this, you may override the ToString() method for the MyIList class as follows :

    namespace IssueDemoApp1
    {
        [ComVisible(true)]
        public class MyIList : IList
        {
    
            public MyIList()
            {
    
            }
    
            public int Count()
            {
                return 11;  //what we expected to be get in COM
            }
    
            public override string ToString()
            {
                return null;
            }
    
        }
    }

    With the ToString() returning a null, a 0 will be displayed on the list box.

    3.8 As to why things appear to go correctly right after the "MsgBox myList" code, I really have no idea at this time.

    3.9 My guess is that internally, after the call to "MsgBox myList", VB code internally started to use myList's IDispatch interface to make the call to Count().

    3.10 IDispatch calls will involve using the GetIdsOfNames() method to first obtain the dispids of methods and properties before calling Invoke() and so this would end up with a correct call to Count(). Recall that what is returned to VB is actually a dual-interface which supports both IDispatch and _Object interfaces.

    4. Solutions.

    4.1 This post has gotten a little long and so I shall expound on my solution in another post.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Sunday, September 23, 2012 7:31 AM
  • Hello Denny,

    1. I have 2 solutions for you. In this post, I shall expound my personally recommended one. I shall provide the other solution in a third post.

    2. Solution 1 : Modify The Declaration for MyIList :

    2.1 Here is a new declaration for MyIList :

    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    public class MyIList : IListClass
    {
        public MyIList()
        {
    
        }
    
        public override int Count()
        {
            return 11;  //what we expected to be get in COM
        }
    }

    2.2 First, derive MyIList from IListClass (the RCW class for the VB IList class).

    2.3 Second, add the ClassInterfaceAttribute and use the "ClassInterfaceType.None" argument. Doing so will ensure that an instance of MyIList will expose COM interfaces implemented explicitly by the class (i.e. those of the IListClass class).

    2.4 What is returned via _ceComponentEvents_OnListRequest() will be a pointer to the _IList COM dual-interface. It will not point to an _Object interface.

    2.5 With this interface pointer, the VB code may call methods of the _IList interface directly or via IDispatch.

    2.6 A call to "List1.AddItem myList.Count" will call on MyIList's Count() method :

    public override int Count()
    {
        return 11;  //what we expected to be get in COM
    }
    

    2.7 However, in this case, the ToString() method will not be accessible and so a call to "MsgBox myList" will result in a crash.

    3. I shall post again to provide the second solution.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    • Marked as answer by Denny Song Sunday, September 23, 2012 12:21 PM
    Sunday, September 23, 2012 8:04 AM
  • Hello Denny,

    1. In this post, I shall describe my second solution.

    2. For this solution, use back your original MyIList class code :

    namespace IssueDemoApp1
    {
        [ComVisible(true)]
        public class MyIList : IList
        {
    
            public MyIList()
            {
    
            }
    
            public int Count()
            {
                return 11;  //what we expected to be get in COM
            }       
        }
    }

    3. The changes are in the COMOcx VB code, in the ShowData() method :

    Private Sub ShowData(myList As IList)
    
        If myList Is Nothing Then
            MsgBox "objIList Is Nothing! "
        Else            
                Dim obj As Object
                Set obj = myList
                            
                List1.AddItem obj.Count            
        End If
    
    End Sub
    

    3.1 Instead of using "myList" directly, first cast it into an instance of the VB Object type (e.g. "obj").

    3.2 After that, use "obj" to call properties and methods of the IList VB class.

    3.4 The idea is that, with the original definition for MyIList, the returned value from _ceComponentEvents_OnListRequest() is a dual-interface pointer to the .NET _Object interface.

    3.5 This _Object dual-interface will support the methods of the IList class via IDispatch. Hence the need to first cast "myList" to a VB Object type.

    4. I personally recommend the earlier solution because it is a long-term one.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Sunday, September 23, 2012 9:11 AM
  • Hi Bio, Thank you very much for the analysis and solutions.

    I would prefer to the first solution so that we don’t need to change existing COMs, which would take us a lot of efforts.

    we will apply the first solution in coming days. thank you very much.

    Denny

    Sunday, September 23, 2012 12:21 PM
  • Hi Bio,

    I got an error when build this line

       public class MyIList : IListClass
    

    Error 1 The type 'COMLib.IListClass' has no constructors defined

    What is the possible reason for that?

    thanks

    Denny.

    Monday, September 24, 2012 4:08 AM
  • Hello Denny,

    1. The IListClass is a class (known as a Runtime-Callable-Wrapper Class) created by the Type Library Importer when you referenced the COMLib type library into your C# project.

    2. The Type Library Importer will generate a RCW class for each coclass that it encounters in the Type Library.

    3. After generating such a class the definition should be something like the following (example) :

        [TypeLibType(2)]
        [Guid("E06F0096-7433-4815-BA8E-E1048D20A0F6")]
        [ClassInterface(0)]
        public class IListClass : _IList, IList
        {
            public IListClass();
    
            [DispId(1610809344)]
            public virtual int Count();
        }

    4. As can be seen above, the IListClass will have a public constructor.

    5. Suggestion : right-click on "IListClass" in your source codes and select "Go To Definition". You should see the definition of IListClass similar to the one shown above.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Monday, September 24, 2012 4:19 AM
  • Hi Bio,

    this is code in my side.

    thanks

    Denny

    #region Assembly Interop.COMLib.dll, v4.0.30319 using System; using System.Runtime.InteropServices; using VBA; namespace COMLib { [Guid("54D7C0B5-C1A6-4704-93FD-E10D7B94B98C")] [ClassInterface(0)] public class IListClass : _IList, IList { [DispId(1745027122)] public virtual short ColumnCount { get; } [DispId(1745027123)] public virtual Collection ColumnNames { get; } [DispId(1745027124)] public virtual string Name { get; } [DispId(1745027120)] public virtual int RecordNumber { get; } [DispId(1745027121)] public virtual bool Updatable { get; } [DispId(1610809413)] public virtual bool Add(params object[] Values); [DispId(1610809399)] public virtual bool BOF(); [DispId(1610809415)] public virtual bool Clear(); [DispId(1610809411)] public virtual int Count(); [DispId(1610809409)] public virtual bool EOF(); [DispId(1745027126)] public virtual string get_FieldSeparator(); [DispId(1745027125)] public virtual object get_Value(ref object Column = Type.Missing); [DispId(1610809410)] public virtual string Group(ref object Column = Type.Missing); [DispId(1610809400)] public virtual void MoveFirst(); [DispId(1610809408)] public virtual void MoveLast(); [DispId(1610809406)] public virtual void MoveNext(); [DispId(1610809407)] public virtual void MoveNextGroup(ref object Column = Type.Missing); [DispId(1610809401)] public virtual void MovePrevious(); [DispId(1610809402)] public virtual void MovePreviousGroup(ref object Column = Type.Missing); [DispId(1610809403)] public virtual void MoveTo(ref string itemkey, ref object Column = Type.Missing); [DispId(1610809404)] public virtual void MoveToGroup(ref string GroupValue); [DispId(1610809405)] public virtual bool NoMatch(); [DispId(1610809412)] public virtual bool Refresh(); [DispId(1610809414)] public virtual bool Remove(); [DispId(1745027126)] public virtual void set_FieldSeparator(ref string value); [DispId(1745027125)] public virtual void set_Value(ref object Column, ref object value); } }


    Monday, September 24, 2012 4:52 AM
  • Hello Denny,

    1. Very unusual. There really should have been a declaration for the public default constructor for IListClass.

    2. I assume that you have referenced COMLib.dll in the usual way.

    3. Try unreferencing COMLib.dll and then referencing it again.

    - Bio.


    Please visit my blog : http://limbioliong.wordpress.com/

    Monday, September 24, 2012 5:19 AM
  • Hi Bio,

    I had tried un-referencing and re-referencing, but there is still not a public constructor.

    Can I zip the projects and send it to you via email, so you can check the difference between my projects and yours. 

    Thanks

    Denny

    Monday, September 24, 2012 8:32 AM