The .NET Framework is stuffed full of treats for the VB developer. Long-awaited improvements such as strict type checking, structured exception handling, and true object-oriented support extend the power of the language dramatically - but at a price. The only way a Visual Basic 6 program can be brought to the .NET world is through a painful migration process that rarely runs without incident. In this article, we'll consider some of these ugly problems - and the insights you can gain from them.
For VB programmers, this migration process is handled by a special Upgrade Wizard provided in the Visual Studio .NET IDE. When you attempt to open a VB 6 project file in VS.NET, the Upgrade Wizard is launched automatically (see Figure 1). It then scans through your project line by line, attempting to modify syntax and make the required substitutions by relying on an internal VB 6 to VB.NET compatibility list. For a large or complex project, the process is surprisingly slow.
All in all, the Upgrade Wizard does remarkably well for simple applications. This feat is particularly amazing when you consider the migration hurdles it must overcome:
VB 6 uses the proprietary Ruby engine for displaying forms. VB.NET uses the common controls from the .NET class library, which expose a different set of properties and methods and use an entirely new event model.
Unfortunately, these problems are difficult to escape. If you try to migrate a traditional, modestly complex VB 6 application, you'll find that the Upgrade Wizard often falls flat on its face.
A Sample Migration Disaster
I discovered a whole new level of .NET incompatibilities when I migrated a midsize VB 6 ordering application to the .NET platform. This program (which I'll call VB6OrderMaker) allows users to order specialty products and submit an invoice via e-mail. It consists of about three dozen forms, most of which are used to provide different product listings. A few other capabilities, like multiple price sets and dynamic print previews, are also part of the mix. As a whole, this program is a good example of typical, slightly old-fashioned VB 6 coding: it doesn't make much use of classes or interfaces, but the code is logical and well organized.
The Upgrade Wizard went to work for over an hour on the VB6OrderMaker application, and ended with an Upgrade Report presenting 360 errors; 2,889 warnings; and 3,249 total issues (see Figure 2), making for an average of about 80 issues per file. The only file that made the jump to .NET without any problems was the module used to run functions from the Windows API.
So what derailed the VB6OrderMaker migration? The list of problems took a long time to sort through, but eventually several distinct themes emerged. These themes represent some of the issues I've seen recur time and time again with other VB 6 migration projects.
The Demise of Proprietary Arrays
In VB.NET, every array must use 0 as a lower bound. This change was introduced to make VB more compatible with other programming languages (like C#), ensuring that all types of .NET developers can easily share custom code snippets and insight. Unfortunately, this change also spells significant trouble for VB developers who have been using their own proprietary arrays.
In the VB6OrderMaker project, array indexes are chosen with specific meanings. For example, the elements 1 and greater are used to contain the items in an order. Element 0 contains a combined line with price totals, and negative array indexes are used to contain licensing information for the ordered items. Even though these specific array indexes are stored in variables instead of being hard-coded, there's no easy way to update the code to meet the new 0-bound requirement. And without fixing this problem, the VB6OrderMaker program can't even be compiled.
The VB 6 Solution
Unlike some of the other migration headaches, this disaster could have been avoided. A better approach is to not use arrays at all in VB 6, but to move straight to collections or a custom class. In the VB6OrderMaker application, orders could be stored in an Order class that contains a collection of ordered items and other license-specific properties. This class could also contain built-in methods for retrieving the total cost of all ordered items, and for converting prices to different currencies. The Order class approach would improve encapsulation, and would result in a data structure that doesn't depend on miscellaneous conversion and helper functions in other modules of the program.
The Secret Variant
Variants are a special VB 6 data type that can store different types of information, including strings or numbers. A variant converts itself automatically according to how you try to use it. Historically, this automatic conversion led to a variety of annoyances, although it was useful in some situations.
The problem is that even though variants are rarely used deliberately in VB 6 code, they are accidentally created every time a programmer forgets to specify a data type.
VB 6 code
' intB will be an Integer, but intA will be a _variant.
Dim intA, intB As Integer
This type of mistake is generally harmless. However, in does result in additional migration warnings. Because VB.NET does not support variants, the Upgrade Wizard automatically converts all variants into the generic System.Object type. Thus, after a typical migration, you may find yourself with many more generic Objects than you expected.
VB.NET migrated code
Dim intA As Integer, intB As Object
If you are using loose type checking (Option Strict Off), the generic Object can still be converted into a simple data type (like a string or integer) as needed. But because the compiler can't verify these operations, each one will be flagged with a warning:
Dim intA As Integer, intB As Object
' UPGRADE_WARNING: Couldn't resolve default _ property of object intB.
' Click for more: ms-help://MS.MSDNVS/vbcon/html/vbup1037.htm
intB = 0
The VB 6 Solution:
This minor issue isn't a problem; in fact, this portion of the code will still work perfectly. However, this idiosyncrasy leads to a large number of additional warnings in many migrated projects. To eliminate it entirely, double-check all your variables for missing data types. Unfortunately, there's no tool that can help automate this tedious process.
The Control Array Kludge
Control arrays present a particularly unpleasant example of what can go wrong with migration. In VB 6, control arrays are often an excellent way to solve certain problems. In VB6OrderMaker, control arrays allow the program to loop through a series of controls and update them based on array information. For example, you can copy pricing information into a series of text boxes using this syntax:
VB 6 code:
For i = 0 to UBound(PriceArray)
txtPrice(i).Text = Val(PriceArray(i)
Next i
Control arrays also allow a VB 6 program to use a single event handler for numerous similar controls, which is an extremely useful convenience. For example, you could implement a set of dynamically highlighting labels, like this:
VB 6 code
Private Sub Description_MouseMove
(Index As Integer, _
Button As Integer, Shift As Integer, _
X As Single, Y As Single)
Description(Index).ForeColor _
= &H800000
End Sub
In .NET, control arrays aren't needed to handle multiple events. Instead, you can use the Handles clause or the AddHandler statement to link up as many controls as you want to a single event handler. You can then use the sender parameter to interact with the control that fires the event.
VB.NET code:
Private Sub Highlight(sender As _ Object, e As MouseEventArgs) _
Handles lblLine1.MouseMove, _ lblLine2.MouseMove
Dim lbl As Label = _ CType(sender, Label)
lbl.ForeColor = _ Color.RoyalBlue
End Sub
The VB 6 Solution
There's no easy fix for this problem. Control arrays are a useful tool in VB 6, but they have been replaced with a more modern system in VB.NET. However, if you import a program such as VB6OrderMaker that uses control arrays, the Upgrade Wizard doesn't convert them. Instead, it uses a special compatibility class, depending on your control. For example, if you have a control array of label controls, you'll end up using Microsoft.VisualBasic.Compatibility.VB6.LabelArray. This control allows VB.NET to "fake" a control array, with predictably inelegant results. This is an example of the Upgrade Wizard at its worst: importing legacy problems from VB 6 into the .NET world.
Pointers, Handles, and Other Low-Level Tricks
Visual Basic provides several undocumented functions that allow expert VB coders to retrieve the memory addresses where simple variables or objects are stored, including VarPtr, StrPtr, and ObjPtr. After retrieving this information, you could pass it to a DLL routine or to the Windows API, which sometimes required this information. In VB.NET, you will rarely need to have this sort of low-level access to memory information, which complicates your code and can introduce obscure bugs. If, however, you need to interact with a DLL or code component that needs an address, you can still retrieve it - but you need to "pin down" the memory first. Pinning down the memory ensures that the Common Language Runtime won't try to move a value between the time when you find its address and the time when the DLL tries to use it. (This automatic movement feature is one of the ways by which the .NET runtime attempts to improve performance.)
The following example creates and pins down a handle for an object called MyObj. It uses a special type called GCHandle in the System.Runtime.InteropServices namespace:
VB .NET code:
Dim MyGCHandle As GCHandle = _
GCHandle.Alloc(MyObj, GCHandle _
Type.Pinned)
Dim Address As IntPtr = _
MyGCHandle.AddrOfPinnedObject()
' (Invoke the DLL or do something _
with the Address variable here.)
' Allow the object to be moved _ again.
MyGCHandle.Free()
The VB 6 Solution
The code you use is up to you, but be aware that the Upgrade Wizard will simply flag attempts to use the VarPtr, StrPtr, or ObjPtr function with an error. You'll have to add the code above on your own.
A New Menu Model
Menus represent one more case in which a minor change in the programming model spells a major migration headache. In VB 6, you have to create a pull-down menu before you can use it in a context menu. You then rely on the built-in PopupMenu command:
VB 6 code:
Private Sub Form_MouseDown(Button _
As Integer, Shift As Integer, _
X As Single, Y As Single)
If Button = 2 Then
PopupMenu mnuOrderOptions
End If
End Sub
In VB.NET, you can't use the same menu for a main menu and a context menu. Instead, you'll need to create a new ContextMenu object, and copy the ordinary menu information into it with the CloneMenu() method:
VB .NET code:
Dim mnuPopUp As New ContextMenu()
Dim mnuItem As MenuItem
For Each mnuItem In mnuOrder _
Options.MenuItems
' The CloneMenu method _
ensures that the context menu _
and main menu items have the same _ event handlers.
mnuPopUp.MenuItems.Add(mnuItem. _
CloneMenu())
Next
Me.ContextMenu = mnuPopUp
The last line here assigns the new context menu to the form's ContextMenu property. The only reason you should do this is to make sure that a reference to the context menu is conveniently available when you need it - for example, in the form's event handler for a MouseDown event.
In addition, VB.NET does not provide a PopupMenu command built into the language. Instead, you use the ContextMenu.Show() method, along with the coordinates where you want to display the context menu.
VB .NET code:
Private Sub Form1_MouseDown _
(ByVal sender As System.Object, _
ByVal e As _
System.Windows.Forms._
MouseEventArgs)
Handles MyBase.MouseDown
If e.Button = Mouse _ Buttons.Right Then
Me.ContextMenu.Show(Me, _ New Point(e.X, e.Y))
End If
End Sub
The VB 6 Solution
You need to patch this problem manually. The best way to manage your context menus - and the best time to create them - is up to you. Once again, the Upgrade Wizard won't offer any help beyond identifying the problem.
The Disappearing Modeless Window
If you use a startup subroutine in VB.NET, your application will end as soon as the subroutine ends - even if other windows are still open. This is a significant change from VB 6, which only shuts down a program if you use the End command, or when all windows have been closed.
For example, consider the migrated startup code below. It suffers from a fatal flaw: as soon as the window is displayed, the application ends prematurely, and the window is closed.
VB .NET code:
Public Sub Main()
' Program will end immediately _ after executing the next line.
frmMain.DefInstance.Show()
End Sub
The VB 6 Solution
In most cases the Upgrade Wizard will not make the required change automatically. However, it's fairly easy to correct manually. Just show all windows modally from the Main subroutine using the ShowDialog() method:
VB .NET code:
Public Sub Main()
' Program will end when _ frmMain is closed.
frmMain.DefInstance.ShowDialog()
End Sub
Starting From Scratch
There are also some types of code that the Upgrade Wizard won't attempt to translate at all. These include code routines for printing, using the clipboard, drawing shapes and lines directly on a form, and implementing context-sensitive help. In some cases, the required VB.NET code isn't necessarily that different. However, the Upgrade Wizard won't help you (it will simply comment the troublesome areas of code and leave them untouched and incompatible).
In addition, there are some types of VB 6 projects that aren't fit for any type of migration: