FRANCESCO BALENA ON VB MIGRATION

 

A .NET library to access Windows Vista and Windows 7 features

clock September 16, 2009 15:58

One of the reasons to migrate your code to .NET is create modern user interface that take advantage of the power of newer versions of Windows. On the other hand, the .NET Framework has been designed to work in the same way over a wide range of Windows versions, including versions that are about ten years old, and therefore it doesn't provide access to the most intriguing features of Vista and Windows 7.

The solution comes in a new library from Microsoft, named Windows API Code Pack for .NET Framework. Version 1.0 of this great tool has been just released, and already implements an impressive range of Windows features:

  • Windows 7 Taskbar Jump Lists, Icon Overlay, Progress Bar, Tabbed Thumbnails, and Thumbnail Toolbars.
  • Windows 7 Libraries, Known Folders, non-file system containers.
  • Windows Shell Search API support, a hierarchy of Shell Namespace entities, and Drag and Drop functionality for Shell Objects.
  • Explorer Browser Control.
  • Shell property system.
  • Windows Vista and Windows 7 Common File Dialogs, including custom controls.
  • Windows Vista and Windows 7 Task Dialogs.
  • Direct3D 11.0, Direct3D 10.1/10.0, DXGI 1.0/1.1, Direct2D 1.0, DirectWrite, Windows Imaging Component (WIC) APIs. (DirectWrite and WIC have partial support)
  • Sensor Platform APIs
  • Extended Linguistic Services APIs
  • Power Management APIs
  • Application Restart and Recovery APIs
  • Network List Manager APIs
  • Command Link control and System defined Shell icons.
  • Shell Search API support.
  • Drag and Drop functionality for Shell objects.
  • Support for Direct3D and Direct2D interoperability.
  • Support for Typography and Font enumeration DirectWrite APIs. 

The library comes with an extensive help file and all code samples are available in both VB.NET and C#. Last but not the least, the entire source code is provided.



Pay attention to orphaned ADODB objects

clock September 1, 2009 19:26

Don't take me wrong: COM Interop is a great piece of technology, and Microsoft developers made wonders with it. If you consider how different the COM and .NET worlds are, it's a miracle that they can communicate with each other in such a smooth way.

Actually, COM Interop is such a magic that it's easy to forget that all the COM objects that we manipulate from .NET aren't the "real" COM objects. Instead, they are .NET wrappers that redirect all calls to the actual COM object by means of COM Interop. This fact has many implications. Consider for example this VB6 code:

Function GetRecordCount(ByVal cn As ADODB.Connection, ByVal sql As String) As Integer
   Dim rs As New ADODB.Recordset
   rs.Open sql, cn
   GetRecordCount = rs.RecordCount
End Function

As you see, the Recordset object isn't closed explicitly, but it isn't a problem in VB6 because all COM objects are automatically closed when they are set to Nothing or go out of scope. However, if you translate this code to VB.NET you are in trouble, because nothing happens when the method exits. In other words, you'll have an open recordset hanging somewhere in memory and cause unexplainable errors later in the application. For example, you'll get an error when you'll later try to open another recordset on the same connection.

We know that some customers had to work around this issue by enabling the MARS option with SQL Server, which enables multiple active recordsets on any given connection. This might be a viable solution if you are working with SQL Server 2005 or 2008, but VB Migration Partner offers a better way to handle this case: use the AutoDispose pragmas.

In fact, when this pragma is used, all disposable objects are correctly disposed of when they go out of scope. In this case, VB Migration Partner emits the following code:

Function GetRecordCount(ByVal cn As ADODB.Connection, ByVal sql As String) As Integer
   Dim rs As New ADODB.Recordset
   Try
      rs.Open(sql, cn)
      GetRecordCount = rs.RecordCount
   Finally
      SetNothing6(rs)
   End Try
End Function

where the SetNothing6 helper method takes care of orderly invoking the Close or Dispose method, if necessary.



Can your VB6 conversion tool handle null propagation?

