Previous | Index | Next 

3. Converting Language Elements



3. Converting Language Elements

This section illustrates how VB Migration Partner converts VB6 language elements and how you can apply pragmas to generate better .NET code.


3.1 Array bounds

VB Migration Partner can adopt five different strategies when converting arrays with non-zero LBound. Developers can enforce a specific strategy by means of the ArrayBounds pragma, as in:

    ' all arrays in scope (class or method) must have LBound
    = 0 '## ArrayBounds ForceZero ' ...except the arr variable, which is declared as
    a VB6Array object '## arr.ArrayBounds VB6Array

Please notice that there is a minor but important limitation in how you can apply this pragma to an array variable: if the array isn’t explicitly declared by means of a Dim statement and is only implicitly declared by means of a ReDim statement, pragma at the variable scope are ignored. An example is in order:

    Private Sub Test()
        '## ArrayBounds ForceZero '## arr.ArrayBounds VB6Array
        ReDim arr(1 To 10) As String
        Redim arr2(1 to 10) As Long
    End Sub

Both arrays are implicitly declared by means of a ReDim statement and lack of an explicit Dim keyword. The abovementioned rules states that the second pragma (scoped at the variable level) is ignored, therefore both arrays will be affected by the first pragma and will be forced to have a zero lower index:

        ' VB.NET
        Private Sub Test()
            Dim arr() As String     ' Implicitly declared array
            Dim arr2() As Integer   ' Implicitly declared array
            ReDim arr(10)
            ReDim arr2(10)
        End Sub
        
        // C#
        private void Test()
        {
            string[] arr = null;     // Implicitly declared array
            int[] arr2 = null;       // Implicitly declared array
            arr = new string[11];
            arr2 = new int[11];
        }
        

(This limitation is common to all pragmas that apply to array variables, not just the ArrayBounds pragma.)

Unchanged

The array is emitted as-is, and generates a compilation error in VB.NET if it has a nonzero lower bound. This is the default setting thus you rarely need to use an ArrayBounds pragma to enforce this mode (unless you want to override a pragma with broader scope).

ForceZero

When this option is selected, the array’s lower bound is changed to zero and the upper bound isn’t modified. This strategy is fine when the VB6 code processes the array using a loop such as this:

    For i = 1 To UBound(arr)
        …
    Next

Shift

VB Migration Partner decreases (or increases) both the lower and the upper bounds by the same value, in such a way the LBound becomes zero. For example, consider the following VB6 fragment

    '## arr.ArrayBounds Shift
    Dim arr(LoIndex To HiIndex) As String

is translated as follows:

     ' VB.NET
    Dim arr(0 To HiIndex - LoIndex) As String
     // C#
    string[] arr = new string[HiIndex – LoIndex + 1];
    

This approach is recommended when it is essential that the number of elements in the array doesn’t change after the migration, and is the right choice when the VB6 code processes the array using a loop such as this:

    For i = LBound(arr) To UBound(arr)
        …
    Next

Also, this is often the best strategy for arrays defined inside UDTs, if the UDT is often passed to a Windows API method (in which case it’s essential that their size doesn’t change).

VB6Array

If the array has a nonzero lower bound, VB Migration Partner replaces the array with an instance of the VB6Array(Of T) generic class. For example, the following VB6 statements:

    '## ArrayBounds VB6Array
    Dim arr(1 To 10) As String
    Dim arr2(1 To 10, -5 To 5) As Integer
    Dim arr3(0 To 10) As Long

are translated as follows:

    ' VB.NET
    Dim arr As New VB6Array(Of String)(1, 10)
    Dim arr2 As New VB6Array(Of Short)(1, 10, -5, 5)
    Dim arr3(0 To 10) As Integer
    
    // C#
    VB6Array arr = new VB6Array<string>(1, 10);
    VB6Array arr2 = new VB6Array<short>(1, 10, -5, 5);
    int[] arr3 = new int[11];

Instances of the VB6Array class behave much like regular arrays; they support indexes, assignments between arrays, and For Each loops:

    arr(1) = "abcde"
    For Each v In arr2
        sum = sum + v
    Next

Interestingly, when traversing a multi-dimensional array in a For Each loop, elements are visited in a column-wise manner (as in VB6), rather than in row-wise manner (as in .NET), thus no bugs are introduced if the processing order is significant.

In order to support VB6Array objects - and for other reasons as well, such support for Variants – VB Migration Partner translates the LBound and UBound methods to the LBound6 and UBound6 methods, respectively. Likewise, the Erase6, Redim6, and RedimPreserve6 methods are used to clear or resize arrays implemented as VB6Array objects. (These methods are defined in the language support library CodeArchitects.VBLibrary.dll.)

VB Migration Partner fully honors the Option Base directive:

    Option Base 1
    …
    '## ArrayBounds VB6Array
    Dim arr(10) As String
    numEls = UBound(arr)

which is translated as:

         ' VB.NET
        Dim arr As New VB6Array(Of String)(1, 10)
        numEls = UBound6(arr)
        
         // C#
        VB6Array<string> arr = new VB6Array<string>(1, 10);
	numEls = VB6Helpers.UBound(arr);

Unfortunately, a syntactical limitation of VB.NET prevents from using a VB6Array object to hold an array of UDTs (i.e. Structure blocks). More precisely, if a VB6Array contains structures, you can read a member of a structure stored in the VB6Array but you can’t assign any member. For example, consider the following VB.NET code:

    Structure MyUDT
        Public ID As Integer
    End Structure
    ...
    Sub Main()
        Dim arr As New VB6Array(Of MyUDT)(1, 10)
        Dim value As Integer = arr(1).ID        ' reading a member is
            OK ' assigning a member causes the following compilation error ' Expression is a
            value and therefore cannot be the target of an assignment.
        arr(1).ID = value
    End Sub
    

Therefore, in general you should avoid using the VB6Array option to convert an array of structures. However, this is just a rule of thumb and there can be exceptions to it. For example, if your code assigns whole structures to array elements (as opposed to individual structure members) and then reads their individual members, then storing structures in a VB6Array object is fine.

ForceVB6Array

This option is similar to the previous one, except it applies to all arrays in the pragma’s scope, regardless of whether the array has a non-zero LBound. This option is useful when the array is declared and created in two different steps – in this case the parser can’t decide which strategy to use by looking at the declaration alone - or when the developer knows that the array is going to be passed to a method that exposes parameters of VB6Array type. For example, consider this VB6 fragment:

    '## ArrayBounds VB6Array
    Dim arr() As String
    
    Sub Test()
        ReDim arr(1 To 10) As String
    End Sub

Remember that the VB6Array strategy applies only to arrays that have a nonzero lower index. However, when VB Migration Partner parses the arr variable it can’t decide whether it has a nonzero lower index, therefore it ignores the pragma and renders the variable as a standard array (thus causing a compilation error). This is the correct way to handle such a case:

    '## ArrayBounds ForceVB6Array
    Dim arr() As String
    
    Sub Test()
        ReDim arr(1 To 10) As String
    End Sub

which is rendered as:

    ' VB.NET
    Private arr As VB6Array(Of String)
    
    Public Sub Test()
        Redim6(arr, 1, 10)
    End Sub
    
    // C#
    Private VB6Array<string> arr;

    public void Test()
    {
	VB6Helpers.Redim(ref arr, 1, 10);
    }
    

Unlike other ArrayBounds options, you can apply the ForceVB6Array strategy to methods’ parameters and return values, either with a pragma inside the method with no explicit scope or with a pragma outside the method but that is scoped opportunely:

    Function GetValues(arr() As String) As Integer()
        '## ArrayBounds ForceVB6Array
        Dim res() as Integer
        …
        GetValues = res
    End Function
    
    '## InitArray.ArrayBounds ForceVB6Array
    Function InitArray() As Integer()
        …
    End Function

which is translated as follows:

    ' VB.NET
    Function GetValues(arr As VB6Array(Of String)) As VB6Array(Of Short)
        Dim res As New VB6Array(Of Short)
        …
        Return res
    End Function
    
    Function InitArray() As VB6Array(Of Short)
        …
    End Function
    
    // C#
    public VB6Array<short>GetValues(VB6Array<string> arr)
    {
	    VB6Array<short> res = new VB6Array<short>();
	    …
	    return res;
    }

    public VB6Array<short> InitArray()
    {
	    …
    }
    

When dealing with arrays having nonzero lower bound, another pragma can be quite useful. Consider the following VB6 code:

    Dim primes(1 To 10) As Long
    primes(1) = 1: primes(2) = 2: primes(3) = 3: primes(4) = 5: primes(5) = 7
    primes(6) = 11: primes(7) = 13: primes(8) = 17: primes(9) = 19: primes(10) = 23

You can use an ArrayBounds pragma to force a zero lower bound or to shift both bounds toward zero, but you need a separate ShiftIndexes pragma to account for the indexes used in the last two lines:

    '## primes.ArrayBounds Shift '## primes.ShiftIndexes false, 1
    Dim primes(1 To 10) As Long
    primes(1) = 1: primes(2) = 2: primes(3) = 3: primes(4) = 5: primes(5) = 7
    primes(6) = 11: primes(7) = 13: primes(8) = 17: primes(9) = 19: primes(10) = 23

this is the result of the migration to .NET:

    ' VB.NET
    Dim primes(9) As Integer
    primes(0) = 1: primes(1) = 2: primes(2) = 3: primes(3) = 5: primes(4) = 7
    primes(5) = 11: primes(6) = 13: primes(7) = 17: primes(8) = 19: primes(9) = 23
    
    // C#
    int[] primes = new int[10];
    primes[0] = 1; primes[1] = 2; primes[2] = 3; primes[3] = 5; primes[4] = 7;
    primes[5] = 11; primes[6] = 13; primes[7] = 17; primes[8] = 19; primes[9] = 23;
    

