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! :)