FRANCESCO BALENA ON VB MIGRATION

 

Generating auto-implemented properties for Visual Basic 2010

clock April 7, 2010 01:55

As our customers already knows, starting with version 1.30 VB Migration Partner can generate Visual Basic 2010 projects and solutions. In this short article I'll illustrate how you can generate code that leverages one of the coolest new features of VB2010: auto-implemented properties.

To see when and why auto-implemented properties can be useful, consider that most properties are just wrappers for a private variable, as in the following example:

Private m_ID As Long
Private m_Name As String

Public Property Get ID() As Long
    ID = m_ID
End Property

Public Property Get Name() As String
    Name = m_Name
End Property

Public Property Let Name(ByVal value As String)
    m_Name = value
End Property

When converted through VB Migration Partner, the above code generates these VB.NET statements:

Private m_ID As Integer
Private m_Name As String = ""

Public Readonly Property ID() As Integer
    Get
        Return m_ID
    End Get
End Property

Public Property Name() As String
    Get
        Return m_Name
    End Get
    Set(ByVal value As String)
        m_Name = value
    End Set
End Property


In cases like this – that is, when a property is merely a wrapper for a private variable and doesn’t perform any additional action – you can generate a new cool feature of Visual Basic 2010: auto-implemented properties. In fact, in VB2010 you can rewrite the above code block with just two lines:

Public ReadOnly Property ID As Integer
Public Property Name As String = ""


VB Migration Partner doesn’t include an explicit option to generate auto-implemented properties. However, you can easily tweak the converted VB2010 code to reach this goal. In fact, you just need three pragmas:

'## REM tranform the variable declaration into an implemented property
'## PostProcess "\b(Dim|Private)\s+m_(?<prop>Name)(?<type>\s+
        As\s+.+\r\n)", "Public Property ${prop}${type}"

'## REM same as above, but this is for readonly properties
'## PostProcess "\b(Dim|Private)\s+m_(?<prop>ID)(?<type>\s+
        As\s+.+\r\n)", "Public ReadOnly Property ${prop}${type}"

'## REM delete the Property procedures
'## PostProcess "[ \t]*(Public|Friend|Private)\s+(ReadOnly\s+)?Property
        \s+(ID|Name)\b.+\r\n(.+\r\n)+?\s*End Property\r\n", ""


The interesting point is that you need only three pragmas regardless of how many properties you want to process. For example, suppose that you have a class that contains 8 properties that can be transformed into auto-implemented properties:
    FirstName, LastName, Address, City, ZipCode, Country  (read-write)
    ID, Age (read-only)
These are the three pragmas that you need:

'## PostProcess "\b(Dim|Private)\s+m_(?<prop>FirstName|LastName|Address|
        City|ZipCode|Country)(?<type>\s+As\s+.+\r\n)",
        "Public Property ${prop}${type}"

'## PostProcess "\b(Dim|Private)\s+m_(?<prop>ID|Age)(?<type>\s+As\s+
        .+\r\n)", "Public ReadOnly Property ${prop}${type}"

'## PostProcess "[ \t]*(Public|Friend|Private)\s+(ReadOnly\s+)?Property\s+
        (FirstName|LastName|Address|City|ZipCode|Country|ID|Age)\b.+\r\n
        (.+\r\n)+?\s*End Property\r\n", ""

Cool, uh?

NOTE: If the original VB6 code accesses the private variable outside the property procedure, you need one more pragma to ensure that all references to the variable are rendered as referenced to the property

'## PostProcess "\bm_(?<prop>FirstName|LastName|Address|City|ZipCode|
        Country|ID|Age)\b", "${prop}"

Also notice that if the property is marked as ReadOnly and you access the inner variable in places other than the Class_Initialize event handler, than you can't render the property as an auto-implemented property.

 



Code Architects' ADOLibrary, a revolutionary approach to ADODB to ADO.NET migration

clock October 2, 2009 08:32

When migrating a VB6 legacy application, obtaining a working piece of VB.NET code is only part of the story. To complete the transition to the .NET world you also want to convert your database-access code to ADO.NET.

The Upgrade Wizard that comes with Visual Studio doesn't offer any option to upgrade ADODB code, with a good reason: "traditional" code translators can't automatically convert from ADODB to ADO.NET, no matter how sophisticated the translator is. (More on this below)

Fortunately, VB Migration Partner isn't a "traditional" code conversion. We have already proved that it correctly translates many VB6 features and keywords that many "migration experts" swore that were impossible to translate automatically. VB Migration Partner 1.20 supports *all* the major VB6 features, inclduing Gosubs, Variants, graphic statements, drag-and-drop, DDE, and a lot more.

After we dealt with the complete set of VB6 features, we decided to focus our attention on the challenges of porting ADODB-based code to ADO.NET.

The result of our efforts is Code Architects' ADOLibrary, a revolutionary tool that makes ADODB-to-ADO.NET migration one or two orders of magnitudes simpler, faster, and less expensive.

In a nutshell, ADOLibrary is a set of native .NET classes that behave exactly like their ADODB counterparts. For example, the ADOConnection object supports all the methods, properties, and events of the ADODB.Connection object, with exactly the same syntax and the same behavior. The same holds true for the ADORecordset class, the ADOCommand class, and ancillary classes such as ADOParameter, ADOField, and ADOProperty.

ADOLibrary is currently in internal beta stage, but it already supports forwardonly-readonly cursors and client-side cursors with optimistic batch updates. We even and transparently support advanced ADODB programming techniques, such as asynchronous events and alternate update strategies (the Update Criteria property), for the benefit of demanding ADODB gurus out there.

With its vast support for most ADODB features, ADOLibrary enables you to migrate thousands of database-related statements in a matter of hours. This is perhaps 100-200x faster than a manual migration and 10-20x more productive than any other code translator on the market. In practical terms, it can save you weeks or months in a real-world migration project and significantly reduce migration costs.

The first ADOLibrary example

Let me give you an idea of what the ADOLibrary works. Say that you have the following piece of VB6/ADODB code:

    Dim cn As New ADODB.Connection
    Dim rs As New ADODB.Recordset
    ' connect to Northwind
    cn.Open "Provider=SQLOLEDB.1;Initial Catalog=Northwind;..."
    cn.CursorLocation = ADODB.adUseClient
    rs.Open "Select * from Products ", cn,
ADODB.adOpenStatic, _
       
ADODB.adLockBatchOptimistic
    
    ' Load a listbox with all product names
    Do Until rs.EOF
        ListBox1.AddItem rs.Fields("ProductName").Value
        rs.MoveNext
    Loop


Here's the VB.NET code that uses the ADOLibrary, as generated by VB Migration Partner:

    ' Assumes: Imports CodeArchitects.ADOLibrary

   
Dim cn As New ADOConnection
   
