[DOC] Differences between VB6 and .NET controls

clock January 23, 2009 05:30

In these three years we at Code Architects work very hard to study, discover, analyze, dissect, and devise workarounds for the huge set of differences between VB6 and VB.NET.

We put all this knowledge into our VB Migration Partner, of course. But we thought it was fair to make this information available to the many VB6 developers who still have one or more apps to convert, regardless of whether they would buy our software.

Yesterday we more than doubled the amount of material freely available on vbmigration.com, and in fact we decided to split this knowledge in two pages:

The control page currently lists the differences between VB6 built-in controls (forms, textboxes, labels, buttons, etc.) and their Windows Forms counterparts. We plan to cover the remaining controls - such Windows Common Controls, RichTextBox, etc. - in the near future. Stay tuned!


VB Migration Partner vs. manual porting from VB6

clock January 20, 2009 02:31

More and more VB6 and VB.NET developers are discovering VB Migration Partner and are realizing that it *is* possible to convert VB6 legacy apps in a fraction of the time (and money) that is required when following other approaches or using other tools. However, a casual visit to VB6/VB.NET forums shows that many false myths and misconceptions about migration are still strong.

For this reason, every now and then I like to partecipate to these discussion and throw in my own point of view. I think it makes sense to republish it here. The following text is adapted from a longer comment I posted toa thread entitled Conversion from VB6 to VB.NET 2005 on DevX Forums.


 

I believe that the confusion about automatic code translators raises because most people expect that they work magically without any human intervention. Alas, this is the approach used by the Upgrade Wizard inside VS and this explains why it performs so poorly. In most real-world cases you can't simply run the tool on a piece of contorted VB6 code and have fully working VB.NET code at the first attempt. Instead, a real-world migration process is an iterative task.

A code translator should migrate your code according to *your* personal style and preferences. VB Migration Partner allows this by means of so-called "migration pragmas", which are special remarks that you embed in the original VB6 code. For example, this pragma tells VBMP that special code must be emitted so that the VB.NET can correctly handle default members even in late-bound mode (which is one of the main limitations of Upgrade Wizard):
    '## project: DefaultMemberSupport True
VBMP supports 60+ different pragmas to automate virtually every adjustment that might be needed after an initial quick-and-dirty migration. Pragmas are essential because they allow you to repeat the migration many times and still keep the VB6 and VB.NET code in sync, even if the migration process takes months.

VB Migration Partner can handle many recurring migration issues quite nicely. In most cases it can do it without human assistance, in the worst case it requires that you add a pragma.

Quoting a previous forum comment: "If you are crazy enough to spend 3 months to make a conversion work with a loss of performance compare to the VB6 application, while a 2 months rewrite would do the job and give you something that easier to maintain, go ahead."

With small projects this approach can make sense, provided that you can afford the extra time/cost and that you have a team that is adequately skilled in both VB6 and VB.NET. One of our customers is tackling one "monster" app of about 15 million lines scattered nearly 1500+ VB6 projects; the customer has estimated that it would take about 20 years/man to rewrite it from scratch, which they can't afford. They also estimated that VB Migration Partner will cut cost and time-to-market by at least 70-75%, which made their choice obvious. [NOTE: it later became clear that the actual savings was closer to 85-90%. The migration should be completed in a few months.]

The real value of a good converter such as ours is that an individual developer (or a team) can handle the migration process even if they aren't expert in either the source or the target language. VB Migration Partner contains a huge database of "good programming" rules as well as "obscure bugs" that are unknown to most developers, including a few expert ones. Let me make one simple example:

    Dim buffer As String
    buffer = Space(256)
    ' GetSystemDirectory API returns # of chars in the result string
    buffer = Left(buffer, GetSystemDirectory(buffer, 256))

This code works well in VB6 but delivers bogus results when translated as-is to VB.NET. VB Migration Partner correctly points to the issue and its solution. The question is: How many VB developers are aware of this subtle difference between the two languages? You might object that this code isn't representative of good VB programming, yet there are tons of code like it out there, and most "human converter" aren't skilled enough to find and fix it. (Here I talk from personal experience: it took hours to track down the problem the first time we bumped into it.)

Let me make one more example. In VB6 the default parameter passing mechanism is ByRef, which means that most methods take a byref argument only because the original developer forgot to add an explicit ByVal keyword. The problem is, VB.NET should use ByVal whenever possible because this coding style prevents a lot of compile and runtime errors. (You can have problems when passing an ADODB object to a byref parameter, and there are many other similar cases)

A developer doing the port or the rewrite, therefore, should carefully check each byref parameter and see whether it can be converted into ByVal without changing the code behavior, but this task takes days on a real-world app, because you'd have to check whether *each* parameter is modified, or passed to a method that modifies it, and so on recursively. VB Migration Partner does it automatically in a matter of seconds, emits a warning, and can optionally convert the ByRef into ByVal for you.

The code analysis engine in VB Migration Partner can do many other interesting things. It can remove unused methods, Declare, and Const statements; replace Exit Function with Return, merge DIM and assignments (eg Dim x As Integer = 1), suggest when a StringBuilder object could replace a set of concatenations, and so forth. Once again, all refactoring actions are optional and can be controlled via pragmas. An average developer can surely perform these task manually, but in practice it happens quite seldom because he or she can hardly afford the extra time that these activities require.

I could continue with more examples of how an automatic code converter can do a better job than developers. Not in all cases, but in a large percentage at least. If you are interested, you can find all docs, KB articles, videos, and code samples at www.vbmigration.com. (We provide both the original VB6 code and the resulting VB.NET code.)

As far as I know, we are the only migration tool authors who dare to expose all this information to the public *before* they purchase the product. I hope potential customers or just programmers who are curious about conversion technology will appreciate this "transparency" policy.



Subtle array cloning issues

clock October 5, 2008 10:46

As you may know, VB6 performs a copy of the array when assigning an array to another array variable. After the assignment, the source and destination arrays are distinct objects, as this code demonstrates:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer
dest() = source()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

Conversely, an array assignment under VB.NET just copies the array address:, the source and destination array variables point to the same set of elements. In other words, changing an element through either variable would affect the value seen by the other array variable.

For these reasons, both the Upgrade Wizard and earlier versions of VB Migration Partner automatically force a copy operation when converting an array assignment. This is how Upgrade Wizard converts the above VB6 code snippet:

Dim source(10) As Integer
source(1) = 111
Dim dest() As Integer  
dest = source.Clone()
Debug.Print source(1), dest(1)    ' displays "111,111"
' changing an element in either array doesn’t affect the other
source(1) = 222
Debug.Print source(1), dest(1)    ' displays "222,111"

