The Book of VB .NET: Migrating to Visual Basic .NET, Part 2
Common Migration Problems
In this section, we'll examine some of the problems that can derail .NET migration. If you can find these problems in your own Visual Basic 6 applications, it may be a good idea to fix them within that environment before migrating the project-or call off the migration process altogether.
TIP If you're still developing projects in Visual Basic 6, the information in this section will help you make choices that will facilitate migration in the future.
Arrays can thwart any attempt at migration. Visual Basic .NET's new insistence that every array have a lower boundary of zero can cause all sorts of trouble. In our original 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, updating them to comply with the zero-boundary requirement would not be easy, and would requires hours of modification and retesting. Some early Microsoft documents promised that the Upgrade Wizard would create special array classes that will mimic normal arrays and allow any lower boundary that you need. This flexibility never materialized in the beta or release versions, and it's uncertain whether or not the new array classes would help at all, or just complicate life even more.
However, it's fairly easy to create a Visual Basic 6 program that won't suffer from this array problem. The best choice is not to use zero-bounded arrays at all, but to move straight to collections or other custom classes. In our VB6OrderMaker application, the Visual Basic 6 code would be clearer and more elegant if orders were stored in an Order class that contained a collection of ordered items as a property (for example, Order.Items), and any other required 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.
Variants are a special Visual Basic 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.
In Visual Basic .NET, variants aren't supported, but the System.Object type provides the same flexibility. If Option Strict is disabled for your application (as it is by default when you are migrating a project), you can use the generic object type in almost the exact same way as you would variants, with the same automatic conversion feature. If Option Strict is enabled, you need to convert object types manually when performing operations with them. This is explained in Chapter 7 of The Book of VB .NET, but a quick review is helpful:
Dim x As Object, y As Object, z As Object x = 1 y = 2 z = x + y ' Will only work in Option Strict is off. z = CType(x, Integer) + CType(y, Integer) ' Always works without a hitch.
The Upgrade Wizard will convert all variants into generic objects. After a typical migration, you may find yourself with many more generic objects than you expected. The problem is that even though variants are rarely used deliberately in VB 6 code, variables could be inadvertently defined without data types, as shown here:
' VB 6 code. Dim intA, intB As Integer ' intB is an Interger, but intA will be a variant.
This oversight, which occurs most often when simple counters and other unimportant temporary variables are defined, causes Visual Basic 6 to use its default data type, which is the variant. During the migration of the VB6Order- Maker program into .NET format, many unspecified counter variables ended up as objects. The Upgrade Wizard flagged every subsequent line of code that performs any operations with these variables, indicating that it can't determine the default properties for those objects:
Dim intA As Integer, intB As Object intA = 0
' UPGRADE_WARNING: Couldn't resolve default property of object intB. ' Click for more: ms-help://MS.MSDNVS/vbcon/html/vbup1037.htm intB = 0
This minor issue isn't a problem-in fact, this portion of the code will still work perfectly. However, this idiosyncrasy led to a large number of additional warnings in the VB6OrderMaker upgrade report.
Why is the Upgrade Wizard so aggressive in flagging the unstructured use of an object? In VB6OrderMaker, these objects really represent simple value types, and the default value is automatically available as long as Option Strict is Off. However, this type of operation could create a problem in other circumstances.
In .NET, standard default properties aren't supported, and attempts to use an object's default property will fail. Usually, the Upgrade Wizard will add the necessary information to qualify a default property (for example, change txtBox = "Text" to txtBox.Text = "Text"). However, when you have a mysterious late-bound type that's defined only as an object, this trick fails. For example, if x, y, and z were real objects with numerous properties (such as x.Value), the statement z = x + y wouldn't work. Visual Basic .NET would have no way of knowing what properties to use for this calculation.
The solution to this problem is simple. When programming in Visual Basic 6, be careful to always define data types. Also, don't use late-bound objects. Not only are they slower, because VB has to inspect them before they are used, but they may lead to migration headaches.
In Visual Basic 6, you could use the Load statement to create a form without displaying it:
' VB 6 code. Load frmMain
In Visual Basic .NET you can accomplish the same sort of thing by creating an instance of your form class, but not displaying it.
Dim frm As New frmMain()
Both of these techniques allow you to pre-configure the form and its controls before displaying the form. In VB6OrderMaker, Load statements are used extensively to provide Wizards that automatically load other forms, use their objects and procedures to calculate totals and create order items, and then unload them. This approach is well organized, but not nearly as efficient or easy to use as a real class- based design. It also leads to migration errors, because the Load statement is not automatically upgraded.
There is at least one easy way to replace the Load statement in a migrated project:
This code initializes the form and makes it available through the shared DefInstance property.
The Upgrade Wizard is not able to update printing code. During the migration of VB6OrderMaker, the code used to select the printer, configure device settings, and output an order was flagged with error messages. The only way to resolve such problems is to rewrite the code for the new PrintDocument object in the .NET class library. If you've spent hours generating custom formatted output in your original application, you will not enjoy the migration process.
Code that interacts with the Windows clipboard will also fail to work after a migration. Once again, the Upgrade Wizard leaves the hard work to you, and you need to rewrite the code with the .NET equivalent. In VB6OrderMaker, the clipboard was used to transfer information into the RichText control for a preview. Clearly, a better approach is to use .NET's new PrintPreview control. Unfortunately, there's no migration path between the two. If you are still working on a VB 6 project, you have no easy way to use print preview features similar to those available in VB .NET, and you will have to resort to third-party components or nontraditional approaches, such as those found in VB6OrderMaker. In .NET, however, there's really no reason not to use the bundled PrintPreview control.
The HelpContextID property is not supported in Visual Basic .NET. If you want to create context-sensitive help, you'll need to use the HelpProvider control, which was discussed in Chapter 4 of The Book of VB .NET. This control provides a number of minor enhancements, but once again, the Upgrade Wizard won't help you make the coding changes. Any program-VB6OrderMaker, for instance-that makes significant use of context-sensitive help will need at least some rewriting.
In Visual Basic .NET, you can't use the same menu component for an application (pull-down) menu and a context menu. In Visual Basic 6, you had to create a pull-down menu before you could use it in a context menu. Microsoft schizophrenia once again?
The VB 6 code used to display a context menu looked like this:
' 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
When importing a project that uses context menus, the Upgrade Wizard leaves the PopUpMenu command in its original state, and flags it as an error. Before you can make it work, you have to create a new ContextMenu object. Then, you have to copy the ordinary menu information into the ContextMenu:
Dim mnuPopUp As New ContextMenu() Dim mnuItem As MenuItem
For Each mnuItem In mnuOrderOptions.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. Because the current form is always available through the Me keyword, it provides a convenient place to attach the ContextMenu reference.
The code for displaying a context menu in VB .NET is similar to that used in VB 6, but not exactly the same:
Private Sub Form1_MouseDown(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown If e.Button = MouseButtons.Right Then Me.ContextMenu.Show(Me, New Point(e.X, e.Y)) End If End Sub
The best way to manage your context menus, and the best time to create them, are up to you. Once again, the Upgrade Wizard won't offer any help beyond identifying the problem.
Control arrays present a particularly unpleasant example of what can go wrong with migration. In Visual Basic 6, control arrays were often the best way to solve problems. With a control array, numerous similar controls are placed into an array. 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
In .NET, this approach can be replaced in various different incompatible ways, including data binding, new list controls, or collection classes. Control arrays, however, aren't supported.
Control arrays also allowed a program to use a single event handler for numerous similar controls, which was 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, as discussed in Chapter 4 of The Book of VB .NET.
Private Sub Highlight(ByVal sender As Object, ByVal e As MouseEventArgs) _ Handles lblLine1.MouseMove, lblLine2.MouseMove ' You can add more events to the Handles list, ' or use the AddHandler statement instead. Dim lbl As Label = CType(sender, Label) lbl.ForeColor = Color.RoyalBlue End Sub
To summarize: Control arrays were a quirky if 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. There's no easy way to design around it in VB 6, since control arrays are often a good design approach in that environment. Unfortunately, this is one of the Upgrade Wizard's fundamental limitations.
In Visual Basic 6, if you defined an object with the New keyword, it had the strange ability to automatically recreate itself:
' VB 6 code. Dim objPerson As New Person Person.Name = "John" Person = Nothing ' The Person object is destroyed. Person.Name = "John" ' At this point, an empty Person object is reinitialized.
This was generally not what programmers expected, and it led to quirky errors and memory waste. In Visual Basic .NET, the New keyword simply allocates space for the object; it does not cause any unusual re- initializing behavior.
If you've made use of this trick, either deliberately or unwittingly in VB 6, the code won't work in .NET. When you try to use the destroyed object, you will receive a null reference error. You'll have to rewrite the preceding example like this:
Dim objPerson As New Person() Person.Name = "John" Person = Nothing ' The Person object is destroyed.
Person = New Person() Person.Name = "John"
Re- initialization may be a minor detail, but it's also one more potential migration headache, particularly if the project you want to import has not been created using best practices.
Visual Basic 6 had built in methods for drawing circles and other shapes directly onto a form. In Visual Basic .NET, these graphical routines have been enhanced and replaced by the GDI+ library, which you can access through the System.Drawing namespaces. Once again, any original code you may have written will need to be scrapped. This includes palette management, which VB6OrderMaker used to ensure that the splash screen is displayed properly on older 256-color monitors.
In Visual Basic 6, you might have used the Terminate event to perform automatic cleanup tasks, such as closing a database connection or deleting a temporary file. In Visual Basic .NET, this technique is a guaranteed to cause problems, because the .NET framework uses non-deterministic garbage collection.
Garbage collection in VB .NET works in much the same way that garbage collection works in many actual communities. In most neighborhoods, residents can pinpoint the time that garbage is placed into the appropriate receptacle, but they really have no idea when someone will be motivated to take it out. Similarly, in .NET garbage collection may be put off until the system is idle, and can't be relied upon to release limited resources. You might find other surprises if you use the Terminate event to try and interact with other forms in your application. Generally, these techniques won't work in VB .NET.
As with control arrays, a substantial difference in programming philosophies underlies this problem. In Visual Basic 6, using the Terminate method was a useful approach to making sure that cleanup was always performed the moment the class was released. In Visual Basic .NET, you are better off adding a Dispose method, and relying on the programmer to call this method after using an object and just before destroying it. The Upgrade Wizard tries to encourage this change: It places code from the Terminate event into a new, separate method so that you can easily call it when needed. It also overrides the Finalize method and adds a call to the new method to ensure that cleanup is performed:
Public Sub Class_Terminate_Renamed() ' Code goes here. End Sub
Protected Overrides Sub Finalize() Class_Terminate_Renamed() MyBase.Finalize() End Sub
This a good start, but you would be better off putting the code in a method called Dispose (rather than Class_Terminate_Renamed, as the Upgrade Wizard uses in our example). You'll also still need to modify the code that uses the class, because you'll want to make sure that it calls the Dispose method before destroying the object.
VarPtr, StrPtr, ObjPtr
These undocumented functions have traditionally been used by Visual Basic experts to find the memory addresses where variables or objects were stored. These functions allowed addresses to be passed to a DLL routine or to the Windows API, which sometimes required this information. In Visual Basic .NET, you hopefully won't need to have this kind of low-level access to memory information, as it complicates programs 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:
' You will need to import the namespace shown below to use this code as written. ' Imports System.Runtime.InteropServices
Dim MyGCHandle As GCHandle = GCHandle.Alloc(MyObj, GCHandleType.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()
Of course, the coding you use is up to you, but be aware that the Upgrade Wizard will simply place an error flag if it finds the unsupported VarPtr, StrPtr, or ObjPtr function.
Memory Storage for User-Defined Types
In Visual Basic 6, you could assume that user-defined types represented a contiguous block of memory. This is not true for the .NET equivalent, structures, which use a more efficient allocation of space. This change only has an effect if you are using low-level DLLs or COM components (for instance, a legacy database that expects data to be passed as a block of memory). To resolve this issues, you'll have to delve into some of .NET's advanced data type marshalling features, which are beyond the scope of this book, but detailed in the MSDN class library reference under the System.Runtime.InteropServices namespace.
Optional parameters in VB .NET require default values, as explained in Chapter 3 of The Book of VB .NET. This is a relatively painless change. However, one consequence is that you can't rely on IsMissing to tell if a value has been submitted (although you can check for your default value or use IsNothing, which is the Upgrade Wizard's automatic change). Keep in mind that overloaded functions often yield better .NET options than do optional values.
Goto and Gosub
Support for these statements was scaled down in early beta versions of VB .NET, but has been added back for the final release. But even though you can doesn't mean you should! Goto and Gosub are archaic programming concepts, and using them is a direct road to programming nightmares. It's a surprise that they remained all the way through Visual Basic 6, let alone made the jump to the .NET world.
Other New Behaviors
"New behavior" is the term that the Upgrade Wizard uses in its upgrade report when it makes a change from one property or method to another that is similar, but not identical. In many cases, this warning just represents an improvement to a control. In other situations, it may represent a deeper problem that requires significant reworking.
Most experienced Visual Basic developers have spent significant time learning their favorite controls, and know how to exploit all the associated quirks and idiosyncrasies. This poses a problem for migration, where optimized code might not work at all (or worse, might appear to work, but will then cause problems under unusual future circumstances).
NOTE This issue raises a larger question. Namely, when is it safe to migrate a project? Even if the process appears to succeed, you will need hours of testing to verify its success before you can trust the updated application enough to release it into a production environment.
Preparing for VB .NET
Are the migration features in Visual Basic .NET impressive or depressing? It really depends on how you look at it. Certainly, they represent a minor technological feat. At the same time, many developers argue that they are useless for using real VB 6 applications in the .NET environment. Even if you can import a project, the result may be a rather ugly mess of old concepts, dressed up with workarounds added by the Upgrade Wizard. This kind of application can be difficult to work with and enhance, thus defeating the purpose of migration.
If you're working on a Visual Basic 6 project today, your best choice is to make rigorous use of class-based designs. If you take this protective step, then even if you can't import an entire project into .NET, you will at least be able to import or recreate all of your business objects and data classes. Generally, data classes are much easier to import than other components, because they don't use such VB 6-specific features as file or printer access, and they don't directly create user interface or require forms support. You can then import your business objects, add a new .NET user interface tier (taking advantage of the latest features in Windows Forms), and add lower-level data access components, if necessary, to support ADO.NET or .NET file access through streams. The ability to upgrade is one of the remarkable benefits of a structured three-tier design. The more you can break your application down into functional units, the better chance you'll have to reuse at least some of its elements in the .NET environment.
What Comes Next?
This article has shown both the beauty and the ugliness of backward compatibility. Visual Basic .NET is at its most elegant when dealing with COM interoperability, allowing you to work with most components and even drop ActiveX controls into your application without a second thought. However, the picture sours if you need to import an average, mid-sized Visual Basic project, which is almost guaranteed to crumble in the face of numerous incompatibilities.
As always, remember that just because a program is created in an older version of Visual Basic it does not mean it needs to be brought to .NET. These "legacy" programs will be supported on the Windows platform for years to come. Conversely, just because an ActiveX control can be inserted into your project doesn't mean that you can't reap more benefits by trying to achieve the same effect using the class library. However, you're likely to have at least some third-party ActiveX controls that you don't want to abandon in certain situations. Remember to consider the tradeoff-if converting to .NET requires that you import dozens of COM components, then you may be better off maintaining your program in unmanaged Visual Basic 6.
If you are a COM guru (or are planning to become one), you will probably want to explore .NET's advanced COM support features. These include tools for handling COM interoperability on your own, such as the System.Windows. Forms.AxHost class and the types in the System.Runtime.InteropServices. Be forewarned: Unless you are a war-hardened COM veteran, you won't be able to do much with these namespaces that you can't do even better with the automatic wrapper class generation features in Visual Studio .NET.
Matthew MacDonald, an author, educator and MCSD developer, has worked with Visual Basic and ASP since their inceptions. He is the author of The Book of VB .NET (No Starch Press) and ASP.NET: The Complete Reference (Osborne McGraw-Hill) and co-author of Programming .NET Web Services (O'Reilly).