Dim rs As New ADORecordset
    ' connect to Northwind
    cn.Open("Provider=SQLOLEDB.1;Initial Catalog=Northwind;...")
    cn.CursorLocation = ADOCursorLocation.adUseClient
    rs.Open("Select * from Products ", cn, ADOCursorType.adOpenStatic, _
        ADOLockType.adLockBatchOptimistic)
    
    ' Load a listbox with all product names    
   
Do Until rs.EOF
        ListBox1.AddItem(rs.Fields("ProductName").Value)
        rs.MoveNext()
   
Loop

As you see, the VB.NET code is basically the same as the original VB6 code, except for the different class and enum names. Even more important, the code is perfectly equivalent to the original VB6 code

If you are an ADODB developer you will find yourself comfortable with the generated code, which is as maintainable as the original VB6 code, except it leverages ADO.NET's superior performance and scalability. You are immediately productive and don't have to study ADO.NET, because our ADOLibrary hides the many differences between the two object models.

Don't let the ADODB-like syntax fool you, though: ADOLibrary relies on ADO.NET objects exclusively and has no dependency on COM or ADODB. It's like having ADO.NET with an ADODB dress.

But the ADOLibrary is more than just an ADODB clone, because it exposes all the ADO.NET structures that it uses internally in addition to all the properties and methods "inherited" from ADODB.

For example, the ADORecordset object exposes the inner DataTable object that stores the individual records. You can therefore bind the "recordset" to .NET controls and data grids. Or you can optimize the converted code as follows, and improve it to keep track of both the product name and the product ID:

    ' Load a listbox with all product names
    ListBox1.ValueMember = "ProductID"
    ListBox1.DisplayMember = "ProductName"
    ListBox1.DataSource = rs.DataTable     ' use native .NET binding!


Thanks to the DataTable member, you can rely on .NET databinding to implement master-detail relationships, advanced data formatting, input validation, and so forth. Just keep in mind is that all these additions and fixes are optional, because the converted .NET code will work immediately as-is.


Why you can't obtain ADODB-to-ADO.NET functional equivalence with a traditional code translator

At least another VB6 conversion tool vendor claims that their product can handle ADODB to ADO.NET conversions by means of code transformation techniques. However, they don't go into many details about which features are converted and which ones require some (or a lot of) manual fixes.

The problem in automatic conversion of ADODB code to ADO.NET is two-fold. First and foremost, it is impossible to achieve full functional equivalence. Second, filling the gap between ADODB and ADO.NET by means of code transformations is that you end up generating verbose (and often illogical) code that is very hard to maintain and evolve in the future.

Consider the following trivial code snippet:

    Set cnNorthwind As New ADODB.Connection
   
Set rsOrders As New ADODB.Recordset
   
cnNorthwind.Open "Provider=SQLOLEDB;Data Source=.;Initial Catalog=Northwind;Integrated Security=SSPI"
    rsOrders.Open "SELECT * FROM Orders", db,
ADODB.adOpenKeyset, _
        ADODB.
adLockOptimistic

Notice that this example uses server-side keyset cursors, that aren't supported by ADO.NET. Keysets are typically used when you need to modify data in the database, therefore the closest ADO.NET structure that can perform the same task is the combination of a DataSet plus a DataAdapter. This is the best code that a conversion tool that relies solely on code transformation can generate

    Dim cnNorthwind As New SqlConnection
   