clock May 21, 2009 03:13

If you have massively used Variants in your VB6 apps, the migration to .NET might be a nightmare. The best that the Upgrade Wizard and other tools can do is translating each “As Variant” into “As Object”, on the assumption that the Object type can do everything that a Variant does.

You shouldn’t be surprised to learn that this is one of the false migration myths, or is a gross oversimplification of the truth, to say the least. In fact, the .NET Object type lacks what arguably is the most useful feature of Variants: support for Null values and Null propagation in expressions.

Under VB6 a Variant variable can hold the special Null value and – even more important – all the string and math expression that involve a Null operand also deliver a Null Variant value. Virtually all database-intensive applications deal with Null values and have used null propagation to an extent. Now, let’s consider this VB6 code:

' rs is an ADODB.Recordset
Dim realPrice As Variant
realPrice = rs("Price")
realPrice = realPrice * (100 – rs("Discount")) / 100
If Not IsNull(realPrice) Then DisplayPrice(realPrice)

Under VB6, if either the Price or the Discount field is Null, the realPrice variable is assigned Null and the DisplayPrice method isn’t invoked. Now, consider the “canonical” VB.NET version of the above code:

Dim realPrice As Object
realPrice = rs.Fields("Price").Value
realPrice = realPrice * (100 – rs.Fields("Discount").Value) / 100
If Not IsNull(realPrice) Then DisplayPrice(realPrice)

Under .NET a Null database field returns a DBNull object – more precisely, the DBNull.Value element. The problem is, no math or string operation is defined for the DBNull type, something I consider one of the most serious mistakes Microsoft made in this area. As a result, the third statement throws an exception if either field is Null. So long, functional equivalence! Welcome, migration head-aches!

Null propagation is a serious problem not to be underestimated. One of our customers has found that, on the average, one out of 40 statements uses Variant expression and relies on null propagation. To put things in the right perspective, in a middle-sized real-world application you can expect to manually fix several thousand statements that use null propagation. What’s worse, there is no simple way to work around Null values in VB.NET, short of splitting all statements in portion and testing each and every subexpression for the DBNull.Value value.

If you use VB Migration Partner, you can solve the above problem in the most elegant and painless way. Just use the following project-level pragmas:

 '## project:NullSupport
 '## project:ChangeType Variant, VB6Variant

The NullSupport pragma tells VB Migration Partner to map some VB library functions – such as Left, Mid, Right, and a few others – to special methods that are defined in VB Migration Partner’s support library and that, not surprisingly, correctly return a Null value if their argument is Null (as they do under VB6).

The ChangeType pragma converts all Variant members – variable, fields, properties, methods, etc. – as the special VB6Variant type (also defined in VB Migration Partner’s support library) which redefines all the math, comparison, string, and logical operators to support null values.

Yes, this is all you need to add null propagation support to your VB.NET code! This feature alone can save you literally weeks in a real-world migration project. And don’t forget that VB Migration Partner is the only VB6 conversion software that supports Null values and Null propagation easily and automatically.

When you hear other vendors claiming that “our tool uses advanced artificial-intelligence-based techniques to ensure functional equivalence with the original VB6 code”, just ask them how it deals with Null values. Smile



Accomplishing the impossible: VARPTR in VB.NET!

clock March 15, 2009 20:39

VB6 supports three functions – VarPtr, StrPtr, and ObjPtr – that were never officially supported by Microsoft, yet they have been extensively used by many expert VB6 developers. In fact, you badly need these functions when calling some especially complex Windows API functions. Of these functions, the most useful one is VarPtr, which returns the address of the memory location where the value of a variable is stored.

Neither the Upgrade Wizard nor any other VB6 migration tool supports the VarPtr keyword, thus I decided that our VB Migration Partner *had* to correctly convert it. Thanks to Google, I found out that the problem had been discussed at length in many forums, but no definitive solution has ever been found yet. You can write an implementation in unsafe C#, or an unmanaged DLL written in C or Delphi, but these solution would force us to distribute a separate DLL with VB.NET apps converted by VB Migration Partner, which I’d rather not to.