It seems that adding a call to the Clone method solves the problem once and for all, but unfortunately this isn’t the case. As a matter of fact, there are several cases when the Upgrade Wizard (and probably other VB6-to-VB.NET conversion tools) doesn’t preserve functional equivalence with the original VB6 code.

For startes, a plain Clone method doesn't work correctly if the array you are assigning is an array of arrays (Under VB6, an array of arrays is a Variant array whose individual elements are arrays.) The problem here is that the Array.Clone method performs a shallow copy and doesn’t correctly copy nested arrays and the converted VB.NET application doesn’t work as it should. For this reason, starting with version 1.10, VB Migration Partner supports the CloneArray6 helper method, which can perform both a shallow copy and a deep copy (if the second argument is True, also nested arrays are copied):

   Dim arrayOfArrays() As Variant
    ' …
    Dim dest() As Variant
    dest() = CloneArray6(arrayOfArrays(), True)

The CloneArray6 method is available both in VB6 and VB.NET, but the VB6 version is a do-nothing version and doesn't change the application behavior. The CloneArray6 method can correctly handle arrays of arrays of any nesting level, arrays of structures, and more in general arrays of any ICloneable object.

Another case when UpgradeWizard generates bugged VB.NET code is when assigning a structure that contain one or more arrays. Consider this VB.NET code, generated by Upgrade Wizard when converting an assignment between two structures:

Structure MyUDT
    Public arr() As Integer
End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt
    ' udt and udt2 point to different structures, but
    ' they share the same set of array elements
    ' …
End Sub

It is obvious that the assignment between structures doesn’t work as in the original VB6 code, because the udt and udt2 structures are pointing to the same array: changing one element in the array in one structure affects the array pointed to by the other structure.Unlike Upgrade Wizard and other conversion tools, VB Migration Partner works around this issue by rendering the assignment as follows:

Structure MyUDT
    Public arr() As Integer

    Function Clone() As MyUDT
        Dim copy As MyUDT = Me
        copy.arr = CloneArray6(Me.arr)
       Return copy
    End Function

End Structure

Sub Test()
    Dim udt As MyUDT
    Dim udt2 As MyUDT
    udt2 = udt.Clone()
    ' udt and udt2 are now completely disjoint objects, and share no arrays
    ' …
End Sub

You might bump into another elusive bug when invoking methods that store a reference to the array passed as an argument. The simplest example of this problem is when you store an array into multiple items of a Collection, as this VB6 code demonstrates:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' notice that here we reuse the same array…
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)
Debug.Print col(1)(0) & "," & col(2)(0)   ' => "Francesco,Giuseppe"

The Upgrade Wizard converts this code verbatim, but the resulting VB.NET code just doesn’t work as expected: both col(1) and col(2) elements actually point to the same array, therefore the text displayed in the Output window is not the one you hoped for:

Debug.WriteLine(col(1)(0) & "," & col(2)(0))  ' => "Giuseppe,Giuseppe"

A solution to this problem is to explicitly ReDim the array after each Add method, as follows:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(values)
' allocate a new memory block for array elements
ReDim values(1)
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(values)

However, this workaround adds unnecessary overhead to the VB6 code. Starting with VB Migration Partner version 1.10 you can avoid this overhead and have the most efficient behavior in both VB6 and VB.NET by using the CloneArray6 method:

Dim col As New Collection
Dim values(1) As String
values(0) = "Francesco": values(1) = "Balena"
col.Add(CloneArray6(values))
values(0) = "Giuseppe": values(1) = "Dimauro"
col.Add(CloneArray6(values))

The bottom line: pay close attention to how arrays are assigned and copied in your VB.NET project and carefully scrutinize the code that your VB6-to-VB.NET migration tool generates in these cases. Else you might be spending hours trying to track down these elusive bugs.



A better cure for a common VB6 misconception

clock October 3, 2008 02:48

In an article I wrote about two months ago I showed how you can fix a programming mistake that is quite common among VB6 developers, who tend to believe that the following declaration

     Dim x1, y1, x2, y2 As Integer

declares four integer variables, whereas it actually declares only one integer (y2) and three Variant variables. The solution I proposed required one PreProcess pragma for statements declaring two variables, another PreProcess pragma for statements declaring three variables, and so forth.

Yesterday Marco Giampetruzzi - from the VB Migration Partner Team - surprised me with a better solution that uses just one pragma to account for all Dim, Public, Private, and Static statements, regardless of the number of variables they declare:

    '## PreProcess "(?<=\b(Dim|Public|Private|Static)\b.*)
                    (?<!\b(Sub|Function|Property)\b.*)
                    (?<!As\s+)(?<var>\b\w+\b)(?=,.*\bAs\s+(?<type>\w+))",
                    "${var} As ${type}", True

Let me briefly explain how it works. The first regex tells VB Migration Partner to search an identifier and name it as "var" (?<var>\w+). This identifier must preceded on the same line by one of the declaration keywords (?<=\b(Dim|Public|Private|Static)\b.*). To prevent that parameters on a method declaration be mistakenly matched, we also check that the identifier not be preceded by a keyword used in method and property declarations (?<!\b(Sub|Function|Property)\b.*). Next, the regex ensures that the indentifier be immediately followed by a comma and (after some characters) by an As clause (=?,.*\bAs\b\s+(?<type>\w+)). Notice that above pragma doesn't support array declarations.

Very smart indeed... thanks Marco!

You can build similar PreProcess pragmas to account for similar cases. For example, the following pragma adds an As clause to parameters inside method declarations:

    '## PreProcess "(?<=\b(Sub|Function|Property)\b.*)
                    (?<!As\s+)(?<var>\b\w+\b)(?=,.*\bAs\s+(?<type>\w+))",
                    "${var} As ${type}", True

If you aren't familiar with regexes, drop a line to our tech support and we'll cook one for you!


Laughable business practices - Part 2

clock August 15, 2008 16:22

In case you haven’t read my last post, here’s a short recap. Some days ago Mauricio Rojas of Artinsoft publicly claimed that my knowledge in code translation is “disappointing”. Mauricio didn’t provide any fact to support his words, however he announced a new series of articles named “VB Migration not for the weak of mind” where he would prove his point.

Mauricio has finally published the first two articles of the series, one on the translation of On Error Resume Next statement to C# and the other on implementing late-binding in C#. Both articles are filled with mistakes, so a few hours ago I posted a comment for each of them.

Neither comment has appeared yet on Mauricio’s blog, but I hope it’s just a matter of time. If you publicly denigrate someone you should give him the opportunity to answer and illustrate his point of view. I am sure Mauricio doesn't ignore that transparency and intellectual honesty is the very essence of blogging.

In the meantime, I’ll expose my ideas and criticisms here. The text here is slightly different from my original (yet unpublished) comments to account for the different context.