Dim rsOrders As New DataSet
    cnNorthwind.Open("
Provider=SQLOLEDB;Data Source=.;Initial Catalog=Northwind;Integrated Security=SSPI")
   
Dim cmd As New SqlCommand
   
cmd.Connection = cnNorthwind
   
cmd.CommandText = "SELECT * FROM Orders"
   
Dim da As New SqlDataAdapter(com.CommandText, com.Connection)
    adap.Fill(rs)

Up to here, the converted code behaves more or less like the original VB6 code. However, the actual problems arise now. For example, consider that

  • the original ADODB code typicall uses a Do Until rsOrders.EOF loop to visit and possibly modify each record in the database. The loop can be transformed into a For Each loop that visits all the rows in the DataTable, but the database is updated only when the DataAdapter's Update method is invoked.
  • when using a keyset on a locked record, you get a runtime error when you modify one or more fields and then move away from the current record. Conversely, the optimistic lock used by the DataAdapter.Update throws a single error when any record in the DataTable fails to update. The two models greatly differ, so expect a lot of manual fixes in this area.
  • The example shows a simple SELECT query, however many VB6 apps retrieve (and update) by means of queries or stored procedures with parameters. ADODB and ADO.NET mechanisms for retrieving and using stored proc parameters are very different and this gap can't be filled by writing some code. Besides, the .NET CommandBuilder.DeriveParameters method has several defects and can't be used in production code, thus in practice you are forced to define all the query parameters manually in code.

It isn't at all surprising that our competitors don't provide any code sample that shows how their tool converts these (and many other) ADODB basic features, such as Recordset and Connection events, transactions, and bookmarks. Not to mention more advanced ones such as the Update Criteria property and the Resync method.

In practice, we estimate that a traditional code translator can automate no more than 10-15% of the statements that read, write, and process data stored in a database. Is 15% enough to qualify for ADODB-to-ADO.NET automatic conversion?

Our competitor also claims that their tool can convert code in a database-agnostic fashion. In .NET parlance, it means that they generate code that uses objects in the System.Data.Common namespace exclusively, e.g. DbConnection and DbCommand. You might believe that it's a good approach at writing database-agnostic code, until you see how ADOLibrary solves the same problem.

Internally, ADOLibrary uses the .NET objects in the System.Data.Common namespace, therefore it is inherently database-agnostic. When it's time to create a concrete class, by default ADOLibrary instantiates the objects in the System.Data.OleDb namespace (e.g. OleDbConnection, OleDbCommand, and so forth), and is therefore as database-agnostic as the original ADODB code. However, this is just the default behavior and you can override it by simply setting a global property.

For example, if your application is designed to work with Microsoft SQL Server exclusively, you can have ADOLibrary instantiate concrete classes in the System.Data.SqlClient namespace, which delivers faster and more scalable code. To enable this optimization you just need to add the following single statement:

    ADOConfig.LibraryKind = ADOLibraryKind.SqlClient

That's it! No need to be confused with base classes or other similar annoyances! Wink

For even more flexibility, you can read the value of the LibraryKind property from the configuration file, to let your end users customize the application for the database they actually use, without you having to recompile your code. Talk about flexibility!



Accomplishing the impossible: VARPTR in VB.NET!

clock March 16, 2009 02:39

VB6 supports three functions – VarPtr, StrPtr, and ObjPtr – that were never officially supported by Microsoft, yet they have been extensively used by many expert VB6 developers. In fact, you badly need these functions when calling some especially complex Windows API functions. Of these functions, the most useful one is VarPtr, which returns the address of the memory location where the value of a variable is stored.

Neither the Upgrade Wizard nor any other VB6 migration tool supports the VarPtr keyword, thus I decided that our VB Migration Partner *had* to correctly convert it. Thanks to Google, I found out that the problem had been discussed at length in many forums, but no definitive solution has ever been found yet. You can write an implementation in unsafe C#, or an unmanaged DLL written in C or Delphi, but these solution would force us to distribute a separate DLL with VB.NET apps converted by VB Migration Partner, which I’d rather not to.

It took a while and a lot of thinking, but in the end I figure out a way to solve the problem. Yes, it is possible to write a VarPtr function in plain VB.NET, with only the help of a method exposed by the Windows API. Actually, you need just a few lines of code:

' -----------------------------------------------------------
' VARPTR implementation in VB.NET
' Part of VB Migration Partner’s support library
'
' Copyright © 2009, Francesco Balena & Code Architects
' -----------------------------------------------------------

Module VarPtrSupport
   ' a delegate that can point to the VarPtrCallback method
   Private Delegate Function VarPtrCallbackDelegate( _
     
ByVal address As Integer, ByVal unused1 As Integer, _
      ByVal
unused2 As Integer, ByVal unused3 As IntegerAs Integer


   ' two aliases for the CallWindowProcA Windows API method
   ' notice that 2nd argument is passed by-reference
   Private Declare Function CallWindowProc Lib "user32" _
      Alias
"CallWindowProcA" _
      (ByVal wndProc As VarPtrCallbackDelegate, ByRef var As Short, _
      ByVal unused1 As Integer, ByVal unused2 As Integer,  _
     
ByVal unused3 As Integer) As Integer

   Private Declare Function CallWindowProc Lib "user32" _
      Alias "CallWindowProcA" _
      (ByVal wndProc As VarPtrCallbackDelegate, ByRef var As Integer, _
      ByVal unused1 As Integer, ByVal unused2 As Integer, _
     
ByVal unused3 As Integer) As Integer
   ' ...add more overload to support other data types...

   ' the method that is indirectly executed when calling CallVarPtrSupport
   ' notice that 1st argument is declared by-value (this is the
   ' argument that receives the 2nd value passed to CallVarPtrSupport)

   Private Function VarPtrCallback(ByVal address As Integer, _
        
ByVal unused1 As Integer, ByVal unused2 As Integer, _
         ByVal unused3 As Integer) As Integer
      Return address
   End Function


  
' two overloads of VarPtr
   Public Function VarPtr(ByRef var As Short) As Integer
      Return CallWindowProc(AddressOf VarPtrCallback, var, 0, 0, 0)
   End Function

   Public Function VarPtr(ByRef var As Integer) As Integer
      Return CallWindowProc(AddressOf VarPtrCallback, var, 0, 0, 0)
   End Function

   ' ...add more overload to support other data types...
End Module

To understand how the trick works, let’s see what happens when you call the VarPtr method. The only line of code in this method invokes one of the overloads of CallWindowProc method. The CallWindowProc method takes five arguments, the first one of which is a delegate that must point to a method that takes four 32-bit values. CallWindowProc invokes the method pointed to by the delegate and passed the other four values to such a method.

The key point in this mechanism is that each CallWindowProc overload takes a value by-reference in its second argument – a Short and an Integer, respectively. This means that the CallWindowProc method (buried inside the User32.dll) receives the address of the Short or Integer variable. This address is a 32-bit integer and is passed verbatim to the VarPtrCallback method. This method in turn receives a 32-bit integer value with by-value semantics, which means that the address parameter now contains whatever value was pushed on the stack by the CallWindowProc method.

Let’s quickly recap: the VarPtr method pushes the address of the Short or Integer variable – that is, the value we are interested in – on the stack. This 32-bit integer is received by the CallWindowProc method (in User32.dll) and is sent to the VarPtrCallback method, which receives it in its first argument and returns it verbatim to the CallWindowProc method, which in turn returns it to the VarPtr method that can finally return it to the caller.

Notice that you might need to add more overloads for the VarPtr method (and the CallWindowProc method), to support data types other than Short or Integer. Just remember that you can’t use this technique with String, Objects, or other reference types. It doesn’t work with Boolean values, either.

Interestingly, you can use the VarPtr method with structures, provided that the structure doesn’t contain any String, Object, or Boolean elements. To get the address of a structure just use VarPtr on its first element, as in this example:

Structure POINTAPI
   Public x As Integer
   Public y As Integer

End Structure

Dim
pnt As POINTAPI
Dim addr As Integer = VarPtr(pnt.x) 

To prove that the VarPtr function works correctly, let’s use it together with the CopyMemory Windows API method to delete an element in an array by quickly shifting all the elements towards lower indices:

Declare Sub CopyMemory Lib "Kernel32.dll" Alias "RtlMoveMemory" _
   (ByVal dest As Integer, ByVal source As Integer, _
    ByVal numBytes As Integer)


Sub
Main()
   Dim arr(1000) As Integer
   ' initialize the array
  
For n As Integer = 0 To UBound(arr) : arr(n) = n : Next
   ' ...
   ' delete first element by shifting all elements towards the beginning
   '
of the array, then clear last element
   CopyMemory(VarPtr(arr(0)), VarPtr(arr(1)), UBound(arr) * 4)
   arr(UBound(arr)) = 0
   ' check that it worked fine
   For n As Integer = 0 To UBound(arr) - 1
      If arr(n) <> n + 1 Then Debug.Print("Wrong value at index {0}", n)
   Next

End Sub

In case you are wondering why you should use CopyMemory and VarPtr to shift all the elements of an array – instead of a plain For … Next loop – the answer is: execution speed. Under VB6 this technique was often used to significantly speed up array operations; the VB.NET compiler produces more efficient code and this technique is seldom necessary, nevertheless the ability to convert this code from VB6 without any major edits means that you don’t have to spend too much time trying to understand what the VB6 developer meant to do.

Please notice that this implementation of VarPtr works well in 32-bit applications only, and fails when running on 64-bit operating systems. To ensure that things work as expected and that 32-bit code is generated even when running on 64-bit versions of Windows, you must select the Target CPU = x86 option in the Advanced Compile Options dialog box, in the Compile tab of the My Project page. (Odds are that you have to select this option anyway when doing complex operations with pointers.)

Important warnings: working with memory addresses under .NET can be very dangerous, much more dangerous than under VB6. The reason is, the garbage collector can fire virtually anytime while the program is executing, therefore the address of an object can suddenly change and the unmanaged method (CopyMemory in above example) would receive the address of a memory area that doesn't contain the data any longer. The neat result would be either a wrong value or an application crash. When using this implementation of VarPtr under VB.NET keep the following points into account:

  1. VarPtr is absolutely safe only when used to return the address of simple local variables, such as Short, Integer, Single, Double, or Date variables. Using local variables is safe because local variables are allocated on the stack and don't move even if an unexpected garbage collection occurs immediately after the VarPtr method returns but before the unmanaged method complete its execution.
  2. Passing the element of a Structure is safe, but only if the Structure is held in a local variable (as opposed to a class field)
  3. In all other cases, VarPtr isn't  100% safe and might occasionally deliver wrong results or crash the application. For example, it isn't 100% safe to pass VarPtr a class field or an element of an array, because array elements are stored in the managed heap and can be moved by the garbage collector (regardless of whether the array is stored in a local variable). 
  4. In a single-thread application the probability that a garbage collection occurs unexpectedly are very low and might even be considered as negligible, but they can't be considered as equal to zero.
  5. You can further minimize the probability of an unexpected GC by avoiding calling methods and language functions (e.g. Left, Int, Abs) inside the call to the unamanged method but, again, you can't reduce this probability to zero.

To recap, except when you are in cases #1 and #2 above, the converted code will work most of the time, but it can't be guaranteed to work always.The only documented way to ensure that an object doesn't move in memory because of unexpected gargabe collection is by pinning the object, by means of methods exposed by the System.Runtime.InteropServices.Marshal class.

Even with this limitation, the VB.NET implementation VarPtr method is quite helpful when doing a quick-and-dirty migration - using VB Migration Partner or the Upgrade Wizard. You can use VarPtr to check that the converted code works as intended, but it is strongly recommended that you get rid of VarPtr before going to production. In the CopyMemory case see above, for example, you can do without the VarPtr by using a different overload of the CopyMemory method that takes by-reference arguments:

 

Declare Sub CopyMemoryByref Lib "Kernel32.dll" Alias "RtlMoveMemory" _
   (ByRef dest As Integer, ByRef source As Integer, _
    ByVal numBytes As Integer)

(This code works because the .NET Framework automatically pins every object passed by reference to an external method.)

Even better, you should do your best to avoid unamanged calls altogether and replace them with calls to methods of pure .NET objects.



More on binary compatibility

clock December 20, 2008 06:11

A reader of a previous post on binary compatibility asked for clarification about my claim about VB Migration Partner being the the only conversion tool on the market offering this important feature, mentioning that at least another tool from our competitors also claim to offer this feature, which they obtain by adding ComVisible, ProgID, and ClassInterface attributes.

I am aware there is some margin for ambiguity and for uncorrect information, so I decided to drill a bit deeper on this topic.

You have binary compatibility between a COM component a .NET component if you can unregister the COM component and install the .NET component in its place, without having to recompile the COM clients that were using the COM component. We call it "binary compatibility" because it's the kind of compatibility we used to enforce under VB6, to ensure that a new version of a given component was perfectly compatible with its previous releases.

The ability to generate a VB.NET component that is binary-compatible with the original VB6 component is greatly useful, for many reasons:

  • You can migrate a complex N-tier application starting with its innermost DLLs, typically those that access the database, without having to recompile or modify in any way existing COM clients. 
  • The ability to convert only a subset of existing DLLs without being forced to migrate the entire application allows you to focus on those components that suffer most from the inherent scalability and performance limitations imposed by COM. This is the so-called phased migration.
  • Just as important, you can immediately debug the converted DLLs because you can use existing COM clients and their user interface - no need to write small throw-away .NET projects with the only purpose of testing the DLLs you just converted.
  • Finally, if your organization is very serious about testing, odds are that you have defined unit tests, use cases, and maybe scripts that rely on commercial testing tools. The great news is that, in most cases, binary compatibility allows you to test the converted DLLs using existing unit tests and scripts, thus saving you even more time.

As you see, binary compatibility can be a tremendous time-saver when migrating a VB6 application that is split in many DLLs, and it's no surprise that a migration tool vendor would like to claim that its product offers this feature.

Unfortunately for our competitors, to generate a .NET class that is binary-compatible with the original VB6 class you need much more than three attributes. You have to extract GUIDs from the original DLL and apply them correctly to the generated .NET class, you must handle fields, enums, types, events, and interfaces with special care, and you have to pay attention to many special cases and exceptions.

More precisely if you just add the ComVisible, ProgID, and ClassInterface attributes, the best you can get is compatibility with existing VBScript and ASP clients, not with compiled COM clients 

The bottom line: based on what I can read and what I heard from customers, I can only reinforce my claim that only VB Migration Partner offers true backward binary-compatibility with existing COM clients and fully supports phased migrations of N-tier VB6 apps.



Exploring version 1.10 - Type inference

clock November 23, 2008 04:19

VB Migration Partner Enterprise Edition is capable, in most cases, to correctly deduce a less generic data type for Variant local variables, parameters, class fields, functions and properties. This applies both to members that were explicitly declared as Variant and to members that lacked an explicit As clause. To see how this feature works, consider the following VB6 code:

'## DeclareImplicitVariables True

Private m_Width As Single

Property Get Width ()
    Width = m_Width
End Property

Property Let Width (value)
    m_Width = value
End Property

Sub Test(x As Integer, frm)
    Dim v1 As Variant, v2
    v1 = x * 10
    v2 = Width + x
    Set frm = New frmMain
    res = frm.Caption
End Sub

By default this code is converted to VB.NET as follows:

Private m_Width As Single

Public Property Width() As Object
    Get
        ' UPGRADE_WARNING (#0364): Unable to assign default member of 'Width'.
        ' Consider using the SetDefaultMember6 helper method.

        Return m_Width
     End Get
    Set(ByVal value As Object)
        ' UPGRADE_WARNING (#0354): Unable to read default member of 'value'.
        ' Consider using the GetDefaultMember6 helper method.

        m_Width = value
     End Set
End Property

Public Sub Test(ByRef x As Short, ByRef frm As Object)
    ' UPGRADE_INFO (#05B1): The 'res' variable wasn't declared explicitly.
    Dim res As Object = Nothing
    ' UPGRADE_INFO (#0561): The 'frm' symbol was defined without an
    ' explicit "As" clause.

    Dim v1 As Object = Nothing
    ' UPGRADE_INFO (#0561): The 'v2' symbol was defined without an
    ' explicit "As" clause.

    Dim v2 As Object = Nothing
    ' UPGRADE_WARNING (#0364): Unable to assign default member of 'v1'.
    ' Consider using the SetDefaultMember6 helper method.
    v1 = x * 10
    ' UPGRADE_WARNING (#0364): Unable to assign default member of 'v2'.
    ' Consider using the SetDefaultMember6 helper method.

    v2 = Width + x
    frm = New Form1()
    ' UPGRADE_WARNING (#0354): Unable to read default member of
    ' 'frm.Caption'. Consider using the GetDefaultMember6 helper method.
    ' UPGRADE_WARNING (#0364): Unable to assign default member of 'res'.
    ' Consider using the SetDefaultMember6 helper method.

    res = frm.Caption
End Sub

Variant variables – either implicitly declared or not – are translated into Object variables, which causes several warning to be emitted and, more important, adds an overhead at runtime. This overhead can be often avoided by declaring the variable of a more defined scalar type. You can have VB Migration Partner change the data type for a given member by means of the SetType pragma, but this operation requires a careful analysis of which values are assigned to the variable.

Users of VB Migration Partner 1.10 Enterprise Edition can have this analysis performed automatically, by inserting an InferType pragma at the project-, file-, method- or variable-level. For example, let’s assume that the previous VB6 code contains the following pragma at the file level:

'## InferType Yes

The Yes argument in this pragma makes VB Migration Partner attempt to infer the type of al the implicitly-declared local variables and of all the local variables, class fields, functions and properties that lack an explicit As clause:

Private m_Width As Single

Public Property Width() As Single
    Get
        Return m_Width
    End Get
    Set(ByVal value As Single)
        m_Width = value
    End Set
End Property

Public Sub Test(ByRef x As Short, ByRef frm As Object)
    ' UPGRADE_INFO (#0561): The 'frm' symbol was defined without an
    ' explicit "As" clause.
    ' UPGRADE_INFO (#05B1): The 'res' variable wasn't declared explicitly.

    Dim res As Object = Nothing
    Dim v1 As Object = Nothing
    ' UPGRADE_INFO (#0561): The 'v2' symbol was defined without an
    ' explicit "As" clause.

    Dim v2 As Single
    ' UPGRADE_WARNING (#0364): Unable to assign default member of 'v1'.
    ' Consider using the SetDefaultMember6 helper method.

    v1 = x * 10
    v2 = Height + x
    frm = New Form1()
    ' UPGRADE_WARNING (#0354): Unable to read default member of
    ' 'frm.Caption'. Consider using the GetDefaultMember6 helper method.
    ' UPGRADE_WARNING (#0364): Unable to assign default member of 'res'.
    ' Consider using the SetDefaultMember6 helper method.

    res = frm.Caption
End Sub

In this new version, VB Migration Partner can infer the type of the Width property from the type of the m_Width variable, which in turn permits to infer the type of the v2 local variable. VB Migration Partner attempts to infer neither the type of the v1 local variable (because it is explicitly declared As Variant) nor the type of the frm parameter (because by default method parameters aren’t under the scope of the InferType pragma).

You can extend type inference to members that were declared with an explicit As Variant by using the Force argument in the pragma. In addition, passing True in the second (optional) argument extends the scope to method parameters:

'## InferType Force, True

The result of the conversion is now the following:

Private m_Width As Single

Public Property Width() As Single
    Get
        Return m_Width
    End Get
    Set(ByVal value As Single)
        m_Width = value
    End Set
End Property

Public Sub Test(ByRef x As Short, ByRef frm As Form1)
    ' UPGRADE_INFO (#0561): The 'frm' symbol was defined without an
    ' explicit "As" clause.
    ' UPGRADE_INFO (#05B1): The 'res' variable wasn't declared explicitly.

    Dim res As String = Nothing
    Dim v1 As Integer
    ' UPGRADE_INFO (#0561): The 'v2' symbol was defined without an
    ' explicit "As" clause.

    Dim v2 As Single
    v1 = x * 10
    v2 = Width + x
    frm = New Form1()
    res = frm.Caption
End Sub

Notice that in this new version the type of frm parameter is inferred correctly, which in turn permits to infer the type of the res local variable.

Type inference works both with scalar types and object types. Deducing the correct type of an object often produces faster and more readable code. For example, consider the following VB6 code:

'## InferType
Public Sub SetAddress(ByVal address As String)
    Dim tb
    Set tb = Me.txtAddress  ' (txtAddress is a TextBox)
    tb = address
End Sub

VB Migration Partner can infer that the tb variable is of type TextBox and, consequently, it can correctly expand its default property:

Public Sub SetAddress(ByVal address As String)
    Dim tb As VB6TextBox = Me.txtAddress    
    tb.Text = address
End Sub


Extending the pragma scope to method parameters isn’t always a good idea, because the current version of VB Migration Partner infers the type of parameters by looking at assignments inside the method and doesn’t account for implicit assignments that result from method calls. For this reason, the type inferred during the migration process might not be correct in some cases.

Important note: Type inference isn’t an exact science and you should always double-check that the data type inferred by VB Migration Partner is correct. Better, we recommend that you use the InferType pragma only during early migration attempts and then you later assign a specific type to members by editing the original VB6 code or by means of the SetType pragma.



Exploring version 1.10 - Unused/unreachable code detection and removal

clock November 21, 2008 01:23
It’s no secret that a typical business application contains a lot of dead code that is never executed. This is especially true if the application has evolved over many years and has been authored by many developers. The percentage of useless code can vary, but is typical in the range 5% to 10%.

In a VB6 application this dead code consumes a little memory but doesn’t otherwise hurt the overall performance. However, things are quite different if you want to port that application to VB.NET – either by hand or automatically by means of a conversion tool.

In fact, if you have no way to spot where this dead code is, you might be wasting 5-10% of your time and money just to migrate code that will be never executed anyway. Considering how much migration initiatives may cost, this small percentage becomes a very high number in absolute terms.

VB Migration Partner has always included a very sophisticated code analysis engine that automatically spots portions of code that are unreachable – for example, the code that immediately follows an Exit Sub statement and that doesn’t begin with a label – as well as methods that aren’t referenced by code elsewhere in the same project or project group. VB Migration Partner distinguishes the two cases by means of different warning messages:

' UPGRADE_INFO (#0521): Unreachable code detected
' UPGRADE_INFO (#0501): The ‘XYZ’ member isn’t used anywhere in current application

The code analysis engine is smart enough to detect whether two methods call each other but not from the main application, or fields that are accessed only by methods that, in turn, are never invoked by other code. In this case the message is slightly different:

' UPGRADE_INFO (#0511): The ‘XYZ’ member is used only by members that
' haven’t found to be used in current application

Keep in mind that you can selectively disable warning messages by means of the DisableMessage pragma, as in:
    '## DisableMessage 521
or you can suppress all messages related to code analysis by means of the DisableMessages pragma:
    '## DisableMessages CodeAnalysis

Like all code analysis tools, VB Migration Partner may issue warnings related to members that appear to be unused only because the tool is analyzing one project at a time. For example, if you convert an N-tier application one component at the time, an analysis tool flags the majority of public members as “unused” even though they are used by client apps that haven’t been included in the code analysis. If the application isn’t too large, you can solve this issue by analyzing a project group that gathers all the individual projects, but this workaround isn’t practical for applications of dozens or hundreds different projects.

Another problem related to correctly spotting unused members is late-binding: if a given method or property is accessed by client code in late-bound mode, no code analysis tool is capable to detect that the member is actually used and shouldn’t be removed.

VB Migration Partner offers a “manual” solution to these problems in the form of two pragmas: the MarkAsReference pragma tells the code engine that a given member is actually referenced – for example, via late binding – and therefore the code analysis engine should omit warnings #0501 and #0511.

The MarkPublicAsReferenced pragma is useful when analyzing and migrating ActiveX DLL and OCX projects: when this pragma is used, all public properties and methods are marked as referenced. In turn, if these public members reference private variables or methods, these private members will be marked as referenced, too. The MarkPublicAsReferenced pragma can be scoped at the project or file level. For example, when migrating an ActiveX project, you typically need one single pragma:

    '## project:MarkPublicAsReferenced

All the features discussed so far have been available in all editions of VB Migration Partner since version 1.0. The forthcoming version 1.10 goes further, because it offers the ability to automatically remove unreachable code and unused members. (This feature is available only in the VB Migration Partner 1.10 Enterprise Edition.)

In version 1.10 you can remove unreachable code by means of the RemoveUnreachableCode pragma, which takes one of the following arguments: On (or omitted, remove the unreachable code), Off (don’t remove the unreachable code), Remarks (comment the unreachable code). For example, let’s consider this VB6 code:

Sub Test()
    ' GoSub DisplayMessage
    ' …
    ' more code here …
    ' …

    Exit Sub
DisplayMessage:
    MsgBox "Inside Test method"
    Return
End Sub


The code that begins with the DisplayMessage label was originally the target of the GoSub statement; it is clear that the developer later remarked out the Gosub keyword but forgot to delete the MsgBox and Return statements. This is how VB Migration Partner migrates this code:

Public Sub Test()
    ' GoSub DisplayMessage
    ' …
    ' more code here …
    ' …

    Exit Sub

    ' UPGRADE_INFO (#0521): Unreachable code detected

DoSomething:
    MsgBox6("Inside Test method")
    ' UPGRADE_INFO (#05A1): The VB6 code contains a Return but no
    ' GoSub has been found in current method.

    Err.Raise(3)
End Sub


Before deleting unreachable code, you might want to check that the program works correctly, by just commenting out the code, by adding the following pragma:

'## RemoveUnreachableCode Remarks
Public Sub Test()
    ...


This is what you get now:

Public Sub Test()
    ' GoSub DisplayMessage
    ' …
    ' more code here …
    ' …

    Exit Sub

    ' UPGRADE_INFO (#06F1): Unreachable code removed
    ' EXCLUDED: DoSomething:
    ' EXCLUDED: Beep()
    ' EXCLUDED: Err.Raise(3)

End Sub

Not that you are sure that the program works well, you can use the RemoveUnreachableCode pragma with no arguments:

'## RemoveUnreachableCode


which produces this code:

Public Sub Test()
    ' GoSub DisplayMessage
    ' …
    ' more code here …
    ' …

    Exit Sub

    ' UPGRADE_INFO (#06F1): Unreachable code removed

End Sub


Notice that the above code contains a new warning, which alerts you on the fact that some code has been removed. As usual, you can get rid of this new warning by means of a suitable DisableMessage pragma.

If you like the idea of automatically removing unreachable portions of code, you’ll surely appreciate the capability of doing the same with whole unused members, that is all the members that would be flagged with either the #0501 or #0511 warnings. In this case you need a RemoveUnusedMembers pragma, whose argument can be On (or omitted), Off, and Remarks – exactly like the RemoveUnreachableCode pragma seen above.
As before, you might want to remark out unused members first, to see how the code works

    '## RemoveUnusedMembers Remarks

and then delete them completely with
 
    '## RemoveUnusedMembers

When removing variables and methods, VB Migration Partner inserts a warning in the migrated code:

' UPGRADE_INFO (#0701): The 'XYZ' member has been removed because it
' isn't used anywhere in current application.

When running the tool to get the definitive VB.NET version of your code, it’s a good idea to get rid of this message with

    '## DisableMessage 0701


One final note: The ability to selectively indicate which members are to be considered as referenced makes VB Migration Partner stand out among other analysis/migration tool that don’t offer the ability to remove dead code or that allow you to enable this feature only at the project-level.

In fact, when migrating large and complex N-tier application, you can rarely remove all unreferenced members, because they might be referenced in client apps that aren’t part of the same project group. In practice, if a tool doesn’t give you the option to selectively decide which members should be removed, this feature is rarely usable when migrating large applications.



Exploring version 1.10 - If merging

clock November 20, 2008 00:51

If merging is VB Migration Partner's ability to merge two or more nested If...End If blocks using the AndAlso operator. This feature is enabled by means of the MergeIfs  pragma. To see how this feature works, consider the following VB6 code:

'## MergeIfs True
Sub Test(ByVal x As Integer, y As Integer, z As Integer)
    If x > 0 Then
        If y = 0 Then
            ' do something here
            ' …

            If z <> 0 Then
                If x < 100 Then
                    ' do something here
                    ' …

                End If
            End If
        End If

        If z = 0 Then If x = 10 Then Beep
    End If
End Sub

This is the VB.NET code that VB Migration Partner produces:

Public Sub TestX(ByVal x As Short, ByRef y As Short, ByRef z As Short)
    ' UPGRADE_INFO (#0581): Two nested If...End If blocks have been merged.
    If (x > 0) AndAlso (y = 0) Then
        ' do something here
        ' ...

       
        ' UPGRADE_INFO (#0581): Two nested If...End If blocks have been
        ' merged.

        If (z <> 0) AndAlso (x < 100) Then
            ' do something here
            ' ...

        End If

        ' UPGRADE_INFO (#0581): Two nested If...End If blocks have been
        ' merged.
         If (z = 0) AndAlso (x = 10) Then  Beep()
    End If
End Sub


The effect of this pragma is to simplify the structure of the code, reduce nesting level, and indirectly make it more readable. As you see, the MergeIfs pragma also affects nested single-line Ifs. A warning is automatically added, so that you can quickly revise all points where this optimization was applied.

By default, test expressions are enclosed between parenthesis, so that you easily see how each If statement contributed to the compound expression. If you don’t like these extra parenthesis, just specify False as the pragmas’s second argument:

    '## MergeIfs True, False

The MergeIfs pragma can have project-, file-, and method-level scope, therefore you can precisely define where this optimization should be applied. In most cases you can safely specify project-level If merging by including the following line in the VBMigrationPartner.pragmas file:

    '## project:MergeIfs True



Speed up your VB.NET collections

clock November 8, 2008 04:24

The VB6 Collection is quite a versatile object: it allows you to add and remove elements by their index like in an array, but without requiring to initialize it to a fixed number of elements. It also allows you to associate a string key to an element, so that you can quickly retrieve (or delete) that element without having to scan the collection one element at a time. (An important detail: string keys are compared in case-insensitive mode.)

Both the Upgrade Wizard and VB Migration Partner convert VB6 Collection objects into the Microsoft.VisualBasic.Collection objects, a choice that ensure the highest degree of compatibility and functional equivalence. A few articles around the Internet suggest that you should try to use a "pure" .NET collection, if possible. 

Unfortunately, this is easier said that done, because no other .NET collection exactly mimicks the behavior of the VB6 Collection. If you never use string keys you can translate a VB6 Collection using the ArrayList or the List(Of T) objects, except these objects have their lower index equal to 0 instead of 1, a detail that would force you to carefully scrutinize and fix your code as necessary. On the other hand, a VB6 collection that is only used with string key might be converted to VB.NET using the HashTable or the Dictionary(Of T), but be careful because these .NET collections deal with string key in case-sensitive mode and therefore aren't perfectly equivalent to the original VB6 Collection object. (TIP: you can use the CollectionUtils.CreateCaseInsensitiveHashtable method to create a case-insensitive dictionary.)

The main reason for replacing the VB.NET Collection with a native .NET collection is performance. The following table recaps the timing (in milliseconds) to perform a given operation 10,000 times. As you see, while the VB.NET collection is marginally faster than the VB6 collection in most cases, it is also much slower than the ArrayList collection at retrieving and deleting elements. (It is also slower than the Hashtable object, but the difference in absolute terms is virtually negligible.)

Test  VB6   VB.NET   ArrayList   Hashtable   VB6Collection  
Add(item) 0 1 0 n/a 2
Add(item,key) 47 7 n/a 4 6
Item(ndx) 203 215 0 n/a 0
Item(key) 16 7 n/a 0 9
For Each 15 7 0 0 1
Remove(ndx) 609 539 46 n/a 247
Remove(ndx) (*)    140 131 46 n/a 70
Remove(key) 16 12 n/a 3 3820
  (*) if items aren't associated with keys

 

The table suggests that, if you are sure that you never use keys, then you should replace a collection with an ArrayList object. Likewise, if you are sure that the collection is always going to contain elements of a specific type - for example, strings - you can improve performance and code robustness by replacing the collection with a List(Of T) object. However, if your code uses all the features of the VB6 Collection - namely, numeric indexes and string keys, you apparently have no choice other than using the standard VB.NET collection, right?

Well, not if you use VB Migration Partner. Starting with forthcoming version 1.10, you can replace a VB.NET collection with the new VB6Collection object, provided in our support library. As shown in previous table, this custom object is remarkably faster than the VB.NET collection in all tests (except when passing a key to the Remove method), and is as fast as the ArrayList object at retrieving elements by their numeric index.

When version 1.10 is released, you can replace one or more instances of the VB.NET Collection with our custom VB6Collection type by means of a ReplaceStatement pragma:

    '## ReplaceStatement Dim col As New VB6Collection
    Dim col As New Collection

Even better: we provide also a generic, strong-typed version of this collection, named VB6Collection(Of T). If you are sure that the collection is going to contain only element of a certain kind - integers, for example - you can write better code by using the generic version of this collection:

    '## ReplaceStatement Dim col As New VB6Collection(Of Integer)
    Dim col As New Collection

The VB6Collection class is 100% compatible with the original VB6 collection and the VB.NET collection, therefore you don't need to edit the VB.NET source code in any other way.



Exploring version 1.10 - The rename engine

clock October 10, 2008 07:50

Yesterday we finally released VB Migration Partner 1.10 BETA, which registered users can download from our site. Being a beta, we decided to keep the more recent "official" version (1.00.06) online, so that customers can have a choice.

The new version includes so many exciting refactoring features that allow you produce the kind of high-quality VB.NET code that experienced .NET develors can write, only much much faster. For example, you can now reliably merge nested IF blocks or convert On Error Goto and On Error Resume Next statements into Try-Catch blocks by just adding a new project-level pragma.

In this article I show how to leverage the brand-new symbol rename engine. A few customers asked for the ability to rename classes, methods, and controls during the migration process so that the VB.NET code would abide by their naming coding standards. Being the authors of a successful book entirely devoted to coding standard, we couldn't ignore the request.

Earlier versions of VB Migration Partner already included a rename engine - necessary to rename VB6 members that happened to be named after VB.NET keywords, among other things - and more expert users were able to hook into that engine by writing a VB Migration Partner Extender, i.e. a separate DLL that interacts with our tool during the three stages of the migration process (parsing, interpreting, code generation).

DECLARATIVE RULES
Writing an extender requires some familiarity with VB Migration Partner's object model, something most customers don't want toget involved into. (VB Migration Partner already comes with a few extenders that allow you to do complex auxiliary tasks, such as polishing UserControl classes and generating cumulative migration reports when migrating multiple projects in batch mode.)

Thus we decided to expose the inner rename engine to the outside and provide our users with the ability to write declarating rename rules based on regular expressions and stored in an XML file. Thanks to the internal rename engine the entire process was very simple and took a couple of days, most of which were spent to write a series of unit tests and a sample XML file containing a set of standard rename rules for you to use, study, and modify as you see fit.

Enabling this new feature is as simple as adding one ApplyRenameRules pragma anywhere in the VB6 project's source code:

    '## ApplyRenameRules  c:\myrules.xml

The argument is the path of the XML file containing all the rename rules in declarative format. If you omit the argument, the standard RenameRules.xml file that comes with VB Migration Partner 1.10 is used. Instead of adding an ApplyRenameRules pragma in each VB6 project you can store it in a VBMigrationPartner.pragmas file shared by one or more projects. Even better, you can store it in the master pragmas file located in VB Migration Partner's install folder, in which case all yourmigrated projects will use it. Here's a fragment of the XML file that comes with the standard installation:

 <?xml version="1.0" encoding="utf-8" ?>
<NamingRules>
  
   <!-- Components section -->
   <Symbol kind="Component" type="VB.Label" >
      <!-- ensure that we have a "lbl" prefix -->
      <Rule pattern="^(lbl)?" replace="lbl" />
      <!-- drop the "Label" suffix -->
      <Rule pattern="Label$" replace="" />
   </Symbol>

   <!-- more rules for all common controls -->

</NamingRules>

The kind attribute is used first to determine the kind of symbol the rule applies to. Valid names are Form, Class, Module, Type, UserControl, Enum, Component, Method (includes subs and functions), Field, Property, Event, EnumItem (the members of an Enum declaration), Constant, and Variables (includes, local variables and parameters.)

If kind is equal to Component or Variable, you can optionally include a type attribute, that specifies the VB6 type of the member to which the rule applies.In the above example, the type attribue is used to specify that the rule applies to all components of type VB.Label (the standard VB6 label). But you aren't confined to renaming controls: for example, the following rule renames all Integer variables named “i" into “index”

   <Symbol kind="Variable" type="Integer" >
      <Rule pattern="^i$" replace="index" ignoreCase="false" />
   </Symbol>

Renaming rules are based on regular expressions. The pattern attribute is used to decide whether the rule applies to a given member, and the replace attribute specifies how the member should be renamed. The ignoreCase optional attribute should be false if you want the pattern to be applied in case-sensitive mode (by default comparisons are case-insensitive). Finally, the changeCase optional attribute allows you to change the case of the result and can be equal to “lower”, “upper”, “pascal” (first char is uppercase), or “camel” (first char is lowercase).

Being Visual Basic a case-insensitive language, in most cases the default behavior is fine. However, consider the following renaming requirement: many VB6 developers preferred to use “C” as a prefix for all class names, but this guideline has been deprecated in VB.NET. Here’s a rule that drops the leading “C”, but only if it is followed by another uppercase character:

   <Symbol kind="Class" >
      <Rule pattern="^C(?=[A-Z])" replace="" ignoreCase="false" />
   </Symbol>

The changeCase attribute is useful to enforce naming rules that involve the case of identifiers. For example, a few developers like all-uppercase constant names; here's a rule that enforce this guideline:

   <Symbol kind="Constant" >
      <Rule pattern="^.+$" replace="${0}" changeCase="upper" />
   </Symbol>

 

There are a few other options that I don't cover here, if you are interested just have a look at the Renaming Program Members section of our manual. 

PROGRAMMATIC RULES
Declarative rules are easy to write and test - if you are familiar with regexes, at least - but there are times when you want to be in full control of how a symbol or a category of symbols are renamed. Achieving this feat with VB Migration Partner is quite easy:

1) Create a Class Library project inside Visual Studio 2005 or 2008 (you can use any .NET language, including VB.NET or C#)
2) Add a reference to the CodeArchitects.VBMigrationEngine.Dll
3) Create a public class, name it as you prefer, and make it inherit from CodeArchitects.VBMigrationLibrary.SpecialRuleBase
4) Override the GetSymbolNetName method
5) write the code that analyzes the properties of the VBSymbol passed as an argument and return its new name.
6) compile the DLL and drop it in VB Migration Partner's install folder.