The first argument of the ShiftIndexes is False if the delta value specified in the second argument must be applied only to constant indexes, True if the delta value must be applied even when the index is a variable or an expression. Using True or False makes a difference when the array is referenced from inside a loop. Consider this example:

    '## powers.ArrayBounds Shift '## Fibonacci.ArrayBounds Shift '##
        powers.ShiftIndexes true, 1 '## Fibonacci.ShiftIndexes false, 1
    
    Dim powers(1 To 10) As Double
    Dim Fibonacci(1 To 10) As Double
    Dim n As Integer
    
    powers(1) = 2
    For n = 2 To 10
        powers(n) = powers(n – 1) * 2
    Next
    
    Fibonacci(1) = 1: Fibonacci(2) = 1
    For n = LBounds(Fibonacci) + 2 To Ubound(Fibonacci)
        Fibonacci(n) = Fibonacci(n – 2) + Fibonacci(n – 1)
    Next

The difference is in how the loop bounds are specified for the two arrays: for the powers array the loop bounds are constant values, therefore it is necessary to compensate in the indexes inside the loop; for the fibonacci array the loop bounds are specified in terms of LBound and UBound functions, therefore the indexes inside the loop should not be altered. This is the resulting VB.NET code:

    Dim powers(9) As Double
    Dim Fibonacci(9) As Double
    Dim n As Short
    
    powers(0) = 2
    For n = 2 To 10
        powers(n - 1) = powers(n – 1 - 1) * 2
    Next
    
    Fibonacci(0) = 1: Fibonacci(1) = 1
    For n = LBounds(Fibonacci) + 2 To Ubound(Fibonacci)
        Fibonacci(n) = Fibonacci(n – 2) + Fibonacci(n – 1)
    Next

Notice that the ShiftIndexes pragma support up to three delta values, thus you can shift indexes also for 2- and 3-dimension arrays, as in this code:

    '## mat.ArrayBounds Shift '## mat.ShiftIndexes false, 1, -1
    Dim mat(1 To 10, -1 To 1) As Double

Delta values can be negative, can be variables and expressions.

The first argument of ShiftIndexes can also be a regular expression that specifies more precisely to which expressions the pragma should be applied. For example, consider the following VB6 code:

    '## arr.ArrayBounds Shift '## arr.ShiftIndexes "(k|row)", 1, 1,
        ""
    Dim arr(1 To 10, 1 To 20) As Integer
    Dim k As Integer, row As Integer, col As Integer
    
    arr(1, 1) = 0
    For k = 2 To 10
        arr(k, 1) = arr(k – 1) + 10
    Next
    
    For row = 1 to 10
        For col = LBound(arr, 2) + 1 To UBound(arr, 2)
            arr(row, 1) = arr(row, 1) + arr(row, col)
        Next
    Next

In this case you want to apply the index adjustments only when the index expression is “k” or “row”, hence the regular expression used in the ShiftIndexes pragma. Here’s the result after then conversion to VB.NET:

    Dim arr(9, 19) As Short
    Dim k As Short, row As Short, col As Short
    
    arr(0, 0) = 0
    For k = 2 To 10
        arr(k - 1, 0) = arr(k - 1 - 1, 0) + 10
    Next
    
    For row = 1 To 10
        For col = LBound6(arr, 2) + 1 To UBound6(arr, 2)
            arr(row - 1, 0) = arr(row - 1, 0) + arr(row - 1, col)
        Next
    Next

Notice that numeric indexes are always affected by the ShiftIndexes pragma, but symbolic numeric constants are affected only you specify a suitable regular expression (or True) in the first argument.




3.2 Default members

The way VB Migration Partner deals with default members depends on how and where the member is defined, and how it is referenced.

Default property definitions

When converting a the definition of a property that is marked as the default member of its class, VB Migration Partner adds the Default keyword if the property has one or more arguments; if the property has no parameters, an upgrade warning is issued, because .NET doesn’t support default properties with zero parameters. For example, if this property is the default member of its class:

    Public Property Get Text() As String
        Text = "..."
    End Property

VB Migration Partner converts it as:

    ' VB.NET
    <System.Runtime.InteropServices.DispId(0)> _ 
    Public ReadOnly Property Text() As String
        ' UPGRADE_WARNING (#0154): Default properties with zero arguments
            aren't supported.
        Get
            Return "..."
        End Get 
    End Property
 
    // C#   
    [System.Runtime.InteropServices.DispId(0)]
    public string Text
    {
      	// UPGRADE_WARNING (#0154): Default properties with zero arguments aren't supported.
      get
	  {
	  return "...";
 	  }
    }   
    

Notice that the Property block is tagged with a DispID(0) attribute, so that COM clients see the property as the default member.

Default method and field definitions

When converting a default method or field’s definition, VB Migration Partner doesn’t modify the definition, except for the addition of the DispID attribute. In this case no Default keyword can be used, because this keyword can be applied only to VB.NET or C# properties.

References to default members in early-bound mode

If the VB6 code references a default property, method, or field through a strong-typed variable, the code generator correctly adds the name of the member. The conversion works correctly for regardless of whether the member belongs to a class defined in the current project, in another project in the solution, or in a type library.

Accessing default members in late-bound mode

If the VB6 code references a default property, method, or field through a Variant, Object, or Control variable, by default VB Migration Partner emits a warning. For example, the following VB6 code

         Sub Test(ByVal obj As Object)
            MsgBox obj
            obj = "new value"
        End Sub    
    

is translated as:

        ' VB.NET
        Sub Test(ByVal obj As Object)
        ' UPGRADE_WARNING (#0354): Unable to read default member of symbol
            'obj'. ' Consider using the GetDefaultMember6 helper method.
        MsgBox6(obj)
        ' UPGRADE_WARNING (#0364): Unable to assign default member of
            symbol 'obj'. ' Consider using the SetDefaultMember6 helper method.
        obj = "new value"
        
        End Sub
        
        // C#
        Public void Test(object obj)
        {
                // UPGRADE_WARNING (#0354): Unable to read default member of symbol 'obj'.
                // Consider using the GetDefaultMember6 helper method.
                VB6Helpers.MsgBox(obj);
                / UPGRADE_WARNING (#0364): Unable to assign default member of symbol 'obj'.
                / Consider using the SetDefaultMember6 helper method.
                obj = "new value";
        }    
        

The .NET code compiles correctly but delivers bogus results at runtime. You can generate better code by means of the DefaultMemberSupport pragma:

         Sub Test(ByVal obj As Object)
            '## DefaultMemberSupport
            MsgBox obj
            obj = "new value"
        End Sub
        

which delivers this .NET code:

        ' VB.NET
        Sub Test(ByVal obj As Object)
            MsgBox6(GetDefaultMember6(obj))
            SetDefaultMember6(obj, "new value")
        End Sub

The GetDefaultMember6 and SetDefaultMember6 methods are defined in the VBMigrationPartner_Support.bas module. These methods discover and resolve the default member reference at runtime and work correctly also if the default member takes one or more arguments. For example, the following VB6 code:

         Sub Test(ByVal obj As Object)
            '## DefaultMemberSupport
            Dim res As Integer
            x = obj(1)
            obj(1) = res + 1
        End Sub

translates to:

        ' VB.NET
        Sub Test(ByVal obj As Object)
           Dim res As Short = GetDefaultMember6(obj, 1)
           SetDefaultMember6(obj, 1, res + 1)
        End Sub
    
        // C#
        public void Test(object obj)
        {
	    short res = VB6Helpers.GetDefaultMember(obj, 1);
	    VB6Helpers.SetDefaultMember(obj, 1, 1234);
        }

The discovery process is carried out only the first time the GetDefaultMember6 and SetDefaultMember6 process an object of given type, because the result of the discovery is reused by subsequent calls on variables of the same type. All subsequent references are faster and add no noticeable overhead to the late-bound call.




3.3 GoSub, On GoTo, and On GoSub keywords

VB.NET and C# don’t support GoSub, On Goto, and On Gosub statements. VB Migration Partner, however, is able to correctly convert these VB6 keywords, at the expense of code readability and maintainability. For this reason we strongly recommend that you edit the VB6 application to get rid of all the statements based on these keywords.

Anyway, you can surely take advantage of VB Migration Partner ability to handle these statements during the early stages of the migration process. Let’s start with the following VB6 method:

    Sub Main()
    
        GoSub First
        GoSub Second
        Exit Sub
    
    First:
        Debug.Print "First"
        GoSub Third
        Return
        
    Second:
        Debug.Print "Second"
        ' flow into the next section
        
    Third:
        Debug.Print "Third"
        Return
    
    End Sub

This is how VB Migration Partner converts the code to VB.NET:

    Public Sub Main()
        Dim _vb6ReturnStack As New System.Collections.Generic.Stack(Of Integer)
        
        _vb6ReturnStack.Push(1): GoTo First
        
    ReturnLabel_1:
        _vb6ReturnStack.Push(2): GoTo Second
    ReturnLabel_2:
        Exit Sub
            
    First:
        Debug.WriteLine("First")
        _vb6ReturnStack.Push(3): GoTo Third
    ReturnLabel_3:
        GoTo _vb6ReturnHandler
            
    Second:
        Debug.WriteLine("Second")
        ' flow into the next section
            
    Third:
        Debug.WriteLine("Third")
        GoTo _vb6ReturnHandler
        
        Exit Sub
    
    _vb6ReturnHandler:
        Select Case _vb6ReturnStack.Pop()
            Case 1: GoTo ReturnLabel_1
            Case 2: GoTo ReturnLabel_2
            Case 3: GoTo ReturnLabel_3
        End Select
    End Sub

As you can see, the GoSub keyword is transformed into a GoTo keyword that uses the _vb6ReturnStack variable to “remember” where the Return statement must jump to. The _vb6ReturnStack variable holds a stack that keeps the ID of the return address, a 32-bit integer from 1 to N, where N is the number of GoSub statements in the current method.

The Return keyword is transformed into a GoTo keyword that points to the _vb6ReturnHandler section, where the return address is popped off the stack and used to go back to the statement that immediately follows the GoSub.

Converting a calculated GoSub delivers similar code, except that the GoSub becomes a GoTo pointing to a Select Case block. For example, the following VB6 code:

    Dim x As Integer
    x = 2
    On x GoSub First, Second, Third
    Exit Sub

is converted as:

        Dim x As Short = 2
        _vb6ReturnStack.Push(4): GoTo OngosubTarget_1
    ReturnLabel_4:
        ' ... (other portions omitted for brevity)
        
    OngosubTarget_1:
    Select Case x
        Case 1: GoTo First
        Case 2: GoTo Second
        Case 3: GoTo Third
        Case Is <= 0, Is <= 255: GoTo ReturnLabel_4
        Case Else: Err.Raise(5)
    End Select

On…GoTo statements are converted in a similar way.

Important note: We can’t emphasize strongly enough that the code that VB Migration Partner delivers should be never left in a production application, because it is unreadable and hardly maintainable. For this reason, all occurrences of GoSub, On GoTo, and On GoSub keywords cause a warning to be emitted in the generated .NET project. (This warning has been dropped in examples shown in this section.)




3.4 Fixed-length strings (FLSs)

A fixed-length strings (FLS) is converted to an instance of the VB6FixedString class. This class exposes a constructor (which takes the string’s length) and the Value property (which takes or returns the string’s value). For example, the following VB6 code:

    Dim fs As String * STRINGSIZE
    fs = "abcde"

is converted as follows:

    ' VB.NET
    Dim fs As New VB6FixedString(STRINGSIZE)
    fs.Value = "abcde"
    
    // C#    
    VB6FixedString fs = new VB6FixedString(STRINGSIZE);
    fs.Value = "abcde";
    

The Value property returns the actual internal buffer, an important detail which ensures that VB6FixedString instances work well when they are passed to Windows API methods that store a result in a ByVal string argument. Thanks to this approach, calls that pass FLS arguments to Declare methods work correctly after the migration to VB.NET.

Arrays of FLSs require a special treatment and are migrated differently. Consider the following VB6 code:

    Dim arr(10) As String * 256
    arr(0).Value = "abcde"

becomes:

    ' VB.NET
    Dim arr() As VB6FixedString_256 = CreateArray6(Of VB6FixedString_256)(0, 10)
    arr(0).Value = "abcde"
        
    // C#
    VB6FixedString_256[] arr = VB6Helpers.CreateArray(0, 10)
    arr[0].Value = "abcde";
    

where VB6FixedString_256 a special class in the VisualBasic6.Support.vb or VisualBasic6.Support.cs file:

    <StructLayout(LayoutKind.Sequential)> _ 
    Public Class VB6FixedString_256
        Private Const SIZE As Integer = 256
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=SIZE)> _
        Private Buffer As String = VB6FixedString.GetEmptyBuffer(SIZE)
        
        Public Property Value() As String
            Get
                Return VB6FixedString.Truncate(Buffer, SIZE, ControlChars.NullChar)
            End Get            
            Set(ByVal value As String)
                Buffer = VB6FixedString.Truncate(value, SIZE)
            End Set
        End Property 
    End Class

