I am having a problem with a COM interop in VB6. I am trying to write a .Net class in C# to access from within vb6 (something I've done too many time to count), and it works pretty well except that there's a strange error if I try to run it from within the VB6 IDE after I've already closed it once. If I close the IDE then re-open it I get 1 go at running the code, then after that it gets the error.

It's pretty strange - the .Net class contains a serial port object which are normally very finicky to work with, but that seems to get created and destroyed properly. It seems like the problem is coming from a LINQ query on a Dictionary<> that says 'The Value Is Not In The Sequence' meaning that I pass it a lookup value and it's not being found.

I know this description of the problem is pretty terrible (mostly because I don't understand the problem) so I will try and post the relevant code:

The C# interop class and interface:

namespace MeyersIntegrationLibInterop
{
    using System.Runtime.InteropServices;
    using MeyersIntegrationLib;

    [System.Runtime.InteropServices.Guid("20130A92-B432-49D0-9580-2A6A5744106E")]
    public interface IComInterop_MeyersIntegration
    {
        [DispId(1)]
        void Initialize(ref string[] SensorList,ref string[] SensorUnits);
        [DispId(2)]
        bool Send();
        [DispId(3)]
        void UpdateSensorValues(ref double[] NewValues);
        [DispId(4)]
        void Kill();
        [DispId(5)]
        bool Setup(string sData);
    }
    
    [Guid("BD220E34-D096-4D23-847C-83782549C4B1")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("ComInterop_MeyersIntegrationRS232")]
    public class ComInterop_MeyersIntegrationRS232 : IComInterop_MeyersIntegration
    {
        MeyersOverRS232 underylingClass;
        bool HasInitialized = false;
        public void Initialize(ref string[] SensorList,ref string[] SensorUnits)
        {
            underylingClass = new MeyersIntegrationLib.MeyersOverRS232(new SensorCollection(SensorList, SensorUnits));  
        }
        public bool Setup(string sData)
        {
            string[] sSplit = sData.Split(',');

            //*****************
            //Hardcode setup default serial info here
            try
            {
                return Setup(sSplit[0], int.Parse(sSplit[1]));
            }
            catch
            {
                return Setup(sSplit[0], 9600);
            }
            //*****************
        }
        public bool Setup(string COMPort, int BAUDRate)
        {
            //*****************
            //Hardcode setup default serial info here
            return HasInitialized = underylingClass.Init(COMPort, BAUDRate, System.IO.Ports.Parity.Even, 8, System.IO.Ports.StopBits.One);
            //*****************
        }
        public bool Send()
        {
            if (!HasInitialized)
                throw new Exception("ComInterop_MeyersIntegrationRS232::Send() -> The object is not initialized, or the initialization failed");
            return underylingClass.Send();
        }

        public void UpdateSensorValues(ref double[] NewValues)
        {
            int i = 0;
            foreach (double d in NewValues)
                underylingClass.Sensors.SetSensorValue(i, NewValues[i++]);
        }

        public void Kill()
        {
            underylingClass.Close();
        }

        public static string[] GetComports()
        {
            return System.IO.Ports.SerialPort.GetPortNames();
        }
    }
}

The VB6 class:

Private mType As Long '0=text file, 1=tcp, 2=rs232
Private mDataIndexes() As Long
Private mMeyersObject As Object
Private mCount As Long
Private mErrors As Long
Private mFrames As Long
Private mActive As Boolean
Private mLastError As String
Private mHasInit As Boolean
Private mReconnectCount As Long

Public Sub Init()
    On Error GoTo ErrHand
    Dim tType As String
    Dim tIndexes() As String
    Dim i As Long
    Dim tSensorNames() As String
    Dim tSensorUnits() As String
    Dim tCount As Long
    
    mReconnectCount = 10
    
    If (mHasInit = True) Then Exit Sub
    mActive = False
    mHasInit = True
    tIndexes = Split(gINIFile.INIGetSetting("MeyersOutput", "OutputIndexes", "0"), ",")
    tCount = UBound(tIndexes)
    mCount = tCount
    ReDim tSensorNames(tCount)
    ReDim tSensorUnits(tCount)
    ReDim mDataIndexes(tCount)
    For i = 0 To tCount
        mDataIndexes(i) = CLng(tIndexes(i))
        tSensorNames(i) = gDataDet(tIndexes(i)).description
        tSensorUnits(i) = gDataDet(tIndexes(i)).units
    Next i
    
    tType = LCase(gINIFile.INIGetSetting("MeyersOutput", "OutputType"))
    If (tType = "text") Then
        mType = 0
        Set mMeyersObject = CreateObject("ComInterop_MeyersIntegrationText")
        mMeyersObject.Initialize tSensorNames, tSensorUnits
        mActive = mMeyersObject.Setup(gINIFile.INIGetSetting("MeyersOutput", "Filepath", ""))
        frmMeshNetwork.lblMeyersConType.Caption = "Text file"
    ElseIf (tType = "tcp") Then
        mType = 1
        Set mMeyersObject = CreateObject("ComInterop_MeyersIntegrationTCP") 'New MeyersIntegrationLib.ComInterop_MeyersIntegrationTCP
        mMeyersObject.Initialize tSensorNames, tSensorUnits
        mActive = mMeyersObject.Setup(gINIFile.INIGetSetting("MeyersOutput", "IPAddress") & "," & gINIFile.INIGetSetting("MeyersOutput", "Port"))
        frmMeshNetwork.lblMeyersConType.Caption = "TCP/IP"
    ElseIf (tType = "rs232") Then
        mType = 2
        Set mMeyersObject = CreateObject("ComInterop_MeyersIntegrationRS232") ' New MeyersIntegrationLib.ComInterop_MeyersIntegrationRS232
        mMeyersObject.Initialize tSensorNames, tSensorUnits
        mActive = mMeyersObject.Setup(gINIFile.INIGetSetting("MeyersOutput", "ComPort") & "," & gINIFile.INIGetSetting("MeyersOutput", "BaudRate"))
        frmMeshNetwork.lblMeyersConType.Caption = "RS232"
    End If
    
    If (mActive = False) Then
        GoTo ErrHand
    End If
    frmMeshNetwork.lblMeyersStatus.Caption = "Active"
    Exit Sub
ErrHand:
    frmMeshNetwork.lblMeyersStatus.Caption = "Error"
    frmMeshNetwork.lblMeyersLastError.Caption = "Init Failed"
End Sub

Public Sub Update()
    On Error GoTo ErrHand
    Dim tValues() As Double
    Dim i As Long
    Dim bGood As Boolean
    
    ReDim tValues(mCount)
    For i = 0 To mCount
        tValues(i) = gDataVal(mDataIndexes(i))
    Next i
    mMeyersObject.UpdateSensorValues tValues
    bGood = mMeyersObject.Send
    If (bGood = False) Then GoTo ErrHand
    mFrames = mFrames + 1
    
    'update ui
    frmMeshNetwork.lblMeyersErrors.Caption = mErrors
    frmMeshNetwork.lblMeyersFrameCount.Caption = mFrames
    Exit Sub
ErrHand:
    If (mActive = True) Then
        mErrors = mErrors + 1
        frmMeshNetwork.lblMeyersErrors.Caption = mErrors
        mLastError = Err.description
        frmMeshNetwork.lblMeyersLastError.Caption = mLastError
        'try to reconnect to the tcp listener (if in tcp mode)
        If (mErrors Mod mReconnectCount = 0) Then
            If (mType = 1) Then
                mMeyersObject.Setup (gINIFile.INIGetSetting("MeyersOutput", "IPAddress") & "," & gINIFile.INIGetSetting("MeyersOutput", "Port"))
            End If
        End If
    End If
    'MsgBox Err.description
End Sub

Public Sub Kill()
    On Error Resume Next
    If (Not mMeyersObject Is Nothing) Then
        mMeyersObject.Kill
    End If
End Sub

SensorDescription class (the underlying class has a collection of these):

public class SensorDescription
    {
        #region Members and Properties
        static int SensorCount = 0;
        public string Name { get; private set; }
        public string Unit { get; private set; }
        public int Index { get; private set; }
        #endregion
        
        #region CTOR(s)
        public SensorDescription(string sName, string sUnit)
        {
            Name = sName.Trim();
            Unit = sUnit.Trim();
            Index = SensorCount++;
        }
        #endregion

        #region Private Methods
        private string GetCSV()
        {
            return Name + Common.CSVDelimiter + Unit + Common.CSVDelimiter;
        }
        #endregion
        #region Common Overrides
        public override int GetHashCode()
        {
            return GetCSV().GetHashCode();
        }
        public override string ToString()
        {
            return GetCSV();
        }
        public static implicit operator string(SensorDescription RHS)
        {
            return RHS.GetCSV();
        }
        public override bool Equals(object obj)
        {
            if (obj is SensorDescription)
                return ((SensorDescription)obj).ToString() == this.ToString();
            else if (obj is int)
                return ((int)obj) == this.Index;
            else if (obj == null)
                return false;
            else
                throw new Exception(string.Format("Invalid comparison: {0} of type {1} cannot be compared to a SensorDescription object", obj.ToString(), obj.GetType().ToString()));
        }
        public static bool operator ==(SensorDescription LHS, object RHS)
        {
            if (object.ReferenceEquals(LHS, RHS))
                return true;

            if (LHS == null || RHS == null)
                return false;

            return LHS.Equals(RHS);
        }
        public static bool operator !=(SensorDescription LHS, object RHS)
        {
            if (object.ReferenceEquals(LHS, RHS))
                return false;

            return !(LHS.Equals(RHS));
        }
        #endregion
    }

The collection class that holds sensordecriptions

/// <summary>
    /// Class that contains a collection of sensor descriptions and maps them to their value via a dictionary
    /// Uses LINQ (.Net framework 3.5+ required)
    /// </summary>
    public class SensorCollection
    {
        //Underlying collection
        private Dictionary<SensorDescription, double> SensorData;

        #region CTOR(s)
        public SensorCollection()
        {
            SensorData = new Dictionary<SensorDescription, double>();
        }
        public SensorCollection(IList<string> Names, IList<string> Units)
        {
            SensorData = new Dictionary<SensorDescription, double>();

            List<string> s = new List<string>();

            int iIndex = 0;
            if (Names.Count != Units.Count)
                throw new Exception("SensorCollection()->Names and units must be 1 to 1");
            //SensorData = new Dictionary<SensorDescription, double>();
            var SensorList = (from m in Names
                             select new SensorDescription(Names[iIndex], Units[iIndex++])).ToList();
            SensorList.ToList().ForEach(x => SensorData.Add(x, 0));
        }
        public SensorCollection(IList<SensorDescription> SensorDescriptions)
        {
            SensorData = new Dictionary<SensorDescription, double>();
            SensorDescriptions.ToList().ForEach(x => SensorData.Add(x, 0));
        }
        #endregion

        #region Public Methods
        public void SetSensorValue(SensorDescription sSensorDescription, double dValue)
        {
            if (SensorData.ContainsKey(sSensorDescription))
                SensorData[sSensorDescription] = dValue;
            else //****************************************This is the thrown exception in vb6
                throw new Exception(string.Format("Sensor {0} was not found in the sensor collection", sSensorDescription)); 
            //*************************************************************************
        }
        public void SetSensorValue(string sSensorName, string sSensorUnit, double dValue)
        {
            SensorDescription tFind = SensorData.Select(x =>
                (x.Key.Name == sSensorName && x.Key.Unit == sSensorUnit) ? x.Key : null).Single(x => x != null);
            SetSensorValue(tFind, dValue);
        }
        public void SetSensorValue(int iSensorIndex, double dValue)
        {
            SensorDescription tFind = SensorData.First(x => 
                x.Key.Index == iSensorIndex).Key;
            SetSensorValue(tFind, dValue);            
        }
        #endregion
        #region Common Overrides
        public override string ToString()
        {
            return string.Join(",",
                                (
                                    from m in SensorData.ToList()
                                    select m.Value.ToString()
                                )
                                .ToArray()
                              );//.TrimEnd(',');
        }
        public static implicit operator string(SensorCollection RHS)
        {
            return RHS.ToString();
        }
        #endregion
    }

The meyers base class:

public abstract class MeyersDataForTransport
    {
        /// <summary>
        /// Collection of sensors to send
        /// </summary>
        public SensorCollection Sensors { get; protected set; }
        public DateTime TimeStamp { get; set; }
        public TimeStampMode TimestampMode { get; set; }
        public string TimestampFormat { get; set; }
        //Base CTOR
        protected MeyersDataForTransport(SensorCollection _Sensors)
        {
            TimestampFormat = Common.MeyersTimestampFormat;
            TimestampMode = Common.DefaultTimestampMode;
            Sensors = _Sensors;
        }
        
        //Public abstract methods
        public abstract bool Init(params object[] parameters);
        public virtual bool Send()
        {
            if (this.TimestampMode == TimeStampMode.Automatic)
                this.TimeStamp = DateTime.Now;
            return SendDataToTarget();
        }
        protected abstract bool SendDataToTarget();
        public abstract void Close();
    }

And the RS232 class that is derived from the meyers base:

/// <summary>
    /// Class for sending meyers data over RS232
    /// </summary>
    public class MeyersOverRS232 : MeyersDataForTransport 
    {
        //Serial device object
        private SerialPort Serial;

        public MeyersOverRS232(SensorCollection _Sensors)
            : base(_Sensors)
        { }

        ~MeyersOverRS232()
        {
            GC.SuppressFinalize(this);
            Close();
            GC.WaitForPendingFinalizers();
        }
        public override bool Init(params object[] parameters)
        {
            return Init(parameters[0].ToString(), int.Parse(parameters[1].ToString()));
        }
        /// <summary>
        /// Tries to connect to a COM port to send Meyers data through
        /// </summary>
        /// <param name="ComPort">COM port to use (ie COM1)</param>
        /// <param name="Baud">Baud rate of the serial device</param>
        /// <param name="Parity">Parity of the serial device</param>
        /// <param name="DataBits">Databits of the serial device</param>
        /// <param name="StopBits">Stopbits of the serial device</param>
        /// <returns>True on success, False on error</returns>
        public bool Init(string ComPort, int Baud, Parity Parity, int DataBits, StopBits StopBits)
        {
            try
            {
                Serial = Serial ?? new SerialPort(ComPort, Baud, Parity, DataBits, StopBits);
                Serial.Encoding = Common.StringEncoding;
                Serial.Handshake = Handshake.None;
                Serial.Open();
            }
            catch (Exception e)
            {
#if DEBUG
                throw e;
#else
                Serial.Dispose();
                Serial = null;
                return false;
#endif
            } 
            return true;
        }
        /// <summary>
        /// Closes the COM port and frees up the resources used by it
        /// </summary>
        public override void Close()
        {
            try
            {
                if (Serial == null)
                    return;
                if (Serial.IsOpen)
                    Serial.Close();
                Serial.Dispose();
                Serial = null;
            }
            catch (Exception e)
            {
#if DEBUG
                throw e;
#else   
                Serial = null;
#endif
            }
        }
        /// <summary>
        /// Implementation of the data sending
        /// </summary>
        /// <returns>True on successful send, false on failure</returns>
        protected override bool SendDataToTarget()
        {
            try
            {
                Serial.Write(
                    this.TimeStamp.ToString(this.TimestampFormat) + Common.CSVDelimiter +
                    Sensors +
                    Common.EOL);
            }
            catch
            {
                return false;
            }
            return true;
        }
    }

I realize that's an awful lot of code to go through - but any general pointers are welcome! :)

Edited 4 Years Ago by skatamatic: n/a

It's not good to use interops, simply because of this issue that you are experiencing. It usually takes a while for the interop process to dispose, if you open task manager then you would see the process starting, when using the interop with excel, the excel process starts and displays 'EXCEL' in task manager. You have to force a GC on the interop to dispose of it properly.

protected void GC(object Clean)
    {
        try
        {
            if (Clean != null)
            System.Runtime.InteropServices.Marshal.ReleaseComObject(Clean);
        }

Edited 4 Years Ago by Cameronsmith63: Typo

Comments
Good tip

It's not good to use interops, simply because of this issue that you are experiencing. It usually takes a while for the interop process to dispose, if you open task manager then you would see the process starting, when using the interop with excel, the excel process starts and displays 'EXCEL' in task manager. You have to force a GC on the interop to dispose of it properly.

protected void GC(object Clean)
    {
        try
        {
            if (Clean != null)
            System.Runtime.InteropServices.Marshal.ReleaseComObject(Clean);
        }

Yeah I know, interops suck. But they are necessary for what I am doing (slowly porting existing vb6 code over to .net while still adding features to use in vb6). It seems pointless to write the features in vb6 since I will just have to rewrite them further down the line. I'll try that out, thanks!

This article has been dead for over six months. Start a new discussion instead.