Update (Aug 25): not only Mauricio never published my comments, he went as far as deleting one of the two articles. You can read the whole story and read an offline copy of his posts here.

------

My comment to “VB Migration (not for the weak of mind) Part 2

I read with interest Mauricio’s funny stories about human language dialects. I am sure that everybody agrees that it's easier to translate from Russian to Ucranian rather than to English or Spanish, even though both tasks can be successfully completed by a human that is skilled enough.

But the analogy is pointless in this specific context: Mauricio’s article was supposed to focus on *automatic* translation between *programming* languages, not manual translation between natural languages. It's like comparing apples to bicycles, for those who like analogies.

Mauricio announced he wanted to prove me wrong and that he knows how to automate the translation of *any* VB6 method to a piece of C# that is functionally equivalent. I don't see anything like that in this article. I only see some advice about what you should or shouldn't do in VB6 or C#, which is a completely different matter.

For example, we all know that On Error Resume Next can bring to hard-to-find bugs, nevertheless millions of VB6 projects use this statement and a conversion tool should account for this fact. The article was supposed to show how a VB6-to-C# translator can do it. We'll have to wait for a long time, I am afraid.

The real pearl in this article is where the author states that:

When you do a translation, you should try to make it as native as possible. So why will you keep using a COM function when there is an equivalent in .NET. So why not use System.IO.File.CopyFile("sourcefile1", "destfile1", true); instead?

I am sorry to inform Mauricio that he cannot replace the FileSystemObject.CopyFile method with a System.Io.File.CopyFile method for at least three reasons:

  1. the System.IO.File class doesn't even expose such a method Wink... or maybe he was thinking of the Copy method? (Tip: never trust your memory when writing a technical article...)
  2. the System.IO.File.Copy method doesn't accept wildcards in its first argument, whereas the FileSystemObject.CopyFile method does.
  3. the System.IO.File.Copy method requires a complete file name in its 2nd argument, whereas the FileSystemObject.CopyFile method also accepts a folder name.


If you buy a conversion tool converts calls to the FileSystemObject.CopyFile method into calls to the System.IO.File.Copy method, then you have to carefully check that the original VB6 code never uses wildcards or folder names. If the method is located inside a library or in a source file that is shared by hundreds different VB6 projects you have to scrutinize each and every project. In a real-world application this job alone can take hours.

We at Code Architects are aware of these subtle differences, which Mauricio apparently ignores. To spare our customers’ time and effort, our VB Migration Partner tool converts the CopyFile method into a call to a helper object that behave *exactly* like the original FileSystemObject class, except it's fully .NET and has no COM dependencies. If you use a conversion tool that takes a different approach we can only be sorry for you.

----

My comment to “Late-binding migration to C#

In this article Mauricio introduces the following VB6 code snippet to show how a converter can translate VB6 late-bound methods:

Sub ChangeValue(x As Object, newValue As Object)
    x.Value = newValue
    x.Refresh
End Sub

Sub Test()
    ChangeValue Me.Button1, False
End Sub

A VB6-to-C# converter can translate late-bound code in basically two ways: by using .NET reflection or by generating an overloaded method for each possible argument type. Mauricio shows the output of a code translator that takes the latter approach:

void ChangeValue(Button x, object newValue)
{
    x.PerformClick();
    x.Refresh();
}

void Test()
{
    ChangeValue(this.Button1, false);
}

Unfortunately for Mauricio, this is the poorest example he could ever think of. In fact, the only thing that it proves is that people should never write - or be allowed to write - technical articles if they are too lazy to double-check every single code snippet that they publish. In fact

  1. If he had tested the VB6 code he would have realized that it cannot run at all, because you can’t pass False to the newValue Object parameter. (Of course, that parameter must be declared as Variant, not Object.)
  2. The original VB6 code never triggers the button's Click event, because the Value property is assigned False. If he had tested the equivalent (!) C# code he would have realized that it behaves exactly in the opposite way, in that it always raises the event! A converter which blindly converts all assignments to the Button.Value property into a PerformClick method has a severe bug that should be fixed as soon as possible.

----

Regardless of this specific (and flawed) example, let me comment on the technique itself.

Using overloads of C# methods to simulate late-binding is surely feasible, but for completeness' sake it should be explained what happens when applying this technique to real-world apps instead of toy examples.

A) MAINTAINABILITY
If you have a method whose object argument can receive any of the 30 control types used in the project, then this technique generates as many as 30 methods. If the method takes 2, 3 or more object arguments, then you have to account for all possible combinations of argument types. In a real-world program you might end up with hundreds methods.

Maintaining this redundant code over the years is a nightmare. For example, what if you later want/need to change one statement and forget to propagate the changes to all other overloaded methods?

B) CODE SIZE
These additional methods significantly increase the memory footprint of the application and slow down execution because each method must be JIT-compiled separately. If a given overload is used sparingly, this overhead can offset the speed advantage of early-binding.

C) FUNCTIONAL EQUIVALENCE
If a late-bound object doesn't expose a given method you receive runtime error #438 both under VB6 and VB.NET. Expert VB developers often leverage this behavior and the On Error Resume Next statement to write code that adapts itself to different object types. During the conversion to C# you lose functional equivalence, which means more work for those developers who have to check that the converted code works like the original one.



A common VB6 misconception... and how to cure it

clock July 31, 2008 12:17

One of our users is facing the migration of a large VB6 application and found that the coders often declared local variables using this "concise" syntax:

    Dim x1, x2, y1, y2 As Integer

It is evident that the original intention was to declare four Integer variables. The truth is, this statement declares only y2 as an integer variable, whereas the type of x1, x2, and y1 variables is affected by Defxxx statements (e.g. DefInt or DefDbl). If the current file contains no Defxxx statement, then the type of these variable is Variant. This was a common misconception among less experienced VB6 developers or developers that grew up with C or C++.

Both the Upgrade Wizard and VB Migration Partner convert the first three Variant variables into VB.NET "As Object" variables, which is formally correct but isn't what the original developer meant. Our user asked whether there was anything he could do about it. Quite surprisingly, VB Migration Partner offers a neat and elengant solution to this issue.

In fact, if you are migrating a VB6 application written by developers who consistently applied this sloppy coding practice, you can help VB Migration Partner to restore the intended data type by means of one or more PreProcess pragmas. More precisely, you need one pragma to account for Dim statements with two variables, another pragma to account for Dim statements with three variables, and so forth:

'## PreProcess "\b(?<kw>Private|Public|Dim|Static)\b\s+(?<v1>\w+)\s*,
\s*(?<v2>\w+)\s+As\s+(?<type>\w+(\.\w+)?)",
"${kw} ${v1} As ${type}, ${v2} As ${type}", true

'## PreProcess "\b(?<kw>Private|Public|Dim|Static)\b\s+(?<v1>\w+)\s*,
\s*(?<v2>\w+)\s*,\s*(?<v3>\w+)\s+As\s+(?<type>\w+(\.\w+)?)",
"${kw} ${v1} As ${type}, ${v2} As ${type}, ${v3} As ${type}", true

'## PreProcess "\b(?<kw>Private|Public|Dim|Static)\b\s+(?<v1>\w+)\s*,
\s*(?<v2>\w+)\s*,\s*(?<v3>\w+)\s*,
\s*(?<v4>\w+)\s+As\s+(?<type>\w+(\.\w+)?)",
"${kw} ${v1} As ${type}, ${v2} As ${type}, ${v3} As ${type}, ${v4} As ${type}", true

'## PreProcess "\b(?<kw>Private|Public|Dim|Static)\b\s+(?<v1>\w+)\s*,
\s*(?<v2>\w+)\s*,\s*(?<v3>\w+)\s*,\s*(?<v4>\w+)\s*,
\s*(?<v5>\w+)\s+As\s+(?<type>\w+(\.\w+)?)",
"${kw} ${v1} As ${type}, ${v2} As ${type}, ${v3} As ${type},
 ${v4} As ${type}, ${v5} As ${type}", true

Notice that these pragmas account for variables and class fields declared with Dim, Static, Public, and Private keywords. You can easily create similar PreProcess to account for more than five variables in a single line.

When these pragmas are used, the above Dim statement is expanded into the following VB6 code immediately before the migration begins:
    Dim x1 As Integer, x2 As Integer, y1 As Integer, y2 As Integer
which, of course, is translated to VB.NET as:
    Dim x1 As Short, x2 As Short, y1 As Short, y2 As Short

Important Note: Keep in mind that these pragmas affect may mistakenly affect the type of variables that SHOULD remain Object variables. Always double check that the resulting VB.NET code works as intended.

Problem solved! 

Update (October 3, 2008): Marco Giampetruzzi (from the VB Migration Partner Team) found a simpler and better way to solve the same problem, as you can read here.



Sharp enough to convert from VB6 to C#

clock July 28, 2008 01:36

Every now and then I visit the competition's web site (as much they visit ours) and recently stumbled into this post, which discusses whether VB6ers should migrate to C# rather than VB.NET. The sentence that hit my attention was

By the way, we’ve seen a few people suggesting a double path approach for those who chose to migrate their VB6 applications to C#. This idea mostly comes from those who offer a solution that only converts to VB.NET, and they even say it’s irrational to think about jumping from VB6 to any .NET language other than VB.NET. Well, the immense set of differences between VB6 and C# or VB.NET are accurately resolved by <name of their conversion tool>.

I had a friendly discussion with a couple of the author’s colleagues some time ago on this topic, therefore I assume that the “unnamed competitor” is Code Architects and our VB Migration Partner tool.

The first part of the sentence is absolutely correct: VB Migration Partner tool doesn’t generate C# code and we don’t plan to add this feature in the foreseeable future.

The reason is simple: our tool can generate VB.NET programs that are 100% equivalent to the original VB6 code, either at the first attempt or after adding a few migration pragmas. It converts Gosubs, On Goto/Gosubs, As Any parameters, As New variables and arrays with true auto-instancing semantics, default member references even in late-bound mode, deterministic finalization for IDisposable objects, arrays with any LBound, all 60+ controls in VB6 toolbox, ADO/RDO/ADO databinding, graphic methods, user-defined system coordinates, OLE drag-and-drop events, and many other features that the Upgrade Wizard doesn’t support.

Generating C# code would be a step in the wrong direction for us, because we couldn’t support several of the above features and we would therefore deliver lower quality code. We aren’t interested in that.

UPDATED (June 3, 2011):  never say never!... two years after this post was written, Visual C# 2010 introduced many interesting new features that make the migration from VB6 less error-prone. The first one is a (limited) form of late-binding (sort of), based on dynamic variables; the second one is the support for optional parameters. As a matter of fact, we have changed our mind and are working at a very innovative VB6-to-C# converter that works around the residual C#-related migration issues and still generate maintainable and efficient code. Stay tuned!

The second part of the sentence isn't correct, though. We don't say that it is irrational to convert a VB6 application to C# rather than VB.NET. We only claim that doing it in one single step using an automated conversion tool isn't a great idea for many reasons. Explaining these reasons requires a lengthy and detailed article, thus I thought I’d better post it here than just replying to the original post.

Comparing languages is a fascinating topic and I'd like to thank the author of the original post for giving me the excuse to explore it in depth. Please notice that I am not making any assumption on our competitor's VB6-to-C# converter: I never used it and they don't publish any converted VB.NET or C# application, as we do in our Code Samples section. Therefore this article is just an overview of some obvious issues that you have when converting VB6 code directly to C#.

An important disclaimer:  Let me emphasize that I don’t mean to compare the power of VB6/VB.NET vs. C#. The only purpose of this article is to demonstrate that there are VB6 peculiarities that can’t be easily migrated to C# code without loosing either readability or functional equivalence, especially if you use an automated code converter. In the best case you end up bloated and unreadable C# code, in the worst case you get C# code that doesn’t behave exactly like the VB6/VB.NET code you start with.


COMPARING VB6, VB.NET AND C#

VB.NET and C# languages aren't perfectly equivalent, therefore if you convert directly from VB6 to C# you nearly always lose something. (I wrote seven books on VB6, VB.NET and C# for Microsoft Press, thus I know what I am saying...) The best examples of this non-equivalence are the On Error statements and late binding. For example, consider this simple VB6 method that copies 10 files using the FileSystemObject type library:

Sub CopyFiles(ByVal throwIfError As Boolean)
    If Not throwIfError Then On Error Resume Next
    Dim fso As New FileSystemObject
    fso.CopyFile "sourcefile1", "destfile1"
    fso.CopyFile "sourcefile2", "destfile2"
    fso.CopyFile "sourcefile3", "destfile3"
    ' seven more CopyFile method calls …
End Sub

VB.NET fully supports the On Error Resume Next statement, therefore this code translates to VB.NET as-is, except for the parenthesis around method arguments. Let’s see now the “equivalent” C# implementation:

void CopyFiles(bool throwIfError)
{
    Scripting.FileSystemObject fso = new Scripting.FileSystemObjectClass();
    try
    {
        fso.CopyFile("sourcefile1", "destfile1", true);
    }
    catch
    {
        if (throwIfError)
        {
            throw;
        }
    }
    try
    {
        fso.CopyFile("sourcefile1", "destfile1", true);
    }
    catch
    {
        if (throwIfError)
        {
            throw;
        }
    }
    try
    {
        fso.CopyFile("sourcefile1", "destfile1", true);
    }
    catch
    {
        if (throwIfError)
        {
            throw;
        }
    }
    // seven more try-catch blocks
}

We started with a VB6 method containing 12 executable statements and ended up with a C# method of 111 statements, nearly 10x bigger! You can’t always have concise C# code if your main goal is functional equivalence with the original VB6 code, sorry!

Also, notice that the C# code can’t omit the third (optional) argument in the FileSystemObject.CopyFile method, which makes the code even more verbose. Just imagine what happens when working with methods that have many optional parameters, such as those exposed by Microsoft Office type libraries! (Note that I purposely avoided to convert the FileCopy method into System.IO.File.Copy, because the focus here is on error handling, not file operations.) 

Don’t miss another important point: I translated the VB6 code to C# manually. An automatic translator tool might fail to deliver code that is truly equivalent and might, for example, bracket all the statements in a single try-catch block and add a warning about the different semantics and behavior. The text of such a warning may vary, but it would be a variation of the following sentence:  “My job ends here, it’s up to you ensuring that I didn’t mess things up.” Laughing

You might believe that the problem surfaces only with the On Error Resume Next statement, and that converting the On Error Goto <label> command can be easily translated to equivalent C# code. Alas, this isn't the case if you have multiple On Error Goto statements in one method, or if you use the Resume Next statement, or if you turn error handling on and off by alternating On Error Goto <labal> and On Error Goto 0 commands. Code of this sort can be a nightmare even for the bravest developer, and I doubt a VB6-to-C# convert can handle it correctly. Of course, you don't have any problem if you convert to VB.NET, because all these variants are fully supported by VB.NET.

By the way, if you are curious about how often you’ve used On Error statements it’s time to run our free VB6 Bulk Analyzer tool. You’d be surprised!

Let’s now tackle late binding. You can use late-binding in both VB6 (always) and in VB.NET (if Option Strict is Off), therefore translating the following VB6 code to VB.NET is a trivial task for both humans and automatic translators:

Sub IncrementValue(ByVal obj As Object, ByVal valueBeingAdded As Object)
    obj.Value = obj.Value + addedValue
    obj.Refresh
End Sub

C# doesn’t support late binding, thus there is no easy way to render this code into C# short of using reflection. This is how I’d translate it, after adding a variable and a few remarks to make it more readable:

void IncrementValue(object obj, object valueBeingAdded)
{
    // read Value property
    object value = obj.GetType().InvokeMember("Value",
       
System.Reflection.BindingFlags.GetProperty, null, obj, null);
    // increment it (assumes it's a double)
    object newValue = Convert.ToDouble(value) + Convert.ToDouble(valueBeingAdded);
    // assign back to Value property
    obj.GetType().InvokeMember("Value",
        System.Reflection.BindingFlags.SetProperty, null, obj,
        new object[] { newValue });
    // call the Refresh method
    obj.GetType().InvokeMember("Refresh",
        System.Reflection.BindingFlags.InvokeMethod, null, obj, null);
}

At least, this C# code is perfectly equivalent to the original VB6 code, right?

Wrong. In fact, the VB6 "+" operator also works with strings, therefore I can’t blindly assume that the Value property is numeric. As a matter of fact, when working with late binding you can’t assume anything about the objects you work with. Here’s the new version that works with strings and numeric properties:

void IncrementValue(object obj, object valueBeingAdded)
{
    // read Value property
    object value = obj.GetType().InvokeMember("Value",
        System.Reflection.BindingFlags.GetProperty, null, obj, null);
    // either increment it (assumes it's a double) or append the string
    object newValue;
    if (value is string)
    {
        newValue = Convert.ToString(value) +
            Convert.ToString(valueBeingAdded);
    }
    else
    {
        newValue = Convert.ToDouble(value) +
            Convert.ToDouble(valueBeingAdded);
    }
    // assign back to Value property
    obj.GetType().InvokeMember("Value",
        System.Reflection.BindingFlags.SetProperty, null, obj,
        new object[] { newValue });
    // call the Refresh method
    obj.GetType().InvokeMember("Refresh",
        System.Reflection.BindingFlags.InvokeMethod, null, obj, null);
}

Do we now have a piece of C# that is perfectly equivalent to the original VB6 code? The answer is again no, because there are many other details that I should account for, including:

a) I assumed that Value is a property – the C# code fails if Value is a class field, unlike VB6 and VB.NET
b) I assumed that Refresh has no parameters – the VB6 and VB.NET code works correctly even if Refresh has one or more optional arguments, but the C# code fails in this case
c) I didn’t care about error codes: if an overflow occurs or if the object doesn’t expose the Value or Refresh members, I should ensure that the caller code sees the same error code that it would see under VB6 or VB.NET.

In the (very unlikely) hypothesis that you never, ever used On Error Resume Next statements and late binding calls, let’s see an example that uses two nested loops and leverages the fact that the Exit statement allows you to specify whether you want to leave the Do loop or the For loop. (I could have made the example more complex by adding a While…Wend loop.)

Sub FindElement(arr() As Integer, ByVal value As Integer)
    Dim row As Integer, col As Integer
    ' outer loop
    For row = 0 To UBound(arr)
        ' inner loop
        col = 0
        Do While col <= UBound(arr, 2)
            If arr(row, col) >= value Then Exit For
            col = col + 1
        Loop
    Next
    Debug.Print "FOUND AT ROW=" & row & ", COL=" & col
    ' additional code here...
End Sub

This code converts to VB.NET without any problem, but requires some extra work to convert to C#:

void FindElement(int[,] arr, int value)
{
    int row = 0;
    int col = 0;
    // outer loop
    row = 0;
    while (row < arr.GetUpperBound(0))
    {
        // inner loop
        col = 0;
        while (col < arr.GetUpperBound(1))
        {
            if (arr[row, col] >= value)
            {
                goto ExitOuterLoop;
            }
            col++;
        }
        row++;
    }
ExitOuterLoop:
    System.Diagnostics.Debug.WriteLine("FOUND AT ROW=" + row.ToString()
        + ", COL=" + col.ToString()); 
    // additional code here...
}

Aarghh! The dreaded GoTo! I like concise and readable code, but I surely don’t want my code to be infested by Goto statements, therefore I rewrote the code the way most C# developers would do:

void FindElement(int[,] arr, int value)
{
    int row = 0;
    int col = 0;
    bool found = false;
    // outer loop
    row = 0;
    while (row < arr.GetUpperBound(0))
    {
        // inner loop
        col = 0;
        while (col < arr.GetUpperBound(1))
        {
            if (arr[row, col] >= value)
            {
                found = true;
                break;
            }
            col++;
        }
        If ( found )
        {
            break;
        }
        row++;
    }
    System.Diagnostics.Debug.WriteLine("FOUND AT ROW=" + row.ToString()
        + ", COL=" + col.ToString()); 
    // additional code here...
}

Not counting remarks, we started from a 11-line VB6 method and ended up with a C# method containing 26 lines, a 236% “improvement.” 

To recap: I picked three simple VB6 examples that use common features – error handling, late-binding, and nested loops- and in all cases I ended up with more verbose and less readable C# code, in spite of all my attempts to make the C# code look like “native” C#. Never forget that the situation can only get worse if the translation is performed by means of a software tool.

The bottom line: converting from VB6 to “equivalent” C# often delivers ugly, verbose, unmaintainable code or code that isn’t perfectly equivalent to the original VB6 code.

In some cases you can come up with C# code that approximates the original VB6 code’s behavior, but that’s a completely different story. When migrating an application with 1 million code lines, I don’t want to worry about the thousands of occurrences of On Error statements or late-bound calls. Not to mention other important differences such as arrays with any LBound, auto-instancing (As New) variables and arrays thereof, fixed-length strings, and so forth.


USING VB6-TO-C# CONVERTERS (aka ONE DOUBLE-JUMP)

Once again, it is important to bear in mind that in all previous examples I made a manual translation from VB6 to C# and came up with the most efficient and less verbose C# that is equivalent to the original VB6 code. Sure, you can drop a few curly braces and merge a couple of lines into a single statement, but I would be surprised if it were possible to significantly improve the C# code seen above.

Actually, I would be very surprised if a VB6 conversion tool could be able to generate the kind of C# code that a human developer would write. My guess is that in the best cases you’d see many calls to support methods – e.g. to work around late-binding deficiencies or simulate methods in the VB library – and a lot of code that just doesn’t “look like” C#.

In the worst (and more frequent) cases, you’d obtain C# code that isn’t perfectly equivalent to the original VB6 code and that forces you to carefully test each and every method that uses error handling, late binding, auto-instancing (As New) variables, arrays, file operations... in other words, virtual every single statement in your application.

If you are switching to C# with the purpose of having well-structured and readable code that performs at least as well as the original VB6 code, you are not going to achieve this goal by means of an automatic VB6-to-C# converter.

USING VB.NET-TO-C# CONVERTERS (aka TWO SINGLE-JUMPS)

At times you really need to convert from VB6 to C#. My recommendation is that you should try hard to convince your (internal or external) customer that translating to C# gives you more headaches than benefits. …but for the sake of discussion, let’s say that you have no choice and that C# is the target language.

Even in such case, a double-jump from VB6 to C# using an automatic converter isn’t the best option, both for technical and business reasons. It is much more rational jumping from VB6 to VB.NET and then using a VB.NET-to-C# converter to get the final result.

I built my opinion on many facts:

a) translating to VB.NET is simpler and faster – either using the free Upgrade Wizard that comes with Visual Studio or a commercial tool such as our VB Migration Partner. You can have a prototype sooner and you can bring a first version to the market in a fraction of the time it takes to just have a C# project that compiles without errors.

b) if you have a group of VB6 developers, odds are that they are already familiar with VB.NET. If they aren’t, they can learn VB.NET quickly because the two languages are similar. By comparison, becoming proficient with C# is a longer and more expensive process. By converting to VB.NET first you leverage the experience of your developers and reduce overall migration costs. (Don’t forget that C# developers usually ask for more money.)

c) There are several VB.NET-to-C# converters on the market, including the exceptional Instant C# (by Tangigle Software Solutions) and C-Sharpener for VB by Elegance Technologies. They deliver the best C# code that a software can automatically do. The companies behind these tools have just one mission: translating from VB.NET to C# in the best way, and in fact they are often updated and produce code that is more and more optimized and each new release. I use one of them and I am entirely satisfied.

d) from the business perspective, it is more convenient to purchase two separate tools, a VB6-to-VB.NET converter and a VB.NET-to-C# converter, because you can use the latter in other circumstances. If you have a tool that converts from VB6 to C# in one step, you can't use it to convert the VB.NET code that you happen to have already (for example ASP.NET pages) or that had to be translated manually from VB6 because of some deficiency in the migration tool.

e) speaking of convenience, the Standard Edition of both Instant-C# and C-Sharpener for VB cost less than 200$, which is a real bargain considering how useful they can be in many situations. This price is negligible if compared with the total cost of the migration of a real-world VB6 business application.

One might object that using a single integrated tool is better than using two distinct converters. But consider this: our VB Migration Partner converts at about 18,000 lines per minute; one of these converters splits out code at about 15,000 lines per minute; both of them come with a batch version, therefore you can easily automate your VB6-to-C# conversions. By combining them you can migrate a 100,000 line VB6 project to C# in about 12 minutes, 2x or 3x faster than what it takes to the Upgrade Wizard to perform just the first jump to VB.NET.



Solve migration problems with the Support class

clock July 25, 2008 09:07

The Microsoft.VisualBasic.Compatibility.dll assembly (part of the .NET Framework) contains many classes that are used by the Upgrade Wizard conversion tool included in Visual Studio. Among these classes you can find the "controlarray" equivalent for each control and many other classes to support special VB6 features (e.g. WebClasses).

In most cases you can forget about the DLL, if you migrate your code using our VB Migration Partner software. For example, VB Migration Partner doesn't use a different controlarray class for each different control. Instead, it leverages generics and has just one class - namely VB6ControlArray(Of T) - that works as a control array for each possible control. The clear benefit is that VB Migration Partner seeminglessly support arrays of *any* control, including custom UserControls and ActiveX controls. (Sorry, Upgrade Wizard...)




However, the Microsoft.VisualBasic.Compatibility assembly contains at least one class that deserves your attention: the Support module. This module exposes several helper methods that can solve many conversion issues. For example, the following methods are real lifesavers when converting between OLE types and .NET types:

Function CursorToIPicture(cur As cursor) As Object
Returns an OLE IPicture object that corresponds to the specified System.Windows.Forms.Cursor.

Function FontToIFont(font As Font) As Object
Converts a System.Drawing.Font to a Visual Basic 6.0 stdFont object.

Function IconToIPicture(ico As Icon) As Object
Gets an OLE IPicture object for a given System.Drawing.Icon.

Function IFontToFont(objFnt As Object) As Font
Converts a Visual Basic 6.0 stdFont object to a System.Drawing.Font.

Function ImageToIPicture(img As Image) As Object
Gets an OLE IPicture object for a given System.Drawing.Image.