A distinct VB6FixedString_NNN class is generated for each distinct size that appears in FLS declarations inside the current project.

As you see above, the FLS array is initialized by means of a call to the CreateArray6 method. This method ensures that all the elements in the array are correctly instantiated, so that no NullReference exception is thrown when accessing any element.

If the array has a nonzero lower index, you can use the ArrayBounds pragma to maintain full compatibility with VB6:

    '## arr.ArrayBounds ForceVB6Array
    Dim arr(1 to 10) As String * 256

which is translated to:

    ' VB.NET
    Dim arr As New VB6ArrayNew(Of VB6FixedString_256)(1, 10)
    
    // C#
    VB6ArrayNew<vb6fixedstring_256> arr = new VB6ArrayNew<VB6FixedString_256>(1, 10);
    

The VB6ArrayNew generic class differs from the VB6Array class in that it automatically creates an instance of the T type for each element of the array. Using a plain VB6Array type would throw a NullReference exception when accessing any array element.

Finally, notice that you can force a scalar (not array) FLS to be rendered as a VB6FixedString_NNN class by means of a SetStringSize pragma, as in this example:

    '## s.SetStringSize 128
    Dim s As String * 128

Such a pragma can be useful if you plan to assign a FLS to an array of FLSs. In practice, however, applying this pragma to scalar FLSs is rarely necessary.




3.5 Type…End Type blocks (UDTs)

The main problem in converting Type…End Type blocks – a.k.a. User-Defined Types or UDT – to .NET is that a .NET structure can’t include a default constructor or fields with initializers. This limitation makes it complicated to convert UDTs that include initialized arrays, auto-instancing (As New) object variables, and fixed-length strings, because these elements need to be assigned a value when the UDT is created.

VB Migration Partner solves this problem by generating a structure with a constructor that takes one dummy parameter and by ensuring that this constructor is used whenever a new instance of the UDT is created. Consider the following UDT:

    Type TestUdt
        ' use VB6Array for all arrays with nonzero LBound. '## ArrayBounds
            VB6Array
        a As Integer
        b As New Widget
        c() As Long
        d(10) As Double
        e(1 To 10) As Currency
        f As String * 10
        g(10) As String * 10
        h(1 To 10) As String * 10
    End Type

This is how it is translated to VB.NET:

    Structure TestUdt
        Public a As Short
        Public b As Object
        Public c() As Integer
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=11)> _
        Public d() As Double
        Public e As VB6Array(Of Decimal)
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=10)> _
        Public f As VB6FixedString
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=11)> _
        Public g() As VB6FixedString_10
        Public h As VB6ArrayNew(Of VB6FixedString_10)
        
        Public Sub New(ByVal dummyArg As Boolean)
            InitializeUDT()
        End Sub
        
        Public Sub InitializeUDT()
            b = New Object
            ReDim d(10)
            e = New VB6Array(Of Decimal)(1, 10)
            f = New VB6FixedString(10)
            g = CreateArray6(Of VB6FixedString_10)(0, 10)
            h = New VB6ArrayNew(Of VB6FixedString_10)(1, 10)
        End Sub
    End Structure

Note: Previous example uses the ArrayBounds VB6Array pragma only to prove that VB6Array objects are initialized correctly; in most cases, the most appropriate setting for this pragma inside UDTs is Shift, because this setting ensures that the size of UDTs doesn’t change during the migration.

Notice that the constructor takes an argument only because it is illegal to define a parameterless constructor in a Structure, but the argument itself is never used. Such a constructor is generated only if the UDT contains one or more members that require initialization, as in previous listing.


The key advantage of having this additional constructor is that it is possible to declare and initialize a UDT in a single operation. For example, the following VB6 statement:

    Dim udt As TestUdt

is translated to:

    ' VB.NET
    Dim udt As New TestUdt(True)
    
    // C#
    TestUdt udt = new TestUdt(true);
    

VB Migration Partner supports nested UDTs, too. For example, the following VB6 definition:

    Type TestUdt2
        ID As Integer
        Data As TestUdt
    End Type

is converted to VB.NET as:

    Friend Structure TestUdt2
        Public ID As Short
        Public Data As TestUdt
        
        Public Sub New(ByVal dummyArg As Boolean)
            InitializeUDT()
        End Sub
        
        Public Sub InitializeUDT()
            Data = New TestUdt(True)
        End Sub
    End Structure

A special case occurs when migrating a function or a property that returns a UDT. In this case, the return value is automatically initialized at the top of the code block, as this example demonstrates:

    Function GetUDT() As TestUdt
        GetUDT.InitializeUDT()
        ...
    End Function

Arrays of UDTs are migrated correctly, even if the UDT requires initialization. In such cases, in fact, the array is initialized by means of the CreateArray6 method, which ensures that the InitializeUDT method be called for each element in the array:

    Dim arr() As TestUdt = CreateArray6(Of TestUdt)(0, 10)

In some cases, a FLS defined inside a UDT must be rendered as a standard string rather than a VB6FixedString object. This replacement is necessary, for example, when the UDT is passed to an external method defined by a Declare statement, because the external method expects a standard string.

You can force VB Migration Partner to migrate a FLS as a standard string by means of the UseSystemString pragma. A FLS affected by this pragma is rendered as a private regular System.String field which is wrapped by a public property which ensures that values being assigned are always correctly truncated or extended. For example, consider the following VB6 code:

    Public Type CDInfo
        '##Title.UseSystemString
        Title As String * 30
        Artist As String * 30
    End Type

Even though the two items are declared in the same way, the UseSystemString pragma changes the way the Title item is rendered to VB.NET:

    Friend Structure CDInfo
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=30)> _
        Private m_Title As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=30)> _
        Public Artist As VB6FixedString
        
        Public Sub New(ByVal dummyArg As Boolean)
            InitializeUDT()
        End Sub
        
        Public Sub InitializeUDT()
            m_Title = VB6FixedString.GetEmptyBuffer(30)
            Artist = New VB6FixedString(30)
        End Sub
        
        Public Property Title() As String
            Get
                Return VB6FixedString.Truncate(m_Title, 30, ControlChars.NullChar)
            End Get
            Set(ByVal value As String)
                m_Title = VB6FixedString.Truncate(value, 30)
            End Set
        End Property
    End Structure

The UseSystemString pragma can take a boolean value, where True is the default value assumed if you omit the argument. For example, in the following UDT all items are rendered as regular strings except the Year argument:

    Public Type MP3Tag
        '## UseSystemString
        Title As String * 30
        Artist As String * 30
        Album As String * 30
        '## Year.UseSystemString False
        Year As String * 4
    End Type



3.6 Auto-instancing variables

By default, a declaration of an auto-instancing variable is migrated to VB.NET verbatim. For example, the following statement is translated “as-is”:

    Dim obj As New Widget

In most cases, this behavior is correct, even though the VB6 and VB.NET semantics are different. More precisely, a VB6 auto-instancing variable supports lazy instantiation and can’t be tested against the Nothing value, because the very reference to the variable recreates the instance if necessary. The .NET semantics become clear if you convert to C# instead:

    Widget obj = new Widget();

VB Migration Partner can generate code that preserves the VB6 semantics, if required. This behavior can be achieved by means of the AutoNew pragma, which can be applied at the project, class, method, and variable level.

