Francesco's blog

 Friday, January 11, 2008

Many readers asked whether I was about to release a book about VB 2008 and, more in general, how come I have reduced my writing activity so dramatically. The answer is in this post.

Immediately after publishing my C# 2005 book I started working on a very ambitious software project. Now, after exactly two years of hard work, VB Migration Partner is in beta test stage and is quickly approaching the public release.

The name of this tool says it all: in a nutshell, it is a converter of VB6 applications to VB.NET. I won’t waste your time trying to emphasize how weak the Upgrade Wizard that comes with Visual Studio is: if you’ve never tried it out, just google for “vb6 migration” and read what developers and bloggers have to say about it. Our goal at Code Architects was to build a better mouse trap than that.

A word on the migration-vs-rewriting debate: Many industry experts suggest that you should rewrite your VB6 from scratch rather than trying to port it to .NET automatically using a conversion tool, because the two environments are just too different. In theory this argument makes a lot of sense; in practice, however, when you have a real-world business app with some hundreds thousands lines of code – or even millions of lines! – a rewrite from scratch is simply too expensive, especially when the developers who wrote it originally aren’t working with the company any longer, the documentation is poor, remarks are scarce, etc. When facing the perspective of investing many years/man in this daunting task, it’s no surprise that many companies have postponed the migration to “some other time.” Now they are realizing, however, that the migration step can’t be postponed any longer, either because a few VB6 apps don’t work under Microsoft Vista – possibly because they use 3rd-party controls that are incompatible with the new Microsoft operating system – or, above all, because Microsoft declared that VB6 support ends on March 2008.

The problem is, automatic code converters don’t have a good reputation and rarely keep their promises, regardless of the language you are migrating from/to. In most cases, they manage to convert 95% of existing code, or less. With a 100,000 line application – a medium-size application of average complexity, then – having to manually fix the remaining 5% means that you have to fix about five thousand lines. It doesn’t sound too bad, until you realize that most of these “fixes” might take hours or even days. For example, consider that VB6 and VB.NET have two vastly different (and incompatible) programming models for drag-and-drop. Therefore one OLEDrag method counts as an individual problematic statement, yet it forces you to rewrite a large portion of your code. Drag-and-drop isn’t the only example, because the same concept applies to graphic statements, printing, data-binding, object finalization, component initialization, and many others.

We tested our VB Migration Partner on hundreds of VB6 apps – for well over one million lines – and found out that it delivers code that compiles and runs correctly most of the times, with an average of one compilation error every 1,100 lines of code. This corresponds to an accuracy higher than 99.9%. Not bad, uh? Of course the actual accuracy depends on how well the VB6 code is and on the features that the application uses, thus you can get a better idea of how good our tool is by having a look at the VB6 features that itsupports ...and that are out of reach for most other conversion tool on the market:

  • arrays with nonzero LBound
  • Gosub, On...Goto, and On ...Gosub
  • auto-instancing variables (Dim x As New Person), which keep their lazy-instantiation feature even under VB.NET
  • As Any parameters and callback addresses in Declare parameters (e.g. EnumWindows)
  • default properties resolved correctly even for late-bound variables
  • deterministic finalization for objects such as Connection and Recordset, to ensure that the connection is correctly closed when the variable is set to Nothing or the current scope is exited
  • (limited) support for Variant variable and null propagation in expressions
  • all 60+ controls in the VB6 toolbox, with the only exceptions of OLE container and Repeater, plus the support for common ActiveX controls and components such as WebBrowser, ScriptControl, Scripting runtime, and the MSWLess library
  • control arrays, including arrays of 3-rd party controls
  • popup menus
  • the Controls.Add method and the VBControlExtender object, to dynamically create data-entry forms
  • graphic methods (Line, Circle, PSet, PaintPicture, etc.), with support for any ScaleMode, including custom user modes
  • the Printer object and the Printers collection
  • OLE drag-and-drop
  • UserControl classes, with support for advanced features such as the Ambient and Extender objects
  • data-binding to DAO, RDO, and ADO Data controls, ADO recordsets, DataEnvironment objects
  • non-hierarchical DataEnvironment objects
  • ADO data source and simple data consumer classes and user controls (e.g. custom ADO Data controls)
  • MultiUse, PublicNotCreatable, GlobalMultiUse classes
  • persistable classes, MTS/COM+ classes