It took a while and a lot of thinking, but in the end I figure out a way to solve the problem. Yes, it is possible to write a VarPtr function in plain VB.NET, with only the help of a method exposed by the Windows API. Actually, you need just a few lines of code:

' -----------------------------------------------------------
' VARPTR implementation in VB.NET
' Part of VB Migration Partner’s support library
'
' Copyright © 2009, Francesco Balena & Code Architects
' -----------------------------------------------------------

Module VarPtrSupport
   ' a delegate that can point to the VarPtrCallback method
   Private Delegate Function VarPtrCallbackDelegate( _
     
ByVal address As Integer, ByVal unused1 As Integer, _
      ByVal
unused2 As Integer, ByVal unused3 As IntegerAs Integer


   ' two aliases for the CallWindowProcA Windows API method
   ' notice that 2nd argument is passed by-reference
   Private Declare Function CallWindowProc Lib "user32" _
      Alias
"CallWindowProcA" _
      (ByVal wndProc As VarPtrCallbackDelegate, ByRef var As Short, _
      ByVal unused1 As Integer, ByVal unused2 As Integer,  _
     
ByVal unused3 As Integer) As Integer

   Private Declare Function CallWindowProc Lib "user32" _
      Alias "CallWindowProcA" _
      (ByVal wndProc As VarPtrCallbackDelegate, ByRef var As Integer, _
      ByVal unused1 As Integer, ByVal unused2 As Integer, _
     
ByVal unused3 As Integer) As Integer
   ' ...add more overload to support other data types...

   ' the method that is indirectly executed when calling CallVarPtrSupport
   ' notice that 1st argument is declared by-value (this is the
   ' argument that receives the 2nd value passed to CallVarPtrSupport)

   Private Function VarPtrCallback(ByVal address As Integer, _
        
ByVal unused1 As Integer, ByVal unused2 As Integer, _
         ByVal unused3 As Integer) As Integer
      Return address
   End Function


  
' two overloads of VarPtr
   Public Function VarPtr(ByRef var As Short) As Integer
      Return CallWindowProc(AddressOf VarPtrCallback, var, 0, 0, 0)
   End Function

   Public Function VarPtr(ByRef var As Integer) As Integer
      Return CallWindowProc(AddressOf VarPtrCallback, var, 0, 0, 0)
   End Function

   ' ...add more overload to support other data types...
End Module

To understand how the trick works, let’s see what happens when you call the VarPtr method. The only line of code in this method invokes one of the overloads of CallWindowProc method. The CallWindowProc method takes five arguments, the first one of which is a delegate that must point to a method that takes four 32-bit values. CallWindowProc invokes the method pointed to by the delegate and passed the other four values to such a method.

The key point in this mechanism is that each CallWindowProc overload takes a value by-reference in its second argument – a Short and an Integer, respectively. This means that the CallWindowProc method (buried inside the User32.dll) receives the address of the Short or Integer variable. This address is a 32-bit integer and is passed verbatim to the VarPtrCallback method. This method in turn receives a 32-bit integer value with by-value semantics, which means that the address parameter now contains whatever value was pushed on the stack by the CallWindowProc method.

Let’s quickly recap: the VarPtr method pushes the address of the Short or Integer variable – that is, the value we are interested in – on the stack. This 32-bit integer is received by the CallWindowProc method (in User32.dll) and is sent to the VarPtrCallback method, which receives it in its first argument and returns it verbatim to the CallWindowProc method, which in turn returns it to the VarPtr method that can finally return it to the caller.

Notice that you might need to add more overloads for the VarPtr method (and the CallWindowProc method), to support data types other than Short or Integer. Just remember that you can’t use this technique with String, Objects, or other reference types. It doesn’t work with Boolean values, either.

Interestingly, you can use the VarPtr method with structures, provided that the structure doesn’t contain any String, Object, or Boolean elements. To get the address of a structure just use VarPtr on its first element, as in this example:

Structure POINTAPI
   Public x As Integer
   Public y As Integer

End Structure

Dim
pnt As POINTAPI
Dim addr As Integer = VarPtr(pnt.x) 

To prove that the VarPtr function works correctly, let’s use it together with the CopyMemory Windows API method to delete an element in an array by quickly shifting all the elements towards lower indices:

Declare Sub CopyMemory Lib "Kernel32.dll" Alias "RtlMoveMemory" _
   (ByVal dest As Integer, ByVal source As Integer, _
    ByVal numBytes As Integer)


Sub
Main()
   Dim arr(1000) As Integer
   ' initialize the array
  
For n As Integer = 0 To UBound(arr) : arr(n) = n : Next
   ' ...
   ' delete first element by shifting all elements towards the beginning
   '
of the array, then clear last element
   CopyMemory(VarPtr(arr(0)), VarPtr(arr(1)), UBound(arr) * 4)
   arr(UBound(arr)) = 0
   ' check that it worked fine
   For n As Integer = 0 To UBound(arr) - 1
      If arr(n) <> n + 1 Then Debug.Print("Wrong value at index {0}", n)
   Next

End Sub

In case you are wondering why you should use CopyMemory and VarPtr to shift all the elements of an array – instead of a plain For … Next loop – the answer is: execution speed. Under VB6 this technique was often used to significantly speed up array operations; the VB.NET compiler produces more efficient code and this technique is seldom necessary, nevertheless the ability to convert this code from VB6 without any major edits means that you don’t have to spend too much time trying to understand what the VB6 developer meant to do.

Please notice that this implementation of VarPtr works well in 32-bit applications only, and fails when running on 64-bit operating systems. To ensure that things work as expected and that 32-bit code is generated even when running on 64-bit versions of Windows, you must select the Target CPU = x86 option in the Advanced Compile Options dialog box, in the Compile tab of the My Project page. (Odds are that you have to select this option anyway when doing complex operations with pointers.)

Important warnings: working with memory addresses under .NET can be very dangerous, much more dangerous than under VB6. The reason is, the garbage collector can fire virtually anytime while the program is executing, therefore the address of an object can suddenly change and the unmanaged method (CopyMemory in above example) would receive the address of a memory area that doesn't contain the data any longer. The neat result would be either a wrong value or an application crash. When using this implementation of VarPtr under VB.NET keep the following points into account:

  1. VarPtr is absolutely safe only when used to return the address of simple local variables, such as Short, Integer, Single, Double, or Date variables. Using local variables is safe because local variables are allocated on the stack and don't move even if an unexpected garbage collection occurs immediately after the VarPtr method returns but before the unmanaged method complete its execution.
  2. Passing the element of a Structure is safe, but only if the Structure is held in a local variable (as opposed to a class field)
  3. In all other cases, VarPtr isn't  100% safe and might occasionally deliver wrong results or crash the application. For example, it isn't 100% safe to pass VarPtr a class field or an element of an array, because array elements are stored in the managed heap and can be moved by the garbage collector (regardless of whether the array is stored in a local variable). 
  4. In a single-thread application the probability that a garbage collection occurs unexpectedly are very low and might even be considered as negligible, but they can't be considered as equal to zero.
  5. You can further minimize the probability of an unexpected GC by avoiding calling methods and language functions (e.g. Left, Int, Abs) inside the call to the unamanged method but, again, you can't reduce this probability to zero.

To recap, except when you are in cases #1 and #2 above, the converted code will work most of the time, but it can't be guaranteed to work always.The only documented way to ensure that an object doesn't move in memory because of unexpected gargabe collection is by pinning the object, by means of methods exposed by the System.Runtime.InteropServices.Marshal class.

Even with this limitation, the VB.NET implementation VarPtr method is quite helpful when doing a quick-and-dirty migration - using VB Migration Partner or the Upgrade Wizard. You can use VarPtr to check that the converted code works as intended, but it is strongly recommended that you get rid of VarPtr before going to production. In the CopyMemory case see above, for example, you can do without the VarPtr by using a different overload of the CopyMemory method that takes by-reference arguments:

 

Declare Sub CopyMemoryByref Lib "Kernel32.dll" Alias "RtlMoveMemory" _
   (ByRef dest As Integer, ByRef source As Integer, _
    ByVal numBytes As Integer)

(This code works because the .NET Framework automatically pins every object passed by reference to an external method.)

Even better, you should do your best to avoid unamanged calls altogether and replace them with calls to methods of pure .NET objects.



Subtle threading issues and how to fix them (automatically) with VB Migration Partner

clock February 10, 2009 03:09

One customers brought an issue to our attention. He was using a migrated VB.NET component in a heavy multi-threaded environment (the component is called by ASP.NET pages) and found that occasionally the program crashed with an unexplainable error message. A quick look at the stack trace confirmed the initial suspect: the error was caused by two threads that initiates a call to the same method at the same time. We have fixed the problem in our library in a few minutes (the fix will appear in version 1.10.03, due in a few days), but we realized that the problem isn't confined to our library. To explain why, a short digression is in order.

VB6 components use Single-Threaded Apartment (STA) model: each thread lives in an “apartment” of its own. In plain English, this means that each thread works with a different set of global variables. For example, assume that the ActiveX DLL project contains a BAS module with the following variable declaration:

    ' ...(in the UserInfo.bas file)
    Public UserName As String


Thanks to STA threading model, each client – running in a different thread – can write this variable and read it back, without worrying about another client (in another thread) overwriting the variable with a different value. As you see, STA makes multi-threading programming virtually as easy as single-thread (i.e. traditional) programming. Of course,when writing multi-threaded apps you have to take care of many other potential concurrency issues – for example, two threads working on the same temporary file or registry key – but at least you don’t need to worry about global variables.

By contrast, components written in VB.NET – and all .NET languages, for that matter – use free-threading, which means that all threads can access all variables at the same time. Therefore, the VB.NET component exposes the same UserName field to all threads, which means that any thread can overwrite the value stored there by another thread.

To simplify programming in free-threaded contests, the .NET Framework supports the ThreadStatic attribute. You can apply this attribute only to static class fields – either fields defined in modules or fields defined in classes and marked with the Shared kewword:

    <ThreadStatic()> _
    Public UserName As String


All fields marked with the ThreadStatic attribute are stored in the Thread Local Storage (TLS) area. The separation among threads is automatic, because each thread owns a different TLS.

In theory, when migrating a VB6 DLL meant to be used by free-threaded clients, you should scrutinize each and every variable in BAS modules and decide whether to tag them with the ThreadStatic attribute. To make this task as easy as possible, as well as to avoid any manual edit of the resulting VB.NET code, starting with version 1.10.03 VB Migration Partner supports the ThreadSafe pragma, which can be applied at the project-, file-, method- and variable-level scope. In most cases, therefore, all you need to do is using a single pragma that affects all the variables in the current project:

    '## project:ThreadSafe True
    Public UserName As String



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

clock July 31, 2008 06: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 Professional Edition 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 27, 2008 19: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.

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.



Refactoring VB.NET code with regular expressions

clock July 21, 2008 04:22

One of the key features of VB Migration Partner is migration pragmas, and possibly the most powerful pragma is PostProcess, which enables you to modify the VB.NET being generated. The PostProcess pragma is based on regular expressions and works much like the Regex.Replace method.

The PostProcess pragma can really do wonders in the hands of a developer who is familiar with regexes. As a matter of fact, we have already uploaded several KB articles that show how you can refactor your code using PostProcess pragmas:

[HOWTO] Modify project-level options in VB.NET programs
[HOWTO] Move the declaration of a variable into a For or For Each loop
[HOWTO] Transform “Not x Is y” expressions into “x IsNot y” expressions
[HOWTO] Replace App6 properties with native VB.NET members
[HOWTO] Get rid of warnings related to Screen.MousePointer property
[HOWTO] Convert While loops into Do loops
[HOWTO] Simplify code produced for UserControl classes
[HOWTO] Change the base class of a VB.NET class

[HOWTO] Avoid compilation errors caused by unsupported designers

In this post I am going to show you how you can use the PostProcess pragma in a creative way to optimize common Boolean expressions. The regex patterns I will show aren't exactly simple and I won't explain each in depth. But trust me, they work and they work great!

Important note #1: like all substitutions based on regular expressions, there is a small probability that the techniques discussed in this article might suffer from false matches and, therefore, might produce invalid VB.NET code. Ensure that you add these pragmas to your VB6 code only after you have checked that the conversion works well and be prepared to remove these pragmas if you notice that they produce invalid VB.NET code.

Important note #2: most regex patterns in this article are too long for the page width and will be split in two or more lines. If you are in trouble, at the bottom of this KB article you will find a link to a text file that gathers all of the PostProcess pragmas I am introducing here.

1. Comparisons with True and False

Some VB6 developers like to explicitly compare a Boolean variable with True or False, as in this example:
    Dim x As Integer, res As Boolean, res2 As Boolean
    ' ...
    If res = True And res2 = False Then x = 0


Explicit comparison of a Boolean variable with True or False adds a little overhead, which can be avoided by re-rewriting the expression as follows:
    If res And Not res2 Then x = 0

Here are the two PostProcess pragmas that do the trick, the former accounting for comparisons with True and the latter for comparisons with False:

'## PostProcess "(?<=\n[ \t]*)(?<key>If|ElseIf)\b(?<pre>.+)\b(?<cond>\S+)
    [ \t]*=[ \t]*\bTrue\b(?<post>.+)\bThen\b",
    "${key}${pre}${cond}${post}Then", True

'## PostProcess "(?<=\n[ \t]*)(?<key>If|ElseIf)\b(?<pre>.+)\b(?<cond>\S+)
    [ \t]*=[ \t]*\bFalse\b(?<post>.+)\bThen\b",
    "${key}${pre}Not ${cond}${post}Then", True


The condition can appear also inside Do, Loop, and While statements, thus we need two more PostProcess pragmas:

'## PostProcess "(?<=\n[ \t]*)(?<key>(Do|Loop)[ \t]+(While|Until)|While)\b
    (?<pre>.+)\b(?<cond>\S+)\s*=\s*\bTrue\b",
    "${key}${pre}${cond}", True

'## PostProcess "(?<=\n[ \t]*)(?<key>(Do|Loop)[ \t]+(While|Until)|While)\b
    (?<pre>.+)\b(?<cond>\S+)\s*=\s*\bFalse\b",
    "${key}${pre}Not ${cond}", True


2. Not keyword in Do While and Loop While statements

If a Do While or Loop While statement is followed by a condition that is prefixed by the Not keyword, you can drop the Not keyword and transform the statement into Do Until or Loop Until, respectively. For example, the following code
    Do While Not res
        ' ...
    Loop

can be transformed into:
    Do Until res
        ' ...
    Loop


This is the PostProcess pragma that can perform this substitution for you:

'## PostProcess "(?<=\n[ \t]*)(?!.*\b(And|Or|Xor)\b)(?<key>Do|Loop)
    [ \t]+While[ \t]+Not[ \t]+(?<cond>.+)", "${key} Until ${cond}", True


Notice that the regex pattern fails if the condition contains an And, Or, or Xor operator. This check is necessary to avoid to mistakenly match an expression such as this:
    Do While Not res And x = 10

3. Not keyword in Do Until and Loop Until statements

If a Do Until or Loop Until statement is followed by a condition that is prefixed by the Not keyword, you can drop the Not keyword and transform the statement into Do While or Loop While, respectively. For example, the following code
    Do Until Not res
        ' ...
    Loop

can be transformed into:
    Do While res
        ' ...
    Loop


The PostProcess pragma that can perform this substitution is as follows:

'## PostProcess "(?<=\n[ \t]*)(?!.*\b(And|Or|Xor)\b)(?<key>Do|Loop)
    [ \t]+Until[ \t]+Not[ \t]+(?<cond>.+)", "${key} While ${cond}", True


4. Assigning True/False to a variable in a If...Then...Else block

A common (yet inefficient) coding pattern among developers is to use an If…Then…Else block to assign True or False to a Boolean variable. For example, consider the following VB6 code:
    Dim x As Integer, res As Boolean, res2 As Boolean
    ' ...
    If x <> 0 Then
        res = True
    Else
        res = False
    End If            

    If x > 10 And x < 20 Then
        res2 = False
    Else
        res2 = True
    End If


It is evident that it can be simplified (and optimized) as follows:

    res = (x <> 0)
    res2 = Not (x > 10 And x < 20)


Creating a regular expression pattern that allows you capture and transform the If…Then…Else block isn’t a trivial task, also because we need a different PostProcess pragma to account for single-line If statements:

'## PostProcess "(?<=\n[ \t]*)If[ \t]+(?<cond>.+?)[ \t]+Then[ \t]*\r?\n[ \t]
    *\b(?<var>(Return\b|\S+[ \t]*=))[ \t]*True[ \t]*\r?\n[ \t]*Else
    [ \t]*\r?\n[ \t]*\k<var>[ \t]*False[ \t]*\r?\n[ \t]*End[ \t]+If
    [ \t]*\r?\n", "${var} (${cond})\r\n", True

'## PostProcess "(?<=\n[ \t]*)If[ \t]+(?<cond>.+?)[ \t]+Then[ \t]+
    (?<var>(Return\b|\S+[ \t]*=))[ \t]*True[ \t]+Else[ \t]+\k<var>
    [ \t]*False[ \t]*\r?\n", "${var} (${cond})\r\n", True


The next two PostProcess pragmas account for the cases when the variable is assigned False when the condition is true. Again, we need a second pragma to account for single-line IF statements.

'## PostProcess "(?<=\n[ \t]*)If[ \t]+(?<cond>.+?)[ \t]+Then[ \t]*\r?\n[ \t]
    *\b(?<var>(Return\b|\S+[ \t]*=))[ \t]*False[ \t]*\r?\n[ \t]*Else
    [ \t]*\r?\n[ \t]*\k<var>[ \t]*True[ \t]*\r?\n[ \t]*End[ \t]+If
    [ \t]*\r?\n", "${var} Not (${cond})\r\n", True

'## PostProcess "(?<=\n[ \t]*)If[ \t]+(?<cond>.+?)[ \t]+Then[ \t]+
    (?<var>(Return\b|\S+[ \t]*=))[ \t]*False[ \t]+Else[ \t]+\k<var>
    [ \t]*True[ \t]*\r?\n", "${var} Not (${cond})\r\n", True


Notice that the patterns that follow account both for variable assignements and for the Return keyword used to return a value from a function (such Return keyword is used by VB Migration Partner if possible).

5. Assigning different values to a variable inside an If...Then...Else block

Another common coding pattern - which is actually a variant of the previous one - is to use an If…Then…Else block to assign one of two possible values to the same variable, as in these examples:
    If x <> 0 Then
        y = x + 10
    Else
        y = 20
    End If

    ' single-line variant
    If x <> 0 Then y = x + 10 Else y = 20


There is a more concise – though not necessarily more efficient – way to perform the same assignment:
    y = IIf(x <> 0, x + 10, 20)

If you are interested in performing this substitution automatically, you can use the following two pragmas, where the latter works for single-line IF statements:

'## PostProcess "(?<!\bEnd[ \t]+)\bIf[ \t]+(?<cond>.+?)[ \t]+Then[ \t]*\r?\n
    [ \t]*\b(?<var>(Return\b|\S+[ \t]*=))[ \t]*(?<v1>[^\r\n]+)\r?\n
    [ \t]*Else[ \t]*\r?\n[ \t]*\k<var>[ \t]*(?<v2>[^\r\n]+)[ \t]*\r?\n
    [ \t]*End[ \t]+If[ \t]*\r?\n",
    "${var} IIf(${cond}, ${v1}, ${v2})\r\n", True

'## PostProcess "(?<!\bEnd[ \t]+)\bIf[ \t]+(?<cond>.+?)[ \t]+Then[ \t]+
    (?<var>(Return\b|\S+[ \t]*=))[ \t]*(?<v1>[^\r\n]+)\bElse[ \t]+\k<var>
    [ \t]*(?<v2>[^\r\n]+)\r?\n",
    "${var} IIf(${cond}, ${v1}, ${v2})\r\n", True


---------------------------

That's it, folk! If you ever wondered why you should care about regular expressions, I hope I gave you something to think about Wink



Bugs that drove us crazy

clock June 25, 2008 00:02

We all know that writing software requires a vast assortment of skills and, above all, a lot of patience. You usually need the latter to feel comfortable when you realize that you've wasted hours and days over someone else's bug.  Here's the story.

Like many commercial applications, VB Migration Partner has a splash screen. No big deal. VB Migration Partner is written in VB2005, therefore we could use VB.NET application framework. As a matter of fact, defining a splash screen in VB.NET is as easy as selecting an item in a combobox in the Application tab of the My Project designer.



During the beta test period a few users reported that at times VB Migration Partner threw an exception at launch, immediately after closing the splash screen. We never managed to reproduce the problem on our computers. Fortunately the exception occured quite rarely. Until a user running Windows 2000 reported that he was seeing the exception at nearly every launch. After installing VB Migration Partner on Windows 2000 in our lab, we could indeed verify that we had a problem.

 

It took a while to find out that we weren't  the only ones suffering from this issue. As a matter of fact, Microsoft recognizes that it is a VB.NET bug. Fortunately, they also offer a workaround. According to whis workaround you must create an invisible form from inside the Form_Load event of your splash screen, something like this:

   Private Sub Form_Load(ByVal sender As Object, ByVal e As EventArgs) _
          Handles MyBase.Load
     Dim frm As New Form
      frm.Opacity = 0
      frm.ShowInTaskbar = False
      frm.FormBorderStyle = FormBorderStyle.None
      frm.Show()
      Return frm
   End Function

Unbelievably, this code makes the problem vanish away! You can find more details in this Microsoft KB article.

We realized that *all* applications written in VB.NET could suffer from this issue, thus we added a method to our support library, named SplashScreenBugFix6, which creates the invisible form for you. You just need to add an InsertStatement pragma in the original VB6 splash screen form, so that a call to this method is automatically generated anytime you convert the VB6 project:

    ' in the VB6 project
    Private Sub Form_Load()
      '## InsertStatement SplashScreenBugFix6()
      ' more code in the Load event
   End Sub



Neat tricks for smooth migration of calls to Windows API methods

clock June 12, 2008 21: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. (Enterprise Edition only)
  • 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 (Enterprise Edition only)
  • 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!




VB Migration Partner - Subscribe to our feed RSS  blog RSS
 
AddThis Feed Button
 
 

Home blog
 
AddThis Social Bookmark Button


 

Sign in

Search

Calendar

<<  March 2010  >>
SuMoTuWeThFrSa
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910

Archive

Categories

 

Microsoft Regional Director
Microsoft is a registered trademark of Microsoft Corporation in the United States and other countries and is used under license from Microsoft
 

My Blogs


My programming tools



VB Migration Partner


VBMaximizer for VB6


Form Maximizer for .NET

 

My books


Programming Microsoft Visual Basic 6


Programming Microsoft Visual Basic 2005: The Language


Practical Guidelines and Best Practices for Microsoft Visual Basic .NET and Visual C# Developers