Function ImageToIPictureDisp(img As Image) As Object
Gets an OLE IPictureDisp object for a given System.Drawing.Image.

Function IPictureDispToImage(pict As Object) As Image
Gets a System.Drawing.Image for a given OLE IPictureDisp object.

Function IPictureToImage(pict As Object) As Image
Gets a System.Drawing.Image for a given OLE IPicture object.

Another group of methods can greatly simplify coordinate conversions:

Function FromPixelsUserHeight(Height As Double, ScaleHeight As Double, OriginalHeightInPixels As Integer) As Double
Converts a pixel measurement to a Visual Basic 6.0 ScaleHeight measurement.

Function FromPixelsUserWidth(Width As Double, ScaleWidth As Double, OriginalWidthInPixels As Integer) As Double
Converts a pixel measurement to a Visual Basic 6.0 ScaleWidth measurement.

Function FromPixelsUserX(X As Double, ScaleLeft As Double, ScaleWidth As Double, OriginalWidthInPixels As Integer) As Double
Converts a pixel measurement to a Visual Basic 6.0 ScaleLeft measurement.

Function FromPixelsUserY(Y As Double, ScaleTop As Double, ScaleHeight As Double, OriginalHeightInPixels As Integer) As Double
Converts a pixel measurement to a Visual Basic 6.0 ScaleTop measurement.

Function FromPixelsX(X As Double, ToScale As ScaleMode) As Double
Converts a pixel measurement to a Visual Basic 6.0 measurement for a given Microsoft.VisualBasic.Compatibility.VB6.ScaleMode.

Function FromPixelsY(Y As Double, ToScale As ScaleMode) As Double
Converts a pixel measurement to a Visual Basic 6.0 measurement for a given Microsoft.VisualBasic.Compatibility.VB6.ScaleMode.

Function PixelsToTwipsX(X As Double) As Double
Converts an X coordinate from pixels to twips.

Function PixelsToTwipsY(Y As Double) As Double
Converts a Y coordinate from pixels to twips.

Function ToPixelsUserHeight(Height As Double, ScaleHeight As Double, OriginalHeightInPixels As Integer) As Double
Converts a Visual Basic 6.0 ScaleHeight measurement to a pixel measurement.

Function ToPixelsUserWidth(Width As Double, ScaleWidth As Double, OriginalWidthInPixels As Integer) As Double
Converts a Visual Basic 6.0 ScaleWidth measurement to a pixel measurement.

Function ToPixelsUserX(X As Double, ScaleLeft As Double, ScaleWidth As Double, OriginalWidthInPixels As Integer) As Double
Converts a Visual Basic 6.0 ScaleLeft measurement to a pixel measurement.

Function ToPixelsUserY(Y As Double, ScaleTop As Double, ScaleHeight As Double, OriginalHeightInPixels As Integer) As Double
Converts a Visual Basic 6.0 ScaleTop measurement to a pixel measurement.

Function ToPixelsX(X As Double, FromScale As ScaleMode) As Double
Converts a Visual Basic 6.0 measurement to a pixel measurement for a given Microsoft.VisualBasic.Compatibility.VB6.ScaleMode.

Function ToPixelsY(Y As Double, FromScale As ScaleMode) As Double
Converts a Visual Basic 6.0 measurement to a pixel measurement for a given Microsoft.VisualBasic.Compatibility.VB6.ScaleMode.

Function TwipsPerPixelX() As Single
Gets a value that is used to convert twips to pixels based on screen settings.

Function TwipsPerPixelY() As Single
Gets a value that is used to convert twips to pixels based on screen settings.

Function TwipsToPixelsX(X As Double) As Double
Converts an X coordinate from twips to pixels.

Function TwipsToPixelsY(Y As Double) As Double
Converts a Y coordinate from twips to pixels.

Finally, there are a few useful methods that don't fall in a specific category:

Function GetActiveControl() As Control
Gets the control that currently has focus.

Function GetEXEName() As String
Gets the name of the executable file (.exe) for the current application.

Function GetHInstance() As System.IntPtr
Gets the instance handle (HINSTANCE) for the current application.

Function GetPath() As String
Gets the current path for the application.

Function TabLayout(ParamArray Args() As Object) As String
Formats strings to simulate the Visual Basic 6.0 Debug.Print functionality.

Sub WhatsThisMode(ByVal Form As Form)
Displays pop-up Help for a form upgraded from Visual Basic 6.0.

Sub ZOrder(Control As Control, Position As Integer)
Converts the Visual Basic ZOrder method for use in Visual Basic .NET.



Neat tricks for smooth migration of calls to Windows API methods

clock June 13, 2008 03:13

VB Migration Partner does a superb in dealing with Windows API calls. Here's a summary of the features that it supports and that are out of reach for the Upgrade Wizard included in Visual Studio 2005/2008:

  • converts As Any parameters, by creating all the necessary overloads
  • deals correctly with API methods that take a callback address (e.g. EnumWindows, EnumFonts)
  • provides recommendation about the .NET object/method that can effectively replace the API method; we cover 300+ different API calls
  • in a few cases, it automatically replace API calls with calls to the corresponding .NET object/method
  • ensures that string immutability doesn't prevent the VB.NET code from working correctly (see this article)
  • generates the correct MarshalAs attributes for elements in Type (Structure) blocks
  • correctly translates fixed-length strings inside Type blocks, so that they work correctly when passed to the Windows API method
  • automatically initializes static arrays inside Type blocks, so that you don't get unexpected crashes when invoking an API method that expects to find a buffer there
  • creates a wrapper method that ensures that orphaned delegates don't cause an unexpected runtime exception, an advanced programming technique discussed in this KB article
  • includes the VB6WindowsSubclasser class that helps you correctly migrate subclassing-based techniques (as explained here)


In spite of all these innovations, there are cases when you still need to manually edit either the original VB6 code or the converted VB.NET. This happens, for example, if the original code uses the VarPtr, StrPtr, or ObjPtr functions to pass memory pointers to an external API method. These three functions aren't supported under VB.NET and there is no simple way to simulate them.

The good news is that in the vast majority of cases you don't need to deal with memory pointers under .NET, because the .NET Framework offer a valid "pure" alternative to the API method in question. In this article I'll illustrate how you can take advantage of VB Migration Partner features to reduce manual edits to the very minimum and take advantage of the convert-test-fix methodology.

For simplicity's sake, let's focus on one of the simplest API methods, the GetSystemDirectory Windows API. Here's a piece of VB6 code that displays the system directory path:

    ' Main.Bas module
    Public Declare Function GetSystemDirectory Lib "kernel32.dll" Alias "GetSystemDirectoryA" _
        (ByVal lpBuffer As String, ByVal nSize As Long) As Long

    Sub Main()
        Dim buffer As String, length As Long, windir As String
        buffer = Space(256)
        length = GetSystemDirectory(buffer, Len(buffer))
        winDir = Left(buffer, length)
        MsgBox winDir
    End Sub