The number of supported features is so high that it is simpler to list the features that are not supported: UserDocument, PropertyPage, WebClass and DHTML Page components; undocumented methods (VarPtr, ObjPtr, StrPtr); “classic” drag-and-drop, and little else.

VB Migration Partner can convert an entire VB6 project group in one operation, in which case it typically delivers better quality code because it can see “inside” a DLL that belongs to the group. The tool also includes a very sophisticated code analyzer that can spot unused members: a large project that has evolved over many years can easily include 5-10% of “dead code”, thus this feature alone can save you a lot of precious time because it allows you to focus on just the code that is really important.

One of the secrets for such high error-free conversion factor is the support for migration pragmas. A migration pragma is a special remark that you can add to the original VB6 code and that teaches VB Migration Partner how a given portion of code must be translated, if many alternatives exist. For example, the following ArrayBounds pragma tells VB Migration Partner that all the arrays in the current projects must have their lower bound index forced to zero:
          '##project:ArrayBounds ForceZero
Pragmas can have project, class, method, or variable scope. A pragma scoped at the variable- or method-level always have higher priority than pragmas scoped at the project- or class-level. For example, you can override the previous pragma inside a method, and then override this latter pragma for a given array variable:
          Sub Test()
             '## ArrayBounds Shift
             '## arr.ArrayBounds ForceZero
             '## names.ShiftIndexes 1
             Dim names(1 To 10) As String
             Dim values(1 To 10) As Long
             Dim arr(1 To 10) As Integer
             names(1) = "John": names(2) = "Ann"
             '...
          End Sub

The Shift option tells VB Migration Partner that the LBound and UBound indexes must be shifted towards zero, so that the total number of elements in the array is preserved. The ShiftIndexes pragma –applied only to the names array – ensures that constant indexes are correctly shifted too:
          Sub Test()
             Dim names(9) As String
             Dim values(9) As Integer
             Dim arr(0 To 10) As Short
             names(0) = "John": names(1) = "Ann"
          End Sub

VB Migration Partner supports over 50 pragmas. For example, the SetName pragma changes the name of a control or variable during the migration, SetType changes its type, AutoNew implements the auto-instancing behavior of As New variables, AutoDispose implements the deterministic finalization when the variable is set to Nothing or the current scope is exited, and so forth.

One of the main defects of a “traditional” conversion tool is that – after the first migration – there is no relationship between the original VB6 project and the new VB.NET project. If the migration process takes weeks or months – which is common for large projects – and if the VB6 application must evolve in the meantime with new features and bug fixing, then at the end of the migration process the VB.NET code is already outdated and must be manually patched. The margin for mistakes in this phase is very high.

VB Migration Partner allows you to work around this issue, by means of pragmas and what we call the convert-text-fix cycle methodology. In a nutshell, you can iteratively migrate a given VB6 project by reaching the zero compilation errors stage and then the zero runtime error stage by simply inserting pragmas in the original project, but without modifying any VB6 executable statement. Thanks to the convert-test-fix cycle you can face complex migration tasks and keep the VB6 and VB.NET versions of your app always in sync.

I am especially proud of VB Migration Partner’s refactoring engine, which is able to apply many optimization techniques the result of the “raw” conversion. For example, it can merge variable declarations and the initializations, to generate Return statements, and to leverage the compound assignment operators (e.g. +=), and more:

    Function GetValue() As Long     =>     Function GetValue() As Integer
       Dim x As Long                          Dim x As Integer = 123
       x = 123                                ...
       ...                                    If x > 0 Then
       If x > 0 Then                             Return x
          GetValue = x                        ElseIf x = 0 Then
          Exit Function                          Return -1
       ElseIf x = 0 Then                      End If
          GetValue = -1                       ...
          Exit Function                       x += 1
       End If                                 ...
       ...                                 End Function
       x = x + 1
       ...
    End Function