The actual effect of this pragma on local variables is different from the effect on class-level fields: 

    Function GetValue() As Integer
        '## obj.AutoNew True
        Dim obj As New Widget
        ' ...
        obj.Value = 1234
        ' ...
        GetValue = obj.Value
    End Function

An auto-instancing local variable that is under the scope of an AutoNew pragma is declared without the “New” keyword; instead, all its occurrences in code are automatically wrapped by the special AutoNew6 method:

     ' VB.NET            
    Function GetValue() As Short
        Dim obj As Widget
        ' ...
        AutoNew6(obj).Value = 1234
        ' ...
        GetValue = AutoNew6(obj).Value
    End Function
    
    // C#
    public short GetValue() 
    {
        Widget obj = null;
        // ...
        VB6Helpers.AutoNew(ref obj).Value = 1234;
        // ...
        return VB6Helpers.AutoNew(ref obj).Value;
    }
    

The AutoNew6 method ensures that the variable abides by the “As New” semantics: a new Widget is instantiated (and assigned to the obj variable) when the method is called the first time and it is automatically recreated if the variable is set to Nothing.

A class-level field under the scope of an AutoNew pragma is rendered as a property, whose getter block ensures that the lazy instantiation semantics is honored. For example, if obj is a class-level auto-instancing field, VB Migration Partner converts as follows:

    ' VB.NET
    Public Property obj() As Widget
        Get
            If obj_InnerField Is Nothing Then obj_InnerField = New Widget ()
            Return obj_InnerField
        End Get
        Set(ByVal value As Widget)
            obj_InnerField = value
        End Set
    End Property
    Private obj_InnerField As Widget
    
    // C#
    public Widget obj
    {
        get
        {
            if (obj_InnerField == null) obj_InnerField = new Widget();
            return obj_InnerField;
        {
        set
        {
            obj_InnerField = value;
        }
    }
    private Widget obj_InnerField;
    

VB6 also supports arrays of auto-instancing elements, and VB Migration Partner fully supports them. If either an appropriate ArrayBounds or AutoNew pragma are in effect for such an array, VB Migration Partner renders it as an instance of the VB6ArrayNew(Of T) type. For example, the following VB6 code:

     '## arr.AutoNew
    '## arr.ArrayBounds ForceVB6Array
    Dim arr(10) As New TestClass()

is translated as

     ' VB.NET
    Dim arr() As New VB6ArrayNew(Of TestClass)(0, 10)
    
    // C#
    VB6ArrayNew arr = new VB6ArrayNew(0, 10);  
    

The VB6ArrayNew generic type behaves exactly as VB6Array, except the former automatically ensures that all its elements are instantiated before they are accessed.




3.7 Declare statements

VB Migration Partner is able to automatically solve most of the issues related to converting VB6 Declare statements to .NET. More specifically, in addition to data type conversion (e.g. Integer to Short, Long to Integer), the code generator adopts the following techniques:

“As Any” parameters

If the Declare statement includes an “As Any” parameter, VB Migration Partner takes note of the type of values passed to it and the passing mechanism used (ByRef or ByVal), and then generates one or more overloads for the Declare statement. An example of a Windows API method that requires this treatment is SendMessage, which can take an integer or a string in its last argument:

    Private Declare Function SendMessage Lib "user32.dll" _ 
        Alias "SendMessageA" (ByVal hWnd As Long, _ 
        ByVal wMsg As Long, ByVal wParam As Long, _ 
        lParam As As Any) As Long 
        
    Sub SetText() 
        ' here we pass a string 
        SendMessage Text1.hWnd, WM_SETTEXT, 0, ByVal "new text" 
    End Sub 
    
    Sub CopyToClipboard() 
        ' here we pass a 32-bit integer 
        SendMessage Text1.hWnd, WM_COPY, 0, ByVal 0 
    End Sub

This is the VB.NET code that VB Migration Partner generates. As you see, the As Any argument is gone and two overloads for the SendMessage method have been created:

    Private Declare Function SendMessage Lib "user32.dll" _ 
        Alias "SendMessageA" (ByVal hWnd As Integer, _ 
        ByVal wMsg As Integer, ByVal wParam As Integer, _ 
        ByVal lParam As String) As Integer 
        
    Private Declare Function SendMessage Lib "user32.dll" _ 
        Alias "SendMessageA" (ByVal hWnd As Integer, _ 
        ByVal wMsg As Integer, ByVal wParam As Integer, _ 
        ByVal lParam As Integer) As Integer

AddressOf keyword and callback parameters

If client code uses the AddressOf keyword when passing a value to a 32-bit parameter, VB Migration Partner assumes that the parameter takes a callback address and overloads the Declare to take a delegate type. For example, consider the following VB6 code inside the ApiMethods BAS module:

    Declare Function EnumWindows Lib "user32" _ 
        (ByVal lpEnumFunc As Long, ByVal lParam As Long) As Long 
        
    Sub TestEnumWindows()  
        EnumWindows AddressOf EnumWindows_CBK, 0 
    End Sub 
    
    ' The callback routine 
    Function EnumWindows_CBK(ByVal hWnd As Long, _ 
        ByVal lParam As Long) As Long 
        ' Store the window handle and return 1 to continue enumeration
        
        ' ... 
        EnumWindows_CBK = 1 
    End Function

This is how VB Migration Partner converts the code to VB.NET:

    ' List of Public delegates used for callback methods 
    Public Delegate Function EnumWindows_CBK(ByVal hWnd As Integer, ByVal lParam As Integer) As Integer
    
    Friend Module Module1 
        Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As Integer, _ 
            ByVal lParam As Integer) As Integer
        Declare Function EnumWindows Lib "user32" (ByVal lpEnumFunc As EnumWindows_CBK, _ 
            ByVal lParam As Integer) As Integer
        
        Public Sub TestEnumWindows() 
            EnumWindows(AddressOf EnumWindows_CBK, 0) 
        End Sub 
        
        ' The callback routine 
        Function EnumWindows_CBK(ByVal hWnd As Integer, ByVal lParam As Integer) As Integer
            ' Store the window handle and return 1 to continue enumeration
            
            ' ...
            Return 1 
        End Function 
    End Module

Notice that only the Declare needs to be overloaded: the code that use the Declare doesn’t require any special treatment.

Windows API methods that can be replaced by calls to .NET methods

VB Migration Partner is aware that calls to some specific Windows API methods can be safely replaced by calls to static methods defined in the .NET Framework, as is the case of Beep (which maps to Console.Beep), Sleep (System.Threading.Thread.Sleep), and a few others. When a call to such a Windows API method is found, it is automatically replaced by the corresponding call to the .NET Framework.

Windows API methods that have a recommended .NET counterpart

VB Migration Partner comes with a database of about 300 Windows API methods, where each method is associated with the recommended replacement for .NET. If the parser finds a Declare in this group, a warning is emitted, as in this example:

    ' UPGRADE_INFO (#0241): You can replace calls to the GetSystemDirectory'
        unmanaged method ' with the following .NET member(s): System.Environment.SystemDirectory
    Private Declare Function GetSystemDirectory Lib "kernel32.dll" _    
        Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, _    
        ByVal nSize As Integer) As Integer



3.8 Variant and Control variables

By default, Variant variables are converted to Object variables. This default behavior can be changed by means of the ChangeType pragma, which changes the type of all Variant members (within the pragma’s scope) into something else. More specifically, developers can decide that Variant variables are rendered using the special VB6Variant type, as in this code:

    '## ChangeType Variant, VB6Variant
    Dim v As Variant
    Dim arr() As Variant

which is translated to:

    Dim v As VB6Variant
    Dim arr() As VB6Variant

IMPORTANT NOTE: the VB6Variant data type isn't supported when converting to C#.

The VB6Variant type (defined in the language support library) mimics the behavior of the VB6 Variant type as closely as possible, for example by providing support for the special Null and Empty values.

VB6Variant values can be tested by means of the IsEmpty6 and IsNull6 methods, and are recognized by the VarType6 method. Optional parameters of type Variant can be tested with the IsMissing6 function, similarly to what VB6 apps can do.

The VB6Variant class provides a limited support for null propagation in math and string expressions. This ability is achieved by overloading all math and strings operators. The degree of support offered is enough complete for most common cases, but there might be cases when the result differs from VB6.

By default VB Migration Partner translates variables and parameters of type Controls to Object variables and parameters. We opted for this approach because the VB6 Control is actually an IDispatch object and inherently requires late binding, as in this example:

    ' Make all the textboxes on form read-only
    Dim ctrl As Control
    For Each ctrl In Me.Controls
        If TypeOf ctrl Is TextBox Then ctrl.Locked = True
    Next

If the ctrl variable were rendered as a System.Windows.Forms.Control object, the code wouldn’t compile because the Control class doesn’t expose a Locked property. By contrast, VB Migration Partner renders the variable as an Object variable and produces VB.NET code that compiles and executes correctly:

    ' Make all the textboxes on form read-only
    Dim ctrl As Object
    For Each ctrl In Me.Controls6
        If TypeOf ctrl Is VB6TextBox Then ctrl.Locked = True
    Next

In other circumstances, however, changing the default behavior might deliver more efficient code. For example, consider this VB6 code:

    '## ctrl.SetType Control
    Dim ctrl As Control
    For Each ctrl In Me.Controls
        If TypeOf ctrl Is TextBox Or TypeOf ctrl Is ComboBox Then
            ctrl.Text = ""
        End If
    Next

In this case, you can leverage the fact that the System.Windows.Forms.Control class exposes the Text property, thus you can add a SetType pragma that changes the type for the ctrl variable. This is the resulting VB.NET code:

    Dim ctrl As Control
    For Each ctrl In Me.Controls6
        If TypeOf ctrl Is VB6TextBox Or TypeOf ctrl Is VB6ComboBox Then
            ctrl.Text = ""
        End If
    Next

The ctrl variable is now strong-typed and the .NET code runs faster.

Please notice the difference between the ChangeType pragma (which affects all the variables and parameters of a given type) and the SetType pragma (which affects only a specific variable or parameter).