The first step is in refactoring this code so that you make all the Declares private and move them to another BAS module, that exposes them by means of standard VB6 methods. (If you usually write tidy and maintainable VB6 code, odds are that you have already taken this step.)

    ' This is the APIHelpers.Bas file
    Private Declare Function GetSystemDirectory Lib "kernel32.dll" Alias "GetSystemDirectoryA" _
        (ByVal lpBuffer As String, ByVal nSize As Long) As Long

    ' returns the Windows directory
    Public Function SystemDirectory() As String
        Dim buffer As String, length As Long
        buffer = Space(256)
        length = GetSystemDirectory(buffer, Len(buffer))
        SystemDirectory = Left(buffer, length)
    End Function

The code that actually displays or otherwise uses the Windows directory path is now simpler. Notice that we explicitly include the module name (APIHelpers) in the method call. This tip reduces the odds that another method with same name exists elsewhere in the project, but the technique explained later works even if you don't include such a name:

    ' the Main.Bas module
    Sub Main()
        Dim windir As String
        winDir = APIHelpers.SystemDirectory
        MsgBox winDir
    End Sub

At this point you have a VB6 project that works exactly like the original one, but it is better organized and structured, with all Declares statements gathered in one single module. Let's see how to migrate this code to VB.NET and get rid of all dependencies from non-NET code.

First and foremost, we prepare a VB.NET module that exposes the same methods as the original APIHelpers.bas but doesn't use any Declare statement. Here's how we can render the SystemDirectory function using native .NET calls:

    ' This is the APIHelpers.vb file (VB.NET)
    Module APIHelpers
        Public Function SystemDirectory() As String
            Return Environment.SystemDirectory
        End Function
    End Module

Next, we use an ExcludeCurrentFile pragma to exclude the APIHelpers.bas VB6 module from migration process and we use an AddSourceFile pragma to add the APIHelpers.vb VB.NET file to the converted Visual Studio project. The neat result is that the code in Main now calls the .NET version of the method, which doesn't use any unmanaged calls:

    ' This is the APIHelpers.Bas file
    '## ExcludeCurrentFile
    '## AddSourceFile "c:\vbnet\modules\apihelpers.vb"

    Private Declare Function GetSystemDirectory Lib "kernel32.dll" Alias "GetSystemDirectoryA" _
        (ByVal lpBuffer As String, ByVal nSize As Long) As Long
    ' ... remainder of module as before...

This solution works great, but we can improve it. In fact, a (minor) problem is that the resulting VB.NET code still uses wrapper methods and doesn't look like the "native" .NET code that an experienced VB.NET developer would write. Fear not, because all you need is a project-level PostProcess pragma:

    ' This is the APIHelpers.Bas file
    '## ExcludeCurrentFile
    '## project:PostProcess "(APIHelpers\.)?SystemDirectory", "Environment.SystemDirectory"

Notice that I dropped the AddSourceFile pragma because you don't need the wrapper method any longer (at least in this simplified example). Using similar techniques you can provide a .NET equivalent for most methods that require API calls under VB6, including methods that take arguments.

One of the long-terms goals we have in Code Architects is to apply these concepts on a larger scale to create VB6 helper modules and their corresponding VB.NET versions, to help all VB6 developers to easily migrate their API-intensive applications. Just stay tuned, as usual!



VB6 Bulk Analyzer, a command-line tool to explore multiple VB projects

clock June 11, 2008 10:26

We routinely spend a lot of time at our customers' site, to help them in their migration needs. If you haven't written an application's source code, understanding what the migration challenges are is a difficult and time-consuming process. It gets quickly much worse if the application is scattered over dozens or hundreds VB6 projects, as is often the case with N-tier (DNA) Visual Basic applications. In such cases, even getting a broad idea of how large the application is, how many forms it has, which controls it uses, and which technologies it relies on is a nightmare. On top of that, if the application is really huge, few people in the company (if any) have a 360° view of the entire application.

For this and other reasons, this week I decided to lock my office door and build the VB6 Bulk Analyzer tool. It's a simple command-line utility that gathers a lot of information about all the VB6 projects and source files inside a specified directory tree and then creates a concise but quite thorough report. You can download the beta build here.

Using this tool is quite easy: just open a command window, move to the root of the directory tree that contains all the VB6 source files that you want to parse, and run the utility. I will show the name of the files being processed, and then a summary of all the code it has parsed. If you parse more than a few dozens files, odds are that the relevant information will scroll away from the window, but you also have a permanent copy in the file named VBAnalyser_Report.txt, in the current directory. The tool supports a few options too, such as /quiet for omitting file names, /out to select a different report file, and /help to display a short explanation. You can also specify multiple folder names, in case your source code is scattered in directories that don't have a common root, as in this example:
       VB6ANALYZER c:\firstapp c:\secondapp  /out:c:\report.txt

The report file contains much useful info, including:

  • number of projects and source files, grouped by type
  • statistics about the source code (code lines, empty and remark lines, etc.), number of methods, properties, etc.
  • statistics about ActiveX classes (grouped by their Instancing property, number of MTS classes, etc.)
  • all the type libraries used in all projects, with how many times the typelib is referenced
  • names of all used controls and components, with a count beside each control
  • list of all the Declare definition, with number of occurrences of each declare
  • list of problematic keywords (GoSub, On...Goto/Gosub, VarPtr, ObjPtr, StrPtr, etc.), with number of times the keyword is used
  • list of problematic data types (Variant, fixed-length strings, etc., with number of times the keyword is used
  • list of problematic control properties, methods, events (e.g. members related to classic Drag-and-drop or DDE)
  • list of problematic constants (e.g. adOpenDynamic and adOpenKeyset means that a piece of ADO code can't be easily ported to ADO.NET)
  • list of COM classes that are instantiated via CreateObjects
  • list of OLEDB data providers explicitly mentioned in a connection string

VB6Analyzer is quite precise, especially if you consider that it doesn't really interpret the VB6 source code. It uses plain regular expressons, yet it doesn't suffer from "false matches" (eg. a keyword that appears in a remark or in a quoted string). Regular expressions make it very fast: on a 3GHz system it analyzes over 600K characters per second, or nearly 9,000 lines per second. 

Please keep in mind that this build is just a beta version which I rolled out in a couple days. Please write me if you find any problem or inconsistent result.

PS: if you are interested in our VB Migration Partner or our migration services, just run VB6 Bulk Analyzer on your project and then send its report from the Contact Us page.