Other refactoring techniques can be achieved by means of pragmas. For example, you can move the declaration of a variable inside a For or For Each method, as in this example:

    Dim i As Integer                =>     For i As Integer = 1 To 100
    For i = 1 To 100                          ...
       ...                                 Next
    Next

On top of the cake: in spite of all these features, VB Migration Partner is up to 8 times faster than the Upgrade Wizard that comes with Visual Studio!

You can learn all about VB Migration Partner on our new site www.vbmigration.com. The web site hosts a Resource section where we dissect each and every difference between VB6 and VB.NET – most of which have never been documented anywhere – thus you can find many useful tips even if you don’t plan to use our tool. I also opened a new blog, entirely devoted to migration techniques.

That's it for now. Stay tuned!

1/11/2008 11:00:33 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [11]  | 
 Saturday, December 22, 2007

When you migrate code from VB6 - regardless of whether you are using an automatic conversion tool - chances are that string-intensive code will actually run slower under VB.NET, if it uses a lot of string concatenation operations. For example, this code takes 2.8 seconds when it runs in VB6 and 27 seconds after its conversion to VB.NET on my 3GHz system:

Dim s As String = ""
For i As Integer = 1 To 100000
    s = s + "*"
Next

The solution is of course trivial: just replace the string variable with a StringBuilder object. Alas, this fix requires that you completely revise your source code, because you need to replace all + and & operators with the Append method, not to mention cases where the StringBuilder is used as an argument to string functions such as Trim or Len.

Is there a way to speed up the previous code with a minimal impact on the code itself? The answer is yes and the solution is actually very simple: you just need to create a VB.NET class that is based on the System.Text.StringBuilder object, that redifines the + and & operators, and that supports the implicit conversion to and from the System.String type. Authoring such a StringBuilder6 class is a matter of minutes:

' a wrapper for the StringBuilder object, with support for + and & operators

Public Class StringBuilder6

    Private buffer As New System.Text.StringBuilder

    ' return the inner string

    Public Overrides Function ToString() As String
       
Return buffer.ToString()
    End Function

    Public Shared Operator +(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6
        op1.buffer.Append(op2)
       
Return op1
    End Operator

    Public Shared Operator &(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6
        op1.buffer.Append(op2)
        Return op1
    End Operator

    ' convert to string

    Public Shared Widening Operator CType(ByVal op As StringBuilder6) As String
       
Return op.ToString()
    End Operator

    ' convert from string

    Public Shared Widening Operator CType(ByVal str As String) As StringBuilder6
        Dim op As New StringBuilder6()
        op.buffer.Append(str)
        Return op
    End Operator

End Class

Once the StringBuilder6 class is in place, you just need to replace the type of the String variable:

Dim s As StringBuilder6 = ""

After this edit, the loop runs in 0.008 seconds, that is about 2000 times faster!!! Not bad, for such a simple fix :-)

Regardless of whether you are migrating code from VB6 or you've written VB.NET or C# code from scratch, the StringBuilder class gives you a quick and simple way to check whether your string concatenations can be optimized by resorting to a StringBuilder.

 

12/22/2007 11:40:06 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [3]  | 
 Wednesday, May 31, 2006

Visual Basic 6's App object exposes a useful property named PrevInstance, which allows you to determine whether there are other instances of the same application running. This property has never been implemented in VB.NET, even though a VB2005 application can use the StartupNextInstance event for this purpose.

' to display this code, open the Application page of the My Project designer and click the Application Events button
Namespace
My
  
Partial Friend Class MyApplication
     
Private Sub MyApplication_StartupNextInstance(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance
         ' another instance of this application has been launched
      End Sub
  
End Class
End
Namespace

The problem of this approach is that the event is raised in the first instance of the application, not in the new instance, therefore when the VB.NET application starts you can't determine for sure whether it is the only instance in the system. In other words, you can learn only whether and when another application is launched, not if the current applicatin is the first and only running instance.

.NET developers have solved this problem in a variety of ways, for example using Mutexes or by iterating over the collection returned by the Process.GetProcesses method. In .NET we have a new and more elegant alternative, based on the System.Threading.Semaphore type. A semaphore is a counter which can be incremented and decremented. If its value becomes zero, a thread can't decrement it and must wait for another thread to increment it to a value >0. Interestingly, if the semaphore has a name it becomes a system-wid object which can be shared by all the applications that ask for a reference to a semaphore with that specific name. It is therefore sufficient that the app creates a semaphore with a unique name just after its launch (e.g. it can be a name that includes the executable's path) to have all the instances of a given app share the same semaphore and therefore the same counter. The only problem left to solve is ensure that the semaphore's counter be correctly resotred when the application terminates, but this is easy to achieve by means of a Finalize method.

In addition to the PrevInstance property - which returns False if the application was the only running instance when the application was launched - the following VB6App class exposes also the InstanceCount property, which returns the total number of instances in that moment (the current app is included in the count). Here's the VB2005 version of this class:

Class VB6App
   ' the default instance
  
Private Shared DefValue As New VB6App

   ' the system-wide semaphore
  
Private semaphore As System.Threading.Semaphore
   ' initial count for the semaphore (very high value)
  
Private Const MAXCOUNT As Integer = 10000

   Private Sub New()
      Dim ownership As Boolean = False
      ' create a unique name, but strip invalid characters
     
Dim name As String = "VB6App_" & System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "")
      semaphore = New System.Threading.Semaphore(MAXCOUNT, MAXCOUNT, name, ownership)
      ' decrement its value 
      semaphore.WaitOne()
      ' if we got ownership, this app has no previous instances
      m_PrevInstance = Not ownership
   End Sub

   ' the PrevInstance property returns True if there was a previous instance running 
  
' when the default instance was created
  
Private Shared m_PrevInstance As Boolean

   Public Shared ReadOnly Property PrevInstance() As Boolean
      Get
         Return m_PrevInstance 
      End Get
   End Property

   ' return the total number of instances of the same application that are currently running 
  
Public Shared ReadOnly Property InstanceCount() As Integer
      Get
         ' release the semaphore and grab the previous count 
        
Dim prevCount As Integer = DefValue.semaphore.Release()
         ' acquire the semaphore again
        
DefValue.semaphore.WaitOne()
         ' eval the number of other instances that are currently running 
        
Return MAXCOUNT - prevCount
      End Get
   End Property

   Protected Overrides Sub Finalize()
      ' increment the semaphore when the application terminates
     
semaphore.Release()
   End Sub

End Class

Notice that this class has a Finalize method without implementing IDisposable. It is one of those special cases when it is OK to violate the Dispose-Finalize pattern.

To understand how the class works, just keep in mind that the Semaphore.Release method increments the internal counter, whereas the WaitOne method decrements it. You must test the VB6App.PrevInstance property as soon as the application starts, for example in the Sub Main method or in the Load event hander for the main form, as to let the VB6App class store the boolean value internally. The same form might then test the value of the InstanceCount property on exit, for example if you need to run some cleanup code when the last application instance is about to terminate:

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
   If Not VB6App.PrevInstance Then 
      ' open the common log file
      ' ...
   End
If
End Sub

Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
   If VB6App.InstanceCount = 1 Then
     
' close the common log file.
      ' ...
  
End If
End Sub

Here's the C# version of the class. (I fixed the original version to fix two syntax errors mentioned in one of the comments below.)

public class VB6App
{
   // the default instance 
   private static VB6App DefValue = new VB6App(); 
   // the system-wide semaphore
   private System.Threading.Semaphore semaphore; 
   // initial count for the semaphore (very high value)
   private const int MAXCOUNT = 10000;

   private VB6App() 
   { 
      // create a named (system-wide semaphore)
      bool ownership = false
      // create the semaphore or get a reference to an existing semaphore

      string name = "VB6App_" + System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\\", "");
      semaphore = new System.Threading.Semaphore( MAXCOUNT, MAXCOUNT, name, out ownership); 
      // decrement its value
     
semaphore.WaitOne(); 
      // if we got ownership, this app has no previous instances
      m_PrevInstance = !ownership;

   }

   // the PrevInstance property returns True if there was a previous instance running
  
// when the default instance was created

   private static bool m_PrevInstance ;

   public static bool PrevInstance 
  
      get 
     
         return m_PrevInstance ; 
     
   }

   // return the total number of instances of the same application that are currently running

   public static int InstanceCount 
  
      get 
     
         // release the semaphore and grab the previous count 
        
int prevCount = DefValue.semaphore.Release(); 
         // acquire the semaphore again
        
DefValue.semaphore.WaitOne(); 
         // eval the number of other instances that are currently running 
        
return MAXCOUNT - prevCount; 
     
   }

   ~VB6App() 
  
      // increment the semaphore when the application terminates
     
semaphore.Release(); 
   }
}

5/31/2006 10:10:12 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
 Monday, May 01, 2006

Many, if not most, Windows Forms samples you can find on the 'net include one or more calls to unmanaged code in Windows DLLs, often in the form of calls to the SendMessage API methods to fix some of the (very few) missing features of .NET controls. The problem is, such a call to unmanaged code creates a problem when the program runs as a ClickOnce application, because it requires higher CAS privileges.

Even though this problem doesn't have a generic solution, when you just need to send a message to the control you are inheriting from, you can avoid an explicit call to SendMessage by invoking the protected DefWndProc method instead. For example, let's say that you are writing an enhanced ComboBox that exposes the TopIndex property, which can set or return the index of the first visible item in the list area. These two operations can be implemented by sending the control the CB_SETTOPINDEX or CB_GETTOPINDEX message, respectively. Here's how you can use the DefWndProc method instead of SendMessage:

Public Class ComboBoxEx
  
Inherits System.Windows.Forms.ComboBox

   Public Property TopIndex() As Integer
      Get
         Const CB_GETTOPINDEX As Int32 = &H15B
         Dim m As New Message()
         m.HWnd = Me.Handle
         m.Msg = CB_GETTOPINDEX
         Me.DefWndProc(m)
         Return m.Result.ToInt32()
      End Get
      Set(ByVal value As Integer)
         Const CB_SETTOPINDEX As Int32 = &H15C
         Dim m As New Message()
         m.HWnd = Me.Handle
         m.Msg = CB_SETTOPINDEX
         m.WParam = New IntPtr(value)
         Me.DefWndProc(m)
      End Set
   End Property

End Class

By the way, such an enhanced ComboBox can be useful when migrating a VB6 app to VB.NET. In fact, the VB6 ComboBox and ListBox controls expopse the TopIndex property, whereas under .NET only the ListBox control exposes this property. If you have any VB6 code that takes advantage of the ComboBox's TopIndex, the simplest approach is replacing the standard ComboBox with a ComboBoxEx instance.

5/1/2006 9:48:39 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
 Saturday, April 29, 2006

I am working at the migration of a large VB6 project and at one point I needed to drop a few methods in the VB6 app, in such a way that the methods would be "invisible" to VB.NET. This action was necessary because these methods performed similarly to a .NET native method. Obviously, I had the option to manually delete the methods after the migration, but if their number is high and if you need to run the wizard more than once on the same VB6 app (as it happens frequently, in the process of preparing the app for the migration), then deleing these portions of code each time becomes a nuisance.

Apparently, you can't instruct the migration wizard to ignore one or more pieces of code. Nevertheless, the solution is quite simple: you just need to bracket these pieces of code - entire methods or just individual statements - in a #If Win32 ...#End If block. The Win32 compilation constant - that is probably ignored by most VB developers today - was introduced when Visual Basic 4 was released, and was recognized also by VB5 and VB6 (but not by any version of VB.NET). Visual Basic 4 is the only version of this language that is available in 16-bit and 32-bit edition, and this compilation constant (together with Win16) allowed to define blocks of code that were compiled under only one of those versions, while allowing to mantain a single source code for both. Therefore, the following VB6 code:

#If WIN32 Then
   Private Sub Do Something(ByVal n As Integer)   
      ' ...
   End Sub
#End If

is correctly migrated to VB.NET by the migration wizard as follows:

#If WIN32 Then
   Private Sub DoSomething(ByVal n As Short)
      ' ...
   End Sub
#End If

but this piece of code will be skipped over by the VB.NET compiler because the Win32 constant isn't defined under VB.NET.