For example, let's say that you want to replace the "lng" prefix with the "int" for all 32-bit integer variables defined inside a method named MyMethod. Here's the code that does it:

Public Class MyRenameRules
   Inherits SpecialRuleBase

   Public Overrides Function GetSymbolNetName(ByVal symbol As VBSymbol, _
         ByVal netName As String) As String
      If symbol.Kind = SymbolKind.LocalVar _
            AndAlso symbol.TypeName = "Long" _
            AndAlso symbol.ParentSymbol.Name = "MyMethod" Then
         If netName.StartsWith("lng") Then netName = "int" & netName.Substring(3)
      End If
      Return netName
   End Function
End Class


It couldn't be simpler, uh?

A word of caution: when you supercede VB Migration Partner's rename engine you are responsible for generating names that are both valid and unique. For example, if you drop either a prefix or a suffix, you should ensure that the new name is still unique, otherwise the generated VB.NET project won't compile. 



Subtle array cloning issues

clock October 5, 2008 10:46

As you may know, VB6 performs a copy of the array when assigning an array to another array variable. After the assignment, the source and destination arrays are distinct objects, as this code demonstrates:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer
dest() = source()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

Conversely, an array assignment under VB.NET just copies the array address:, the source and destination array variables point to the same set of elements. In other words, changing an element through either variable would affect the value seen by the other array variable.