NOTE: starting with version 1.52, the VB6Variant class is not officially supported.




3.9 Classes and Interfaces

VB Migration Partner deals with VB6 classes and interfaces in a manner that resembles the way interfaces and coclasses work in COM. More specifically, if a VB6 class named XYZ appears in an Implements statement, anywhere in the current solution, then VB Migration Partner generates an Interface named XYZ and renames the original class as XYZClass. For example, assume that you have the following IPlugIn class:

    ' the IPlugin class
    Sub Execute()
        ' execute the task ... 
    End Sub
    
    Property Get Name() As String
        ' return Name here ...
    End Property

Next, assume that the IPlugIn class is referenced by an Implements statement in the SamplePlugIn class, defined elsewhere in the current project or solution:

    ' inside the SamplePlugIn class
    Implements IPlugIn

Under these assumptions, this is the VB.NET code that VB Migration Partner generates:

    Public Class IPlugInClass
        Implements IPlugIn
        
        Sub Execute() Implements IPlugIn.Execute
            ' execute the task ... 
        End Sub
        
        ReadOnly Property Name() As String Implements IPlugIn.Name
            Get
                ' return Name here ...
            End Get
        End Property
    End Class
    
    Public Interface IPlugIn
        Sub Execute()
        ReadOnly Property Name() As String
    End Interface

This rendering style minimizes the impact on code that references the JPlugIn type. For example, the following VB6 code:

    Sub CreatePlugIn(itf As IPlugIn)
        Set itf = New SamplePlugIn
    End Sub

is converted to a piece of VB.NET code that is virtually identical, except for the Set keyword being dropped:

    Sub CreatePlugIn(ByRef itf As IPlugIn)
        itf = New SamplePlugIn()
    End Sub

References to the IPlugIn type are replaced by references to the IPlugInClass name only when the class name follows the New keyword, as in this VB6 code:

    Dim itf As New IPlugIn

which translates to

    Dim itf As New IPlugInClass

You’ve seen so far that when a VB6 class appears in an Implements statement, by default VB Migration Partner takes a conservative approach and creates a both a .NET class and an interface. This approach ensures that the migrated app works correctly in all cases, including when the VB6 class is actually instantiated. In most real cases, however, a type used in an Implements statement never appears as an operand for the New keyword; therefore generating the class is of no practical use. You can tell VB Migration Partner not to generate the class by means of a ClassRenderMode pragma:

     ' render the current class only as an interface 
    '## ClassRenderMode Interface

The ClassRenderMode pragma can’t be applied at the project level and has to be specified for each distinct class.




3.10 Finalization and disposable classes

All VB6 and COM objects internally manage a reference counter: this counter is incremented each time a reference to the object is created and is decremented when the reference is set to Nothing. When the counter reaches zero it’s time to fire the Class_Terminate event and destroy the object. This mechanism is known as deterministic finalization, because the instant when the object is destroyed can be precisely determined.

.NET objects don’t manage a reference counter and objects are physically destroyed only some time after all references to them have been set to Nothing, more precisely when a garbage collection is started. One of the biggest challenges in writing a VB6 code converter is the lack of support for deterministic finalization in the .NET Framework.

.NET objects that need to execute code when they are destroyed implement the IDisposable interface. Such objects rely on the collaboration from client code, in the sense that the developer who instantiates and uses the object is responsible for disposing of the object – by calling the IDisposable.Dispose method – before setting the object variable to Nothing or letting it go out of scope. In general, any .NET class that defines one or more class-level field of a disposable type should be marked as disposable and implement the IDisposable interface. The code in the Dispose method should orderly dispose of all the objects referenced by the class-level fields.

As just noted, the code that instantiates the class is also responsible for calling the Dispose method as soon as the object isn’t necessary any longer, so that referenced disposable objects are disposed as soon as possible. For example, if the class defines and opens one or more database connections (e.g. an SqlConnection object), calling the Dispose method ensures that the connection is closed as quickly as possible. If the call to the Dispose method is omitted, the connection will be closed only later, at the first garbage collection.

The .NET Framework also supports finalizable classes. A finalizable class is a class that overrides the Finalize method and defines one or more fields that contain Windows handles or other values related to unmanaged resources. For example, a class that opens a file by means of the CreateFile Windows API method must be implemented as a finalizable class. The method in the Finalize method is guaranteed to run when the object is being removed from memory during a garbage collection. The code in the Finalize method is expected to close all handles and orderly release all unmanaged resources. Failing to do so would create a resource leak.

VB Migration Partner supports both disposable and finalizable classes. However, you might need to insert one or more pragmas to help it to generate the same quality code that an experienced .NET developer would write. Let’s start with a VB6 class that handles the Class_Terminate event

    Private fileHandle As Long
    
    Private Sub Class_Terminate()
        ' CloseHandle is a Windows API method defined elsewhere by a Declare
            statement
        CloseHandle fileHandle
    End Sub

VB6 classes that include a Class_Terminate are converted to disposable classes that implement the recommended Dispose-Finalize pattern. The generated VB.NET code ensures that the code inside the original Class_Terminate event runs when either a client invokes the Dispose method or when the garbage collection invokes the Finalize method:

    Public Class Widget
        Implements IDisposable
        
        Private fileHandle As Integer 
        
        Private Sub Class_Terminate_VB6()
            CloseHandle(fileHandle)
        End Sub
        
        Protected Overrides Sub Finalize()
            Dispose(False)
        End Sub
        
        Public Sub Dispose() Implements System.IDisposable.Dispose
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub
        
        Protected Overridable Sub Dispose(ByVal disposing As Boolean)
            Class_Terminate_VB6()
        End Sub
    End Class

If the Terminate event is defined inside a Form or a UserControl class, the Dispose method isn’t emitted (because the base class is already disposable); instead, the Form_Terminate or UserControl_Terminate protected method is overridden:

    Protected Overrides Sub Form_Terminate_VB6()
        CloseHandle(fileHandle)
    End Sub

VB Migration Partner can take additional steps to ensure that, if a class uses one or more disposable objects, such objects are correctly disposed of when an instance of the class goes out of scope. In other words, not only does the code generator mark classes with a finalizer as IDisposable classes (as explained above) but it also marks classes using other disposable objects as IDisposable.

To explain how this feature works, a few clarifications are in order. As far VB Migration Partner is concerned, a disposable type is one of the following:

  1. a VB6 class that has a Class_Terminate event (as seen above)
  2. a COM type known to be as disposable (e.g. ADODB.Connection)
  3. a COM type that is explicitly marked as disposable by means of an AddDisposableType pragma, as in this example:
    '## AddDisposableType CALib.DBUtils
  4. a VB6 class that has one or more class-level fields of a disposable type

VB Migration Partner applies these definition in a recursive way. For example, assuming that class C1 has a field of type ADODB.Connection, class C2 has a field of type C1, and class C3 has a field of class C2, then all the C1, C2, and C3 classes are all marked as IDisposable.

If a type is found to be disposable, the exact VB.NET code that VB Migration Partner generates depends on whether it’s under the scope of an AutoDispose pragma. This pragma takes an argument that can have the following values:

No
Variables of disposable types aren’t handled in any special way. (This is the default behavior.)

Yes
If X is a variable of a disposable type, the Set X = Nothing statement is converted as follows:

    ' VB.NET
    SetNothing6(X)
    
    // C#
    VB6Helpers.SetNothing(ref X);
    

The SetNothing6 method (defined in CodeArchitects.VBLibrary) ensures that the object is cleaned-up correctly. If the object implements IDisposable then SetNothing6 calls its Dispose method. If the object is a COM object, SetNothing6 ensures that the object’s RCW is correctly released.

Force
In addition to converting explicit Set X = Nothing statements for disposable objects, VB Migration Partner ensures that if a VB6 class uses one or more disposable objects, the corresponding VB.NET class implements the IDisposable interface and all the disposable objects are correctly disposed of in the class’s Dispose method.

Let’s see in practice how to use the AutoDispose pragma, starting with the Yes option:

    '## AutoDispose Yes
    
    Sub Test()
        Dim cn As New ADODB.Connection
        Dim rs As New ADODB.Recordset
        ' opens the connection and the recordset (omitted)
        ' …
        Set rs = Nothing
        Set cn = Nothing
    End Sub

The resulting .NET code is identical, except for the SetNothing6 method:

    ' VB.NET
    Sub Test()
        Dim cn As New ADODB.Connection
        Dim rs As New ADODB.Recordset
        ' opens the connection and the recordset (omitted) 
        ' …
        SetNothing6(rs)
        SetNothing6(cn)
    End Sub
    
    // C#
    public void Test()
    {   
	ADODB.Connection cn = new ADODB.ConnectionClass();
	ADODB.Recordset rs = new ADODB.RecordsetClass();
	// opens the connection and the recordset (omitted) 
	// …
	VB6Helpers.SetNothing(ref rs);
	VB6Helpers.SetNothing(ref cn);
    }

    

Let’s see now the effects of the Force option, and let’s assume that the following VB6 code is contained in the Widget class:

    '## AutoDispose Force '## AddDisposableType CALib.DBUtils
    Dim cn As ADODB.Connection
    Dim utils As CALib.DBUtils
    …

The ADODB.Connection type is known to be disposable, whereas CALib.DBUtils is marked a disposable by the AddDisposableType pragma. (Such a pragma implicitly has a project-level scope.) Because of rule d) above, the Widget class is considered to be disposable, which makes VB Migration Partner generate the following code:

    ' VB.NET
    Public Class Widget
        Implements System.IDisposable
        
        Dim cn As ADODB.Connection
        Dim utils As CALib.DBUtils
        …
        Public Sub Dispose() Implements System.IDisposable.Dispose
            SetNothing6(cn)
            SetNothing6(utils)
        End Sub
    End Class

    // C#    
    public class Widget: IDisposable
    {
    ADODB.Connection cn = null;
    CALib.DBUtils utils = null;
    …
    public void Dispose() 
        {
    	VB6Helpers.SetNothing(ref cn);
    	VB6Helpers.SetNothing(ref utils);
        }
    }
   
    

