This section illustrates how VB Migration Partner converts VB6 language elements
and how you can apply pragmas to generate better .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
private void Test()
{
string[] arr = null;
int[] arr2 = null;
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
Dim arr(LoIndex To HiIndex) As String
is translated as follows:
Dim arr(0 To HiIndex - LoIndex) As String
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:
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
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
…
Dim arr(10) As String
numEls = UBound(arr)
which is translated as:
Dim arr As New VB6Array(Of String)(1, 10)
numEls = UBound6(arr)
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
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
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()
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
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:
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:
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
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:
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 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:
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.
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
[System.Runtime.InteropServices.DispId(0)]
public string Text
{
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:
Sub Test(ByVal obj As Object)
MsgBox6(obj)
obj = "new value"
End Sub
Public void Test(object obj)
{
VB6Helpers.MsgBox(obj);
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)
MsgBox obj
obj = "new value"
End Sub
which delivers this .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.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)
Dim res As Integer
x = obj(1)
obj(1) = res + 1
End Sub
translates to:
Sub Test(ByVal obj As Object)
Dim res As Short = GetDefaultMember6(obj, 1)
SetDefaultMember6(obj, 1, res + 1)
End Sub
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.
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"
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")
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
.NET project. (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"
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:
Dim arr() As VB6FixedString_256 = CreateArray6(Of VB6FixedString_256)(0, 10)
arr(0).Value = "abcde"
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:
Dim arr(1 to 10) As String * 256
which is translated to:
Dim arr As New VB6ArrayNew(Of VB6FixedString_256)(1, 10)
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:
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 .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)
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 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
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. 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
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
AutoNew6(obj).Value = 1234
GetValue = AutoNew6(obj).Value
End Function
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:
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
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:
Dim arr(10) As New TestClass()
is translated as
Dim arr() As New VB6ArrayNew(Of TestClass)(0, 10)
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.
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()
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
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:
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 .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.
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 VB.NET 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 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:
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.
.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 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:
- 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)
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:
Sub Test()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Set rs = Nothing
Set cn = Nothing
End Sub
The resulting .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
public void Test()
{
ADODB.Connection cn = new ADODB.ConnectionClass();
ADODB.Recordset rs = new ADODB.RecordsetClass();
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:
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
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:
Public Sub Dispose() Implements System.IDisposable.Dispose
Try
SetNothing6(cn)
SetNothing6(utils)
Finally
Class_Terminate_VB6()
GC.SupporessFinalize(Me)
End Try
End Sub
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:
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
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:
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
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
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:
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:
<System.Runtime.InteropServices.ProgID("Project1.Widget")> _
Public Class Widget
Public Sub New()
End Sub
End Class
[System.Runtime.InteropServices.ProgID("Project1.Widget")]
public class Widget
{
public Widget()
{
}
}
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:
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:
(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.
Public Class Widget
Shared Sub New()
EnsureVB6LibraryInitialization()
End Sub
End Class
public class Widget
{
static Widget()
{
VB6Project.EnsureVB6LibraryInitialization();
}
}
The EnsureVB6LibraryInitialization method checks that the language support library
is initialized correctly and 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 .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
(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:
Const RESBASE As Short = 100
Const STRINGRES As Short = RESBASE + 1
MsgBox6(My.Resources.str101)
Image1.Picture = My.Resources.bmp107
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:
Image1.Picture = My.Resources.ico108.ToBitmap()
Image1.Picture = Properties.Resources.ico108.ToBitmap();
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
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")
Font fnt As Font = null ;
fnt = Text1.Font;
VB6Helpers.FontChangeBold(ref fnt, true);
VB6Helpers.FontChangeName(ref 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 = GetFontWeight6(Text1.Font)
SetFontWeight6(Text2.Font, x * 2)
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:
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 .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:
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 .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 .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 ( obj.GetType() == typeof(TestUDT) )
else if ( obj.GetType() == typeof(Widget) )
else if ( !(obj is String) && !obj.GetType().IsValueType) )
else if (obj is ITestInterface )
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 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
x = (Not y And Not z)
x = (Not y Or z)
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:
Dim s As String, b() As Byte
s = "abcde"
b = StringToByteArray6(s)
s = ByteArrayToString6(b)
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:
Dim d As Date, v As Double
d = #11/1/2006#
v = DateToDouble6(d)
d = DoubleToDate6(v)
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:
Dim dt As Date
For dt_Alias As Double = DateToDouble6(startDate) To DateToDouble6(endDate)
dt = DoubleToDate6(dt_Alias)
Debug.WriteLine(dt)
Next
DateTime dt;
for (double dt_Alias = VB6Helpers.DateToDouble(startDate);
dt_Alias <= VB6Helpers.DateToDouble(endDate); dt_Alias++)
{
dt = VB6Helpers.DoubleToDate(dt_Alias);
VB6Project.DebugWriteLine(dt);
}
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:
VB6Config.ThrowOnUnsupportedMember = 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 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:
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.
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:
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:
VB6Config.DragIcon = LoadPicture6("c:\dragicon.bmp")
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