For these reasons, both the Upgrade Wizard and earlier versions of VB Migration Partner automatically force a copy operation when converting an array assignment. This is how Upgrade Wizard converts the above VB6 code snippet:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer  
dest = source.Clone()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

It seems that adding a call to the Clone method solves the problem once and for all, but unfortunately this isn’t the case. As a matter of fact, there are several cases when the Upgrade Wizard (and probably other VB6-to-VB.NET conversion tools) doesn’t preserve functional equivalence with the original VB6 code.

For startes, a plain Clone method doesn't work correctly if the array you are assigning is an array of arrays (Under VB6, an array of arrays is a Variant array whose individual elements are arrays.) The problem here is that the Array.Clone method performs a shallow copy and doesn’t correctly copy nested arrays and the converted VB.NET application doesn’t work as it should. For this reason, starting with version 1.10, VB Migration Partner supports the CloneArray6 helper method, which can perform both a shallow copy and a deep copy (if the second argument is True, also nested arrays are copied):

   Dim arrayOfArrays() As Variant
    ' …
    Dim dest() As Variant
    dest() = CloneArray6(arrayOfArrays(), True)

The CloneArray6 method is available both in VB6 and VB.NET, but the VB6 version is a do-nothing version and doesn't change the application behavior. The CloneArray6 method can correctly handle arrays of arrays of any nesting level, arrays of structures, and more in general arrays of any ICloneable object.

