This section illustrates how VB Migration Partner converts VB6 language elements and how you can apply pragmas to generate better VB.NET code.
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:
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()
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:
Private Sub Test()
Dim arr() As String
Dim arr2() As Integer
ReDim arr(10)
ReDim arr2(10)
End Sub
(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
Dim arr(LoIndex To HiIndex) As String
is translated as follows:
Dim arr(0 To HiIndex - LoIndex) As String
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:
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:
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
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 VB.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
…
Dim arr(10) As String
numEls = UBound(arr)
which is translated as:
Dim arr As New VB6Array(Of String)(1, 10)
numEls = UBound6(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
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:
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:
Dim arr() As String
Sub Test()
ReDim arr(1 To 10) As String
End Sub
which is rendered as:
Private arr As VB6Array(Of String)
Public Sub Test()
Redim6(arr, 1, 10)
End Sub
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()
Dim res() as Integer
…
GetValues = res
End Function
Function InitArray() As Integer()
…
End Function
which is translated as follows:
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
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:
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 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
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:
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:
Dim mat(1 To 10, -1 To 1) As Double
Delta values can be negative, can be variables and expressions.
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:
<System.Runtime.InteropServices.DispId(0)> _
Public ReadOnly Property Text() As String
Get
Return "..."
End Get
End Property
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 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:
Sub Test(ByVal obj As Object)
MsgBox6(obj)
obj = "new value"
End Sub
The VB.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)
MsgBox obj
obj = "new value"
End Sub
which delivers this VB.NET code:
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 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)
Dim res As Integer
x = o(1)
o(1) = res + 1
End Sub
translates to:
Sub Test(ByVal obj As Object)
Dim res As Short
res = GetDefaultMember6(obj, 1)
SetDefaultMember6(obj, 1, 1234)
End Sub
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.
VB.NET doesn’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"
Third:
Debug.Print "Third"
Return
End Sub
This is how VB Migration Partner converts the code:
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")
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:
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 VB.NET. (This warning has been dropped in examples shown in this section.)
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:
Dim fs As 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:
Dim arr() As VB6FixedString_256 = CreateArray6(Of VB6FixedString_256)(0, 10)
arr(0).Value = "abcde"
where VB6FixedString_256 a special class in the VisualBasic6.Support.vb module:
<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:
Dim arr(1 to 10) As String * 256
which is translated to:
Dim arr As New VB6ArrayNew(Of VB6FixedString_256)(1, 10)
The VB6ArrayNew(Of T) generic class differs from the VB6Array(Of T) class in that it automatically creates an instance of the T type for each element of the array. Using a plain VB6Array(Of T) 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:
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.
The main problem in converting Type…End Type blocks – a.k.a. User-Defined Types or UDT – to VB.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
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:
Dim udt As 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:
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 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:
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
Title As String * 30
Artist As String * 30
Album As String * 30
Year As String * 4
End Type
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.
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
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:
Function GetValue() As Short
Dim obj As Widget
ByVal6(obj).Value = 1234
GetValue = ByVal6(obj)
End Function
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:
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
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:
Dim arr(10) As New TestClass()
is translated as
Dim arr() As New VB6ArrayNew(Of TestClass)(0, 10)
The VB6ArrayNew(Of T) generic type behaves exactly as VB6Array(Of T), except the former automatically ensures that all its elements are instantiated before they are accessed.
VB Migration Partner is able to automatically solve most of the issues related to converting VB6 Declare statements to VB.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()
SendMessage Text1.hWnd, WM_SETTEXT, 0, ByVal "new text"
End Sub
Sub CopyToClipboard()
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
Function EnumWindows_CBK(ByVal hWnd As Long, _
ByVal lParam As Long) As Long
EnumWindows_CBK = 1
End Function
This is how VB Migration Partner converts the code to VB.NET:
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
Function EnumWindows_CBK(ByVal hWnd As Integer, ByVal lParam As Integer) As Integer
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:
Private Declare Function GetSystemDirectory Lib "kernel32.dll" _
Alias "GetSystemDirectoryA" (ByVal lpBuffer As String, _
ByVal nSize As Integer) As Integer
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:
Dim v As Variant
Dim arr() As Variant
which is translated to:
Dim v As VB6Variant
Dim arr() As VB6Variant
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:
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:
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:
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 VB.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).
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:
Sub Execute()
End Sub
Property Get Name() As String
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:
Implements IPlugIn
Under these assumptions, this is the code that VB Migration Partner generates:
Public Class IPlugInClass
Implements IPlugIn
Sub Execute() Implements IPlugIn.Execute
End Sub
ReadOnly Property Name() As String Implements IPlugIn.Name
Get
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 ISomething class. 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 VB.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:
The ClassRenderMode pragma can’t be applied at the project level and has to be specified for each distinct class.
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.
VB.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 fileHandle
End Sub
VB6 classes that include a Class_Terminate are converted to disposable classes that implement the recommended Dispose-Finalize pattern. The generated 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:
- a VB6 class that has a Class_Terminate event (as seen above)
- a COM type known to be as disposable (e.g. ADODB.Connection)
- a COM type that is explicitly marked as disposable by means of an AddDisposableType pragma, as in this example:
- 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:
SetNothing6(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:
Sub Test()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Set rs = Nothing
Set cn = Nothing
End Sub
The resulting VB.NET code is identical, except for the SetNothing6 method:
Sub Test()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
SetNothing6(rs)
SetNothing6(cn)
End Sub
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:
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:
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
If the Widget class has a Class_Terminate event handler, the code in the Dispose method is slightly different:
Public Sub Dispose() Implements System.IDisposable.Dispose
Try
SetNothing6(cn)
SetNothing6(utils)
Finally
Class_Terminate_VB6()
GC.SupporessFinalize(Me)
End Try
End Sub
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:
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:
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
However, if the method contains one or more On Error statements (which can’t coexist with Try blocks), the code generator emits a warning that reminds the developer that a manual fix is needed:
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
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()
Dim conn As New ADODB.Connection
…
End Sub
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 VB.NET code being generated.
ActiveX EXE projects
ActiveX EXE projects aren’t supported in VB.NET and, by default, VB Migration Partner converts them to standard EXE projects. Developers can change this behavior by means of the ProjectKind pragma:
MultiUse, SingleUse, and PublicNotCreatable classes
MultiUse and SingleUse classes are converted to public VB.NET classes with a public constructor, so that they can be instantiated from a different assembly. PublicNotCreatable classes are converted to public VB.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 VB.NET class:
<System.Runtime.InteropServices.ProgID("Project1.Widget")> _
Public Class Widget
Public Sub New()
End Sub
End Class
GlobalMultiUse and GlobalSingleUse classes
By default, GlobalMultiUse and GlobalSingleUse classes are translated to standard VB.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:
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 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:
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 VB.NET and 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.
Public Class Widget
Shared Sub New()
EnsureVB6LibraryInitialization()
EnsureVB6ComponentInitialization()
End Sub
End Class
The EnsureVB6LibraryInitialization method checks that the language support library is initialized correctly, whereas the EnsureVB6ComponentInitialization method invokes the Sub Main if it hasn’t been already executed.
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
Private Sub Class_InitProperties()
ID = 123
Name = "widget name"
End Sub
Private Sub Class_ReadProperties(PropBag As PropertyBag)
ID = PropBag.ReadProperty("ID", ID_DEF)
Name = PropBag.WriteProperty("Name", NAME_DEF)
End Sub
Private Sub Class_WriteProperties(PropBag As PropertyBag)
PropBag.WriteProperty "ID", ID, ID_DEF
PropBag.WriteProperty "Name", Name, NAME_DEF
End Sub
The resulting VB.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
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 = ""
Private Sub Class_InitProperties()
ID = 123
Name = "widget name"
End Sub
Private Sub Class_ReadProperties(ByRef PropBag As VB6PropertyBag)
ID = PropBag.ReadProperty("ID", ID_DEF)
Name = PropBag.WriteProperty("Name", NAME_DEF)
End Sub
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.
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. 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:
Const RESBASE As Short = 100
Const STRINGRES As Short = RESBASE + 1
MsgBox6(My.Resources.str101)
Image1.Picture = My.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 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 VB.NET, then VB Migration Partner automatically generates the code that manages the conversion, as in this code:
Image1.Picture = My.Resources.ico108.ToBitmap()
VB Migration Partner generates code that accounts also for minor differences between VB6 and VB.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
Set fnt = Text1.Font
fnt.Bold = True
Text2.Font.Name = "Arial"
Assignments to font properties are translated to FontChangeXxxx6 methods in the language support library:
Dim fnt As Font
fnt = Text1.Font
FontChangeBold6(fnt, True)
FontChangeName6(Text2.Font, "Arial")
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:
Dim x As Integer
x = GetFontWeight6(Text1.Font)
SetFontWeight6(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 VB.NET. When such a loop is detected, VB Migration Partner emits the following warning just before the loop:
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:
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 VB.NET field can be modified. For this reason, converting such calls to VB.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:
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:
- TypeOf can’t by applied to VB.NET Structures.
- using TypeOf to test a Nothing value raises an error in VB6, but not in VB.NET.
- 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
ElseIf TypeOf obj Is Widget Then
ElseIf TypeOf obj Is Object Then
ElseIf TypeOf obj Is ITestInterface Then
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 VB.NET code:
If obj.GetType() Is GetType(TestUDT) Then
ElseIf obj.GetType() Is GetType(Widget) Then
ElseIf (Not TypeOf obj Is String AndAlso Not obj.GetType().IsValueType) Then
ElseIf TypeOf obj Is ITestInterface Then
End If
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
VB.NET doesn’t convert to Integer and returns 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
x = (Not y And Not z)
x = (Not y Or 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:
Dim s As String, b() As Byte
s = "abcde"
b = StringToByteArray6(s)
s = ByteArrayToString6(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:
Dim d As Date, v As Double
d = #11/1/2006#
v = DateToDouble6(d)
d = DoubleToDate6(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:
Dim dt As Date
For dt_Alias As Double = DateToDouble6(startDate) To DateToDouble6(endDate)
dt = DoubleToDate6(dt_Alias)
Debug.WriteLine(dt)
Next
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
- “classic” (non OLE) drag-and-drop
- 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 VB.NET that has no compilation errors. For example, ActiveX Documents and Property Pages are converted into VB.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:
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 instances 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:
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.