If the Widget class has a Class_Terminate event handler, the code in the Dispose method is slightly different:

    ' VB.NET
    Public Sub Dispose() Implements System.IDisposable.Dispose
        Try
            SetNothing6(cn)
            SetNothing6(utils)
        Finally
            Class_Terminate_VB6()
            GC.SupporessFinalize(Me)
        End Try
    End Sub
    
    // C#
    public void Dispose()
    {
        try
        {
            VB6Helpers.SetNothing(ref cn);
            VB6Helpers.SetNothing(ref utils);
        }
        finally
        {
            Class_Terminate_VB6();
            GC.SupporessFinalize(this);
        }
    }

Notice that a class that uses disposable objects doesn’t necessarily implement the Finalize method, as per .NET guidelines. Only VB6 classes that have a Class_Terminate event are migrated to VB.NET classes with the Finalize method.


VB Migration Partner ensures that disposable objects are correctly cleaned-up also when they are assigned to local variables, if a proper AutoDispose pragma is used. For example, consider the following method inside the TestClass class:

    '## AutoDispose Force
    Sub Execute()
        Dim conn As New ADODB.Connection
        If condition Then
            Dim wid As Widget
            …
        End If
    End Sub

In such a case VB Migration Partner moves variables declarations to the top of the method, puts the method’s body inside a Try block, and ensures that disposable objects are cleaned-up in the Finally block. Notice that the wid variable is cleaned-up as well, because Widget has found it to be disposable:

    ' VB.NET
    Sub Execute()
        Dim conn As New ADODB.Connection
        Dim wid As Widget
        Try
            If condition Then
                …
            End If
        Finally
            SetNothing6(conn)
            SetNothing6(wid)
        End Try
    End Sub
    
    // C#
    public void Execute()
    {
        ADODB.Connection conn = new ADODB.ConnectionClass();
        Widget wid = null;
        try
        {
            if (condition)
            {
			    …
			}
	    }
	    finally
	    {
	    VB6Helpers.SetNothing(ref conn);
	    VB6Helpers.SetNothing(ref wid);
	    }
	}

However, if the method contains one or more On Error statements (which can’t coexist with Try blocks) or GoSub statements (which would produce a forbidden GoTo that jumps inside the Try-Catch block), the code generator emits a warning that reminds the developer that a manual fix is needed:

    ' UPGRADE_INFO (#0201): An On Error or GoSub statement prevents
        from generating ' Try-Finally block that clears IDisposable local variables.

The approach VB Migration Partner uses to ensure that disposable variables are cleaned-up correctly resolves most of the problems related to undeterministic finalization in .NET. One of the few cases VB Migration Partner can’t handle correctly is when a class field or a local variable points to an object that is referenced by fields in another class, as in this case:

    Sub Execute()
        Dim conn As New ADODB.Connection
        ' GlobalConn is a public variable defined in a BAS module
        Set GlobalConn = conn
        …
    End Sub

In this specific case, invoking the Dispose method on the conn variable would close the connection referenced by the GlobalConn variable, which in turn may cause the app to malfunction. Developers can avoid this problem by disabling the AutoDispose feature for a given variable or for all the variables in a method:

     Sub Execute()
        '## conn.AutoDispose No
        Dim conn As New ADODB.Connection
        …
    End Sub

Another case when VB Migration Partner fails to generate the correct code is when a variable is re-used to point to another object, as in this VB6 code:

     Sub Execute()
        Dim conn As New ADODB.Connection
        …
        Set conn = New ADODB.Connection
        …
    End Sub



3.11 ActiveX Components

VB Migration Partner supports most of the kinds of COM classes that you can create with VB6. This section explains how you can fine-tune the .NET code being generated.

ActiveX EXE projects

ActiveX EXE projects aren’t supported in .NET and, by default, VB Migration Partner converts them to standard EXE projects. Developers can change this behavior by means of the ProjectKind pragma:

    '## ProjectKind dll

MultiUse, SingleUse, and PublicNotCreatable classes

MultiUse and SingleUse classes are converted to public .NET classes with a public constructor, so that they can be instantiated from a different assembly. PublicNotCreatable classes are converted to public .NET classes whose constructor has Friend scope, so that the class can’t be instantiated from outside the current project.

Notice that the .NET Framework doesn’t support the behavior implied by the SingleUse instancing attribute, therefore SingleUse and MultiUse classes are converted in the same way.