Another case when UpgradeWizard generates bugged VB.NET code is when assigning a structure that contain one or more arrays. Consider this VB.NET code, generated by Upgrade Wizard when converting an assignment between two structures:

Structure MyUDT
    Public arr() As Integer
End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt
    ' udt and udt2 point to different structures, but
    ' they share the same set of array elements
    ' …
End Sub

It is obvious that the assignment between structures doesn’t work as in the original VB6 code, because the udt and udt2 structures are pointing to the same array: changing one element in the array in one structure affects the array pointed to by the other structure.Unlike Upgrade Wizard and other conversion tools, VB Migration Partner works around this issue by rendering the assignment as follows:

Structure MyUDT
    Public arr() As Integer

    Function Clone() As MyUDT
        Dim copy As MyUDT = Me
        copy.arr = CloneArray6(Me.arr)
       Return copy
    End Function

End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt.Clone()
    ' udt and udt2 are now completely disjoint objects, and share no arrays
    ' …
End Sub

You might bump into another elusive bug when invoking methods that store a reference to the array passed as an argument. The simplest example of this problem is when you store an array into multiple items of a Collection, as this VB6 code demonstrates:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' notice that here we reuse the same array…
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)
Debug.Print col(1)(0) & "," & col(2)(0)   ' => "Francesco,Giuseppe"

