Converting VB6 apps with external COM classes to C# or VB.NET

by Olman Quesada, on Jul 8, 2021 11:37:58 AM

Recently one of our customers faced a critical issue with COM classes in a converted application. The original VB6 application created instances of external classes at runtime that executed methods returning objects consumed by the application to populate grids, insert/edit data, and save them into a database. The migration of the VB6 app to C# required some problem solving to deal with this aspect of the source code.

The following VB6 code mirrors the issue: A COM class MyComClass registered in a server is instantiated and its DoSearch() method is invoked. At runtime, this method returns an ADODB Recordset that is used to populate a grid.

Dim obj As Variant
Set obj = CreateObject("MyComClass", "myServer")
Dim rs As ADODB.Recordset
Set rs = obj.DoSearch(strCriteria)
Set DataGrid1.DataSource = rs

 

That VB6 code uses late binding to create a MyComClass object and then call DoSearch(). While it works very nicely in VB6, the conversion to .NET is tricky.

ADODB to ADO.Net  using System.Data.Common and helpers namespace.

When a new Upgrade project is created using the VBUC (here you can take a look at the VBUC Quick start guide ) by default ADODB is converted to ADO.NET using System.Data.Common and helper classes.

Uograde Option to convert classic ADO to ADO.Net

This upgrade option implies ADODB.Recordset objects are replaced by Mobilize Enhanced DataSet (ADORecordsetHelper) objects.

For the above code in VB6, the generated code will look like:

object obj = Activator.CreateInstance(Type.GetTypeFromProgID("MyComClass"), "myServer", true);
ADORecordSetHelper rs = ReflectionHelper.Invoke<ADORecordSetHelper>(obj, "DoSearch", new object[]{strCriteria});
DataGrid1.DataSource = rs.Tables[0];

 

Notice how ADODB.Recordset was converted into a ADODBRecordsetHelper (the Mobilize Enhanced DataSet that emulates classic Recordset functionality).

Also, due to the usage of late binding in VB6, the converted code is using Mobilize ReflectionHelper to execute obj.DoSearch(strCriteria.). More info about late-binding and ReflectionHelper here.

Although the converted code compiles, a runtime exception is thrown here:

ADORecordSetHelper rs = ReflectionHelper.Invoke<ADORecordSetHelper>(obj, "DoSearch", new object[]{strCriteria});

The exception is caused because .NET cannot cast a COM object (in this case ADODB.Recordset) into a .NET DataSet class.

The following summarizes the situation at this point:

  1. VBUC doesn't know the return type for MyComClass.DoSearch method.
  2. The VB6 code assigns  the result of MyComClass.DoSearch to an ADODB.Recordset.
  3. The Upgrade Project is converting ADODB to ADO.NET . Therefore, when the code is converted, the VBUC will convert the local variable ADODB.Recordset to ADORecordsetHelper (Mobilize Enhanced DataSet).
  4. However, MyComClass is not migrated, therefore, MyComClass.DoSearch() will still return a ADODB.Recordset.

Alternatives

At this point there are different approaches to addressing this issue, but all of them require analyzing the code to understand how ADODB.Recordsets returned by COM functions like DoSearch() interact with other portions of the original VB6 and migrated code.

1. Reconvert the code and use ADODB via COM Interop

This approach requires reconverting the code and keeping ADODB via COM Interop in .NET.

Uograde Option to use classic ADO via COM Iterop

The code at the beginning of this post would look like this:

object obj = Activator.CreateInstance(Type.GetTypeFromProgID("MyComClass"), "myServer", true);
ADODB.Recordset rs = ReflectionHelper.Invoke<ADODB.Recordset>(obj, "DoSearch", new object[]{strCriteria});

 

In this scenario, we're still using ADODB via COM Interop in .NET, and the code will work in .NET. However, there may be potential issues with this approach:

  1. The migrated code is still using legacy code. ADODB is an outdated technology without support from Microsoft.
  2. It's possible to get functional issues with ADODB libraries in .NET. In my experience, using legacy DLLs in .NET could lead to unexpected paths.
  3. Migrated components to .NET using data binding will not work if a classic ADODB.Recordset is used to populate them.
Let's consider a Microsoft DataGrid being populated with the recordset returned by DoSearch. The VB6 code will look like this:
DataGrid1.DataSource = rs


where rs is an ADODB.Recordset.

Despite this working in VB6, if the DataGrid is migrated and converted to a .NET DataGridView, the equivalent conversion for the previous VB6 line of code will do nothing:

DataGrid1.DataSource = rs;

No exception is thrown, but the DataGridView will not be populated because ADODB.Recordset as a COM object doesn't implement the .NET interfaces expected by DataGridView.DataSource property.

 

2. Use ADODB.Recordsets to fill .NET DataSets

This approach could be cheaper to solve the issue described here, but definitely is just a manual coding workaround that may be challenged with other coding scenarios.

This is how the VBUC converts the VB6 code indicated above, but we know this crashes at runtime:

object obj = Activator.CreateInstance(Type.GetTypeFromProgID("MyComClass"), "myServer", true);
ADORecordSetHelper rs = ReflectionHelper.Invoke<ADORecordSetHelper>(obj, "DoSearch", new object[]{strCriteria});
DataGrid1.DataSource = rs.Tables[0];

 

The above code with a few manual changes could be functional.

object obj = Activator.CreateInstance(Type.GetTypeFromProgID("MyComClass"), "myServer", true);
ADORecordSetHelper rs;
ADODB.Recordset rs2 = ReflectionHelper.Invoke<ADORecordSetHelper>(obj, "DoSearch", new object[]{strCriteria});

System.Data.OleDb.OleDbDataAdapter da = new System.Data.OleDb.OleDbDataAdapter();
da.Fill(rs, rs2, "customer");

DataGrid1.DataSource = rs.Tables[0];

  1. Given DoSearch() returns a ADODB.Recordset, create a new ADODB.Recordset variable that will have the value returned by that method.
  2. Create a DataAdapter. For this example, I'm using an OleDbDataAdapter, but it will work with a OdbcDataAdapter or a SqlClientDataAdapter. Choose the one that fits the data provider being used by the migrated application.
  3. Use the DataAdapter created above to populate the ADORecordsetHelper (enhanced DataSet) with the values in the ADODB.Recordset created in point 1.
  4. Now, the populated ADORecordsetHelper could be used to populate grids and manipulate data returned by DoSearch.

3. Converting the COM class to .NET

Perhaps the best option in terms of getting rid of legacy code: convert both the VB6 application consuming COM classes and the COM classes as well.

If the source code (for the COM classes) is not available, then consider replacing COM classes with a .NET alternative.

Although this approach might extend the scope of the migration project, it eliminates dealing with issues like the one described in this post. Further, it will have a positive impact on the stabilization phase of the project (for more info on the stages of a migration project you can read this).

Conclusion

This type of situation is hard to solve when converting code, so I recommend understanding the architecture of the VB6 application being converted before starting any upgrade process.

If COM objects are used to save data manipulated by the migrated application back into database, then I suggest using approach 1 or 3. It's tempting to try coding a new method that copies back data from the ADO.NET DataSet to a ADODB.Recordset, but keep in mind those data containers also have information about row states and other info that could not be possible to pass to a ADODB Recordset, creating other challenging issues.

Approach #3 is my favorite one as it will prevent major issues later during the stabilization phase. 

 

Topics:VB6VBUCCOM ClassesADO.NetClassic ADO (ADODB)

Comments

Subscribe to Mobilize.Net Blog

FREE CODE ASSESSMENT TOOL