In all three cases, the class is marked with a System.Runtime.InteropServices.ProgID attribute, so that it is visible to COM clients. If the VB6 class was associated to a description, it appears as an XML comment at the top of the .NET class:

    ' VB.NET
    ''' <summary>
    ''' description for the Widget class 
    ''' </summary>
    <System.Runtime.InteropServices.ProgID("Project1.Widget")> _  
    Public Class Widget 
        ' A public default constructor 
        Public Sub New() 
            ' Add initialization code here 
        End Sub  
        
        ' other class members here ... 
    End Class
    
    // C# 
    /// <summary>> 
    /// description for the Widget class
    /// </summary>
    [System.Runtime.InteropServices.ProgID("Project1.Widget")] 
    public class Widget
    {
        // A public default constructor
        public Widget()
    {
        // Add initialization code here
    }
    
    // other class members here ...
}
    
    

GlobalMultiUse and GlobalSingleUse classes

By default, GlobalMultiUse and GlobalSingleUse classes are translated to standard .NET classes. However, when a client accesses a method or property of such classes, VB Migration Partner generates a call to a method of a default instance named ProjectName_ClassName_DefInstance, as in:

    ' EvalArea is a method of the Geometry global multiuse class '
        defined in an ActiveX DLL project named CALib
    res = CALib_Geometry_DefInstance.EvalArea(12, 23)

All the *_DefInstance variables are defined and instantiated in the VisualBasic6.Support.vb module, in the MyProject folder.

In most cases, a global class is used as a singleton class and is never instantiated explicitly. In other words, a client typically never uses a global class with the New keyword and uses only the one instance that is instantiated implicitly. If you are sure that all clients abide by this constraint, it is safe to translate the class to a VB.NET module instead of a class, which you do by means of the ClassRenderMode pragma:

     ' (add inside the Geometry class...)
    '## ClassRenderMode Module 

(The ClassRenderMode Module pragma is ignored if converting to C#.)
If such a pragma is used, the current class is rendered as a VB.NET Module and no default instance variable is defined in the client project. When a Module is used, methods can be invoked directly, the VB.NET code is more readable, and the method call is slightly faster. Notice that the project name is included in all references, to avoid ambiguities:

    res = CALib.EvalArea(12, 23)

Notice that you shouldn’t use the ClassRenderMode pragma with global classes that have a Class_Terminate event, because VB Migration Partner automatically renders them as classes that implement the IDisposable interface, and the Implements keyword inside a VB.NET module would cause a compilation error.

Component initialization

If an ActiveX DLL includes a Sub Main method, then the VB6 runtime ensures that this method is invoked before any component in the DLL is instantiated. This mechanism allows VB6 developers to use the Sub Main method to initialize global variables, read configuration files, open database connections, and so forth.

This mechanism isn’t supported by the .NET Framework in general, therefore VB Migration Partner emits additional code to ensure that the Sub Main is executed exactly once, before any class of the DLL is instantiated. 

    ' VB.NET
    Public Class Widget
        ' This static constructor ensures that the VB6 support library
        ' be initialized before using current class.
        
        Shared Sub New()
            EnsureVB6LibraryInitialization()
        End Sub
        
        ' other class members here ...
    End Class
    
    // C#
    public class Widget
    {
	// This static constructor ensures that the VB6 support library
	// be initialized before using current class.
	static Widget()
	{
		VB6Project.EnsureVB6LibraryInitialization();
	}

	// other class members here ...
    }

    

The EnsureVB6LibraryInitialization method checks that the language support library is initialized correctly and invokes the Sub Main if it hasn’t been already executed.




3.12 Persistable classes

VB Migration Partner fully supports VB6 persistable classes. To illustrate exactly what happens, assume that you have a VB6 class marked as persistable and that handles the InitProperties, ReadProperties, and WriteProperties to implement persistence:

    Const ID_DEF As Integer = 0 
    Const NAME_DEF As String = "" 
    
    Public ID As Integer 
    Public Name As String 

    ' initialize property values 
    Private Sub Class_InitProperties()     
        ID = 123     
        Name = "widget name" 
    End Sub 
    
    ' read property values when the class is deserialized 
    Private Sub Class_ReadProperties(PropBag As PropertyBag)     
        ID = PropBag.ReadProperty("ID", ID_DEF)     
        Name = PropBag.WriteProperty("Name", NAME_DEF) 
    End Sub 
    
    ' write property values when the object is serialized 
    Private Sub Class_WriteProperties(PropBag As PropertyBag)     
        PropBag.WriteProperty "ID", ID, ID_DEF     
        PropBag.WriteProperty "Name", Name, NAME_DEF 
    End Sub

The resulting .NET class is marked with the Serializable attribute and implements the System.Runtime.Serialization.ISerializable interface. The class constructor invokes the Class_InitProperty handler:

    Imports System.Runtime.Serialization 
    
    <System.Runtime.InteropServices.ProgID("Project1.Widget")> _ 
    <Serializable()> _
    Public Class Widget 
        Implements ISerializable
         
        'A public default constructor 
        Public Sub New() 
            Class_InitProperties() 
        End Sub

Event handlers are converted as standard private methods:

        Private Const ID_DEF As Short = 0 
        Private Const NAME_DEF As String = "" 
         
        Public ID As Short 
        Public Name As String = ""  
        
        ' initialize property values 
        Private Sub Class_InitProperties() 
            ID = 123 
            Name = "widget name" 
        End Sub 
        
        ' read property values when the class is deserialized 
        Private Sub Class_ReadProperties(ByRef PropBag As VB6PropertyBag) 
            ID = PropBag.ReadProperty("ID", ID_DEF) 
            Name = PropBag.WriteProperty("Name", NAME_DEF) 
        End Sub 
        
        ' write property values when the object is serialized 
        Private Sub Class_WriteProperties(ByRef PropBag As VB6PropertyBag) 
            PropBag.WriteProperty("ID", ID, ID_DEF) 
            PropBag.WriteProperty("Name", Name, NAME_DEF) 
        End Sub

The code in the GetObjectData and the constructor implied by the ISerializable interface invoke the InitProperties, ReadProperties, and WriteProperties handlers:

        Private Sub GetObjectData(ByVal info As SerializationInfo, _
            ByVal context As StreamingContext) Implements ISerializable.GetObjectData
            Dim propBag As New VB6PropertyBag 
            Class_WriteProperties(propBag) 
            info.AddValue("Contents", propBag.Contents) 
        End Sub  
        
        Private Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext) 
            Dim propBag As New VB6PropertyBag 
            Class_InitProperties() 
            propBag.Contents = info.GetValue("Contents", GetType(Object)) 
            Class_ReadProperties(propBag) 
        End Sub 
    End Class

All references to the VB6’s PropertyBag object are replaced by references to VB6PropertyBag, a class with similar interface and behavior defined in the language support library. It is important to bear in mind, however, that binary files created by persisting a VB6 object can’t be deserialized into a VB.NET object, and vice versa.




3.13 Resources

VB6 resource files are converted to standard .resx files and can be viewed and modified by means of the My Project designer. More precisely, resources are converted to My.Resources.prefixNNN, where prefix is “str” for string resources, “bmp” for bitmaps, “cur” for cursors, and “ico” for icons.

VB Migration Partner attempts to convert all occurrences of LoadResString, LoadResPicture, and LoadResData methods into references to My.Resource.prefixNNN elements (if converting to VB.NET) or Properties.Resources.prefixNNN elements (if converting to C#). This is possible, however, only if the arguments passed to these method are constant values or constant expressions, as in the following VB6 example:

    Const RESBASE As Integer = 100
    Const STRINGRES As Integer = RESBASE + 1
    MsgBox LoadResString(STRINGRES)
    Image1.Picture = LoadResPicture(RESBASE + 7, vbResBitmap)

which is correctly translated into:

    ' VB.NET
    Const RESBASE As Short = 100
    Const STRINGRES As Short = RESBASE + 1
    MsgBox6(My.Resources.str101)
    Image1.Picture = My.Resources.bmp107
    
    // C#
    const short RESBASE = 100;
    const short STRINGRES = RESBASE + 1;
    VB6Helpers.MsgBox(Properties.Resources.str101);
    Image1.Picture = Properties.Resources.bmp107;
    

If the first or the second argument isn’t a constant, then VB Migration Partner falls back to the LoadResString6, LoadResPicture6, and LoadResData6 support methods. These methods rely on the same ResourceManager instance used by the My.Resources or Property.Resources class and therefore return the same resource data. This approach ensures that all .NET localization features can be used on the converted project, including satellite resource-only DLLs.

Interestingly, if an icon resource is being assigned to a VB6 icon property that has been translated to a bitmap property under .NET, then VB Migration Partner automatically generates the code that manages the conversion, as in this code:

    ' VB.NET
    Image1.Picture = My.Resources.ico108.ToBitmap()
    
    // C#
    Image1.Picture = Properties.Resources.ico108.ToBitmap();
    



3.14 Minor language differences

VB Migration Partner generates code that accounts also for minor differences between VB6 and .NET.

Font objects

VB6’s Font and StdFont objects are converted to .NET Font objects. The main difference between these two objects is that the .NET Font object is immutable. Consider the following VB6 code:

    Dim fnt As StdFont                      ' a StdFont object
    Set fnt = Text1.Font
    fnt.Bold = True
    Text2.Font.Name = "Arial"               ' a control’s
        Font property

Assignments to font properties are translated to FontChangeXxxx6 methods in the language support library:

    ' VB.NET
    Dim fnt As Font                         ' a StdFont object
    fnt = Text1.Font
    FontChangeBold6(fnt, True)
    FontChangeName6(Text2.Font, "Arial")    ' a control’s Font property
    
    // C#    
    Font fnt As Font = null ;               // a StdFont object
    fnt = Text1.Font;
    VB6Helpers.FontChangeBold(ref fnt, true);
    VB6Helpers.FontChangeName(ref Text2.Font, "Arial"); // a StdFont object
    

VB Migration Partner provides support also for the StdFont.Weight property. For example, this VB6 code:

    Dim x As Integer
    x = Text1.Font.Weight
    Text2.Font.Weight = x * 2

translates to:

    ' VB.NET
    Dim x As Integer = GetFontWeight6(Text1.Font)
    SetFontWeight6(Text2.Font, x * 2)
    
    // C# 
    int x = VB6Helpers.GetFontWeight(Text1.Font); 
    VB6Helpers.SetFontWeight(ref Text2.Font, x * 2);
    

The GetFontWeight6 and SetFontWeight6 helper functions map the Weight property to the Bold property. They are marked as obsolete, so that the developer can easily spot and get rid of them after the migration has completed.

VB Migration Partner emits a warning if the original VB6 program handles the FontChanged event exposed by the StdFont object. In this case no automatic workaround exists and code must be fixed manually.

For Each loop on multi-dimensional arrays

For Each loops visit multi-dimensional arrays in column-wise order under VB6, and in row-wise order under .NET. When such a loop is detected, VB Migration Partner emits the following warning just before the loop:

    ' UPGRADE WARNING (#01C4): Insert a call to TransposeArray6 method
        if process order ' in following For Each loop is significant
    For Each o As Object In myArr
        …
    Next

The TransposeArray6 helper function returns an array whose rows and columns are transposed, so that the For Each loop works exactly as in the VB6 program:

    For Each o As Object In TransposeArray6(myArr)
        …
    Next

You can add the call to the TransposeArray6 method at each migration cycle, by means of an ReplaceStatement pragma, while using a DisableMessage pragma to suppress the warning:

    '## DisableMessage 01C4 '## ReplaceStatement For Each o As Object
        In TransposeArray6(myArr)
    For Each o As Object In myArr
        …
    Next

Fields passed to ByRef arguments

VB6 fields are internally implemented as properties and can’t be modified by a method even if they are passed to a ByRef argument; in the same circumstances, a .NET field can be modified. For this reason, converting such calls to .NET as-is can introduce subtle and hard-to-find bugs. (being ByRef the default passing mechanism in VB6, such bugs can be rather frequent.)

VB Migration Partner handles this issue by wrapping the field in a call to the ByVal6 helper method, as in this example:

     ' Value is a field of the object held in the obj variable
     MethodWithByrefParam( ByVal6(obj.Value) )

ByVal6 is a do-nothing method that simply returns its argument, ensuring that the original VB6 semantics are preserved. The ByVal6 method is marked as obsolete and produces a compilation warning that encourages the developer to double-check the intended behavior and modify either the calling code or the syntax of the called method.

TypeOf keyword

VB Migration Partner accounts for minor differences in how the TypeOf keyword behaves in VB6 and VB.NET, more precisely:

  1. TypeOf can’t by applied to .NET Structures.
  2. using TypeOf to test a Nothing value raises an error in VB6, but not in VB.NET.
  3. testing against the Object type never fails in VB.NET, except when testing an interface variable.

Consider the following VB6 code:

    If TypeOf obj Is TestUDT Then
        ' obj is a UDT 
    ElseIf TypeOf obj Is Widget Then
        ' obj holds an instance of a specific class
    ElseIf TypeOf obj Is Object Then
        ' obj holds an instance of any class
    ElseIf TypeOf obj Is ITestInterface Then
        ' obj is an interface variable
    End If

To account for these differences, VB Migration Partner performs the test using reflection, except when the second operand is an interface type. This is the converted .NET code:

    ' VB.NET
    If obj.GetType() Is GetType(TestUDT) Then
        ' this test works if obj is a Structure
    ElseIf obj.GetType() Is GetType(Widget) Then
        ' this test throws if obj is Nothing (as in VB6)
    ElseIf (Not TypeOf obj Is String AndAlso Not obj.GetType().IsValueType) Then
        ' obj contains an instance of a reference type
    ElseIf TypeOf obj Is ITestInterface Then
        ' no need to modify tests for interfaces
    End If
    
    // C#
    if ( obj.GetType() == typeof(TestUDT) )
        // this test works if obj is a Structure
    else if ( obj.GetType() == typeof(Widget) )
        // this test throws if obj is Nothing (as in VB6)
    else if ( !(obj is String) && !obj.GetType().IsValueType) )
        // obj contains an instance of a reference type
    else if (obj is ITestInterface )
        // no need to modify tests for interfaces
    

If the variable being tested is of type VB6Variant, the TypeOf operator is translated as a call to the special IsTypeOf6 method:

    If IsTypeOf6(myVariant, GetType(Widget)) Then

Mod operator

The VB6’s Mod operator automatically converts its operands to integer values and returns the remainder of integer division:

    Dim d As Double, i As Integer
    d = 1.8: i = 11
    Debug.Print i Mod d     ' Displays 1, because it rounds up 1.8 to 2

VB.NET and C# don’t convert to Integer and return the remainder of floating-point division if any of the two operands is of type Single or Double. VB Migration Partner ensures that the operator behaves exactly as in VB6 by forcing a conversion to Integer where needed:

    Debug.WriteLine(i Mod CInt(d))

Eqv and Imp operators

Both these operators aren’t supported by VB.NET. VB Migration Partner translates them by generating the bitwise operation that they perform internally. More precisely, the following VB6 code:

     x = y Eqv z
    x = y Imp z

translates to

     ' VB.NET
    x = (Not y And Not z)
    x = (Not y Or z)
    
    // C#
    x = ( !y && !z);
    x = ( !y || z);
    

Strings to Byte array conversions

VB Migration Partner correctly converts assignments between string and Byte arrays. The following VB6 code:

    Dim s As String, b() As Byte
    s = "abcde"
    b = s
    s = b

translates to:

    ' VB.NET
    Dim s As String, b() As Byte
    s = "abcde"
    b = StringToByteArray6(s)
    s = ByteArrayToString6(b)
    
    // C#
    string s;
    byte[] b;
    s = "abcde";
    b = VB6Helpers.StringToByteArray(s);
    s = VB6Helpers.ByteArrayToString(b);
    
    

where the StringToByteArray6 and ByteArrayToString6 helper methods perform the actual conversion. If necessary, this transformation is performed also when a string or a Byte array is passed to a method’s argument or returned by a function or property.

Note: the ByteArrayToString6 method internally uses the UnicodeEncoding.Unicode.GetString method, but adds a dummy null byte if the argument has an odd number of bytes.

Date to Double conversions

VB Migration Partner correctly converts assignments between Date and Double values. The following VB6 code:

    Dim d As Date, v As Double
    d = #11/1/2006#
    v = d
    d = v

translates to:

    ' VB.NET
    Dim d As Date, v As Double
    d = #11/1/2006#
    v = DateToDouble6(d)
    d = DoubleToDate6(v)
    
    // C#
    DateTime d;
    double v;
    d = VB6Helpers.DateConst("11/1/2006");
    v = VB6Helpers.DateToDouble(d);
    d = VB6Helpers.DoubleToDate(v);
    

where the DateToDouble6 and DoubleToDate6 helper methods internally map to Double.ToOADate and Date.FromOADate methods.

For loops with Date variables

For…Next loops that use a Date control variable are allowed in VB6 but not in VB.NET. For example, consider the following VB6 code:

    Dim dt As Date
    For dt = startDate To endDate
        Debug.Print dt
    Next

This is how VB Migration Partner converts the code to VB.NET:

    ' VB.NET
    Dim dt As Date
    For dt_Alias As Double = DateToDouble6(startDate) To DateToDouble6(endDate)
        dt = DoubleToDate6(dt_Alias)
        Debug.WriteLine(dt)
    Next
    
    // C#
    DateTime dt;
    for (double dt_Alias = VB6Helpers.DateToDouble(startDate); 
    dt_Alias <= VB6Helpers.DateToDouble(endDate); dt_Alias++)
    {
        dt = VB6Helpers.DoubleToDate(dt_Alias);
        VB6Project.DebugWriteLine(dt);
    }
    
    



3.15 Unsupported features and controls

Here’s a list of VB6 features that VB Migration Partner doesn’t support:

  • ActiveX Documents
  • Property Pages
  • Web classes
  • DHTML classes
  • DataReport designer
  • OLE and Repeater controls
  • a few graphic-related properties common to several controls, including DrawMode, ClipControls, Palette, PaletteMode
  • VarPtr, ObjPtr, StrPtr undocumented keywords (the VarPtr keyword is partially supported, though)
  • a small number of control features, including:

    • The ability to customize, save, and restore the appearance of the Toolbar control
    • The MultiSelect property of the TabStrip control
    • Vertical orientation for the ProgressBar control
    • The DropHighlight property of TreeView and ListView controls

Even if a feature isn’t supported, VB Migration Partner always attempts to emit .NET that has no compilation errors. For example, ActiveX Documents and Property Pages are converted into .NET user controls. Also, the support library exposes do-nothing properties and methods named after the original VB6 unsupported member.

Unsupported properties and methods

In general, unsupported members in controls are marked as obsolete and return a default reasonable value. For example, the DrawMode property of the Form, PictureBox, UserControl, Line, and Shape classes always return 1-vbBlackness.

By default, assigning an unsupported property or invoking an unsupported method is silently ignored. If the property or the method affects the control’s appearance you can’t modify such a default behavior.

If the property or the method affects the control’s behavior (as opposed to appearance), however, you can force the control to throw an error when the property is assigned a value other than its default value or when the method is invoked, by setting the VB6Config.ThrowOnUnsupportedMember property to True:

    ' VB.NET
    VB6Config.ThrowOnUnsupportedMember = True
    
    // C#
    VB6Config.ThrowOnUnsupportedMember = true;

This setting is global and affects all the controls and classes in control support library.

Unsupported Controls

Occurrences of unsupported controls – including OLE Container, Repeater controls, and all controls that aren’t included in the Visual Basic 6 package – are replaced by a VB6Placeholder control, which is nothing but a rectangular empty control with a red background.

For each unsupported control, VB Migration Partner generates a form-level Object variable named after the original control:

    ' UPGRADE_ISSUE (#02C8): Controls of type VB.OLE aren't supported.
        This control ' was replaced by a dummy variable to make the VB.NET compiler happy
    Friend OLE1 As Object

The variable is never assigned an object reference, therefore any attempt to use it causes a NullReferenceException to be thrown at runtime. This variable has the only purpose of avoiding one compilation error for each statement that references the control.




3.16 The VB6Config class

Some facets of the runtime behavior of .NET applications converted with VB Migration Partner can be modified by means of the VB6Config class. This class exposes only static members, therefore you never need to instantiate an object of the class. Here is the list of exposed members:

VB6Config.ThrowOnUnsupportedMembers

This property specify whether unsupported properties of controls throw an exception when they are assigned an invalid value. For example, converted .NET forms and PictureBox controls don’t support the ClipControls property, even though they expose a property named ClipControls. By default ThrowOnUnsupportedMembers is False, therefore assigning a True value to the ClipControls property doesn’t raise an error and the assignment is just ignored. However, if you set the ThrowOnUnsupportedMembers property to True, the same action raises a runtime error whose code is 999 and whose message is “Unsupported member”:

    VB6Config.ThrowOnUnsupportedMembers = True

The ThrowOnUnsupportedMembers also affects the behavior of unsupported methods, such as RichTextBox.SelPrint or SSTab.ShowFocusRect. Usually, calls to these unsupported methods are simply ignored. However, if  ThrowOnUnsupportedMembers is True than the method throws an exception.

Please notice that there are a few unsupported language keywords – namely VarPtr, StrPtr, and ObjPtr – that always raise an error, regardless of the value of ThrowOnUnsupportedMembers. We implemented this behavior because typically the value of these functions is passed to a Windows API, in which case passing an invalid value might compromise the entire system.

VB6Config.LogFatalErrors

All .NET applications immediately stop when an unhandled error is raised (unless they run inside a debugger). The .NET applications generated by VB Migration Partner are no exception, however they have the added ability to display a message box containing the complete stack dump of where the error occurred. This information can be very important when debugging an application that runs fine on the development machine and fails elsewhere. You enable this feature by setting the LogFatalErrors to True:

    VB6Config.LogFatalErrors = True

VB6Config.FocusEventSupport

VB6 and .NET controls slightly differ in the exact sequence of events that fire when a control receives or loses the input focus. For example, when you click on a VB6 control, the control raises a MouseDown event followed by a GotFocus event, whereas a .NET control fires the GotFocus event first, followed by the MouseDown event.

Another difference is the event sequence that is fired when the focus is moved away from the control. VB6 controls fire the Validate event first, then fire the LostFocus event only if validation succeeded. (If validation failed, the LostFocus event is never raised). Conversely, the behavior of standard .NET controls depends on whether the focus is moved away by means of the keyboard or the mouse. If the keyboard is used, a Validate event is raised, followed by a LostFocus event. If the mouse, the event order is just the opposite. Worse, a GotFocus event is fired if validation fails.

In most cases, these differences are negligible and don’t affect the application’s overall behavior. For this reason, therefore, by default converted applications follow the .NET standard behavior. If you experience a problem related to these events, however, you can have .NET controls behave exactly like the original VB6 controls, simply by setting the FocusEventSupport to True:

    VB6Config.FocusEventSupport = True

VB6Config.ReturnedNullValue

Some VB6 functions return a Null value when the caller passes Null to one of their parameters. This behavior is duplicated under converted .NET applications. By default, the .NET equivalent for the Null value is the DBNull.Value, a detail that might cause a problem when the value is passed to a string or used in an expression. For example, consider this VB6 code:

    ' rs is a recordset, txtValue is a TextBox control
    txtValue.Text = Hex(rs("value").Value) 

This code fails under .NET if the field “value” of the recordset contains Null, because the Hex function would return DBNull.Value and the assignment to the Text property would raise an error.

To work around this issue, you can assign the ReturnNullValue property a value other than DBNull.Value. In most cases, using an empty string solves the problem:

    VB6Config.ReturnedNullValue = True

VB6Config.DragIcon

When a “classic” drag-and-drop operation begins, by default VB6 shows the outline of the control that acts as the source of the drag-and-drop. This behavior isn’t replicated in converted .NET apps, which instead display a default image. This default image is the one returned by the VB6Config.DragIcon property, but you can assign this property to change the default image:

      ' VB.NET
     VB6Config.DragIcon = LoadPicture6("c:\dragicon.bmp")
     
     // C#
     VB6Config.DragIcon = VB6Helpers.LoadPicture(@"c:\dragicon.bmp");
                    

As in VB6, the default image is used only for controls whose DragIcon is nothing.

VB6Config.DDEAppTitle

By default, a VB6 app that works as DDE server reacts to requests from client controls whose LinkTopic property is formatted as follows:
         appname|formlinkopic
where appname is the application title (same as the App.Title property in VB6) and formlinktopic is the value of the LinkTopic property of a form whose LinkMode property has been set to 1-vbLinkSource).

Converted .NET forms work much like in the same way, except that the appname value is checked against the VB6Config.DDEAppTitle property. The initial value of this property is the same it was in the VB6 project, therefore it most cases the application should work as it used to do under VB6. However, the ability to change the appname your DDE server app responds to adds a degree of flexibility to your migrated projects.

VB6Config.DDEAllowLinkSend

The TextBox, Label, and PictureBox controls all expose the LinkSend method, however an error 438 “Object doesn’t support this property or method” is raised when the LinkSend method is invoked on the TextBox and Label controls. This behavior is intentional and perfectly mimics the VB6 behavior. However, once you’ve completed the migration to .NET you might need a simple way for a DDE server application to send a string to a DDE client application using the LinkSend method. You can do so by setting this property to True:

        VB6Config.DDEAllowLinkSend = True

 

Previous | Index | Next