The Upgrade Wizard converts this code verbatim, but the resulting VB.NET code just doesn’t work as expected: both col(1) and col(2) elements actually point to the same array, therefore the text displayed in the Output window is not the one you hoped for:

Debug.WriteLine(col(1)(0) & "," & col(2)(0))  ' => "Giuseppe,Giuseppe"

A solution to this problem is to explicitly ReDim the array after each Add method, as follows:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' allocate a new memory block for array elements
ReDim values(1)
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)

However, this workaround adds unnecessary overhead to the VB6 code. Starting with VB Migration Partner version 1.10 you can avoid this overhead and have the most efficient behavior in both VB6 and VB.NET by using the CloneArray6 method:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(CloneArray6(values))
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(CloneArray6(values))

The bottom line: pay close attention to how arrays are assigned and copied in your VB.NET project and carefully scrutinize the code that your VB6-to-VB.NET migration tool generates in these cases. Else you might be spending hours trying to track down these elusive bugs.





Follow Francesco Balena on VB6 migration’s group on

LinkedIn


VB Migration Partner - Subscribe to our feed RSS  blog RSS
 
AddThis Feed Button
 
 

Home blog
 
AddThis Social Bookmark Button


 

Sign in

Search

Calendar

<<  September 2010  >>
SuMoTuWeThFrSa
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

Archive

Categories

 

Microsoft Regional Director
Microsoft is a registered trademark of Microsoft Corporation in the United States and other countries and is used under license from Microsoft
 

My Blogs


My programming tools



VB Migration Partner


VBMaximizer for VB6


Form Maximizer for .NET

 

My books


Programming Microsoft Visual Basic 6


Programming Microsoft Visual Basic 2005: The Language


Practical Guidelines and Best Practices for Microsoft Visual Basic .NET and Visual C# Developers