This "feature" can actually create a problem if you are migrating a VB6 app that has been evolved from an older application that was originally written in VB4. In this case, in fact, it is possible that its source code contains one or more #If Win32 blocks. In most cases you *want* to migrate this code, but these portions will be ignored after the migration to VB.NET. If this is the case, you should then locate all the the occurrences of #If Win32 statements in the code and delete them before the migration.

4/29/2006 8:18:48 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, February 15, 2006

In the VB6 world - and in the COM world, in general - you "destroy" an object by simply putting it to Nothing, unless of course there are other variables that are pointing to that specific instance. The VB6 runtime invokes the Class_Terminate method, if it exists, then it deallocates all the resources belonging to the object, memory included. This is a recursive process, and destroys any object that is owned by the object being set to Nothing.

We know well that setting a VB.NET object variable to Nothing doesn't fire any event, because the Terminate event isn't supported in .NET. If the object implements the Finalize method, this method is invoked by the garbage collection, but this invocation occurs some time later. The time interval between the Set to Nothing statement and the invocation of Finalize depends on many factors, but it could be as long as a few minutes or even hours, if the application doesn't allocate many objects and doesn't stress the garbate collector.

This behavioral difference can be a serious issue when migrating a VB6 application to VB.NET. For example, if the code in Class_Terminate closes a database connection, or a file, or a serial port, or deletes confidential information from disk, or unloads a form, then the delay between the "logical" destruction of the object (i.e. the Set to Nothing) and its "physical" destruction (when the Finalize method runs) can compromise the correct working of the application after its migration to the .NET world.

Unfortunately, it isn't possible to have VB.NET "automagically" behave like VB6 in this respect. However, it is possible to take a step that can greatly reduce the problem, without much impact on the code structure. Once again, this is possible thanks to generics. To see how, create a Module and add the following code to it, so that the SetNothing method is visible to the entire application:

Public Sub SetNothing(Of T)(ByRef obj As T)
  
' Dispose of the object if possible
  
If obj IsNot Nothing AndAlso TypeOf obj Is IDisposable Then
     
DirectCast(obj, IDisposable).Dispose()
  
End If
  
' Decrease the reference counter, if it's a COM object
  
If Marshal.IsComObject(obj) Then
     
Marshal.ReleaseComObject(obj)
  
End If
  
obj = Nothing
End Sub

Next, you can apply the search-and-replace feature in Visual Studio to the code produced by the migration wizard, to replace all occurrences of the var = Nothing pattern with the SetNothing.(var) pattern. Obviously, this technique doesn't really solve all the abovementioned problems, because it works only with the variables that are explicitly set to Nothing via code, and doesn't work with variables that are implicitly cleared when they exit the current scope (for example at the end of the method).

Notice that the search-and-replace operation include a variable part - that is, the name of the instance that is set to Nothing - therefore making this replacementement automatically seems impossible or unpractical. But, fortunately, Visual Studio supports regular expressions in search-and-replace operations (as I explain here), therefore you can search for the <{:i} = Nothing string and specify the SetNothing(\1) string in the Replace with field. Et voilà :-)

NOTE for those who are unfamiliar with regexes: the Find What string specifies that you want to search for a variable name (:i) that is located at the beginning of a word (<); curly braces in {:i} cause the variable name to be tagged, so that you can refer to the variable name in the Replace With regular expression, by means of the \1 placeholder. That's it.

2/15/2006 7:23:34 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Sunday, January 29, 2006

In this period I am actively researching the many problems you face when migrating VB6 apps to .NET. I don't like *migrating* applications, because I always prefer to rewrite them from scratch to leverate all the features of .NET and above all because the conversion wizard doesn't do a great job and produces ugly and non-maintenable code. Better, the wizard does a decent job, as long as it doesn't have to handle incompatibilities between VB6 and VB.NET.

Some of these incompatiblities, however, can be solved with a bit of imagination, especially now that VB2005 has features that weren't available before. For example, consider the "classic" problem of converting arrays with a lower index other than zero, a problem that has bothered all VB6 developers trying to porting complex code to VB.NET. Let's say you have this code:

      Dim arr(1 to 10) as Integer
      Dim i As Integer, prod As Integer, v As Variant
      For i = LBound(arr) To UBound(arr)
         arr(i) = i
      Next
      For Each v in arr
         prod = prod * v
      Next

The conversion wizard will replace the index "1" with the index "0", therefore the array has one more element. It's evident that, at the end of execution, the value of prod will be zero, whereas it should be equal to the factorial of 10. This sort of bug is quite subtle, and in practice you're forced to scrutinize your source code and re-test the application entirely. A better, manual approach consist of fixing the Dim statement to "shift" the array so that its first non-empty element has zero index, and then modify ALL the references to the elements of the array, to account for the shift:

      Dim arr(0 to 10-1) as Integer     
      Dim i As Integer, prod As Integer
      For i = LBound(arr) To UBound(arr)
         arr(i - 1)  = i
      Next

Unfortunately, also this approach requires a lot of time and attention, and in some cases it can't be used, for example when the array is passed to a method that must work with arrays of any type (and whose code doesn't know that it has to shift the index). In yet other cases, the VB6 source code might use the value returned by LBound or UBound, and this code wouldn't work well after the migration.

The question is therefore: is it possible to convert this code to VB2005 without having to worry about all these issues? The solution has been relatively simple, thanks to generics and a few tricks with inheritance:

' Base class

Public Class VBArrayBase
  
Protected Friend lowerIndex As Integer
  
Protected Friend upperIndex As Integer
End Class

' One dimensional array of type T

Public Class VBArray(Of T)
  
Inherits VBArrayBase
  
Implements IEnumerable

   Dim items() As T

   Sub New(ByVal lowerIndex As Integer, ByVal upperIndex As Integer)
     
Me.lowerIndex = lowerIndex
     
Me.upperIndex = upperIndex
     
ReDim items(upperIndex - lowerIndex)
  
End Sub

   Default Property Item(ByVal index As Integer) As T
     
Get
        
Return items(index - lowerIndex)
     
End Get
     
Set(ByVal value As T)
         items(index - lowerIndex) = value
     
End Set
  
End Property

   Public Function GetEnumerator() As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
     
Return items.GetEnumerator()
  
End Function

End Class

Notice how simply the class provides support for For Each loops: it just has to return the IEnumerator object of the inner array. At this point I just needed to extend the LBound and UBound support to the new class. To do so, I created the following public module:

Public Module ArrayFunctionsVB6
   Function LBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function UBound(ByVal arr As Array, Optional ByVal rank As Integer = 1) As Integer
     
Return Microsoft.VisualBasic.Information.LBound(arr, rank)
  
End Function

   Function LBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.lowerIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function

   Function UBound(ByVal arr As VBArrayBase, Optional ByVal rank As Integer = 1) As Integer
     
If rank = 1 Then
        
Return arr.upperIndex
     
Else
        
Throw New IndexOutOfRangeException()
     
End If
  
End Function
End
Module

The module must expose two overloads for each method, one overload for standard arrays and the other for the new VBArray(Of T) class. Alas, you can't have a project that references two distinct modules - one in the VB compatiblity library and one in another DLL - where each module contains a different overload of the same method. In this case, only one of the two methods is visible to the main program.

Another interesting detail: the code inside the LBound and UBound methods needs to access the Friend members of the VBArray(Of T) class, but these methods can't have VBArray(of T) in the parameter list, because they aren't generic methods. This is the reason why I have the VBArray(Of T) class derive from VBArrayBase, where these Friend members are defined.

Thanks to the VBArray(Of T) class and the ArrayFunctionsVB6 module, you can migrate the VB6 code by changing only the DIM statement, as follows:

     Dim arr As New VBArray(Of Short)(1, 10)      ' Short instead of Integer

The remainder of the code will work flawlessly, exactly as in VB6, including the For Each loop and calls to LBound and UBound. Seeing is believing! :-)

1/29/2006 3:30:03 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 
Get RSS/Atom Feed
RSS 2.0 | Atom 1.0
Search in the blog
Archive
<July 2009>
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678
Categories

Powered by: newtelligence dasBlog 1.8.5223.1