Francesco's blog

 Monday, May 01, 2006

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

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

Public Class ComboBoxEx
  
Inherits System.Windows.Forms.ComboBox

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

End Class

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

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

MsgHookX is an ActiveX DLL that allows VB6 developers to perform safe and efficient intra-applicaton subclassing. (In this context, intra-application sublclassing means that you can intercept any message that Windows sends to a window or a control in the current application, as opposed to a window created by another application, a much more difficult task.) I wrote this DLL many years ago, when VB5 was released, and mentioned it in several books and magazine articles. I made the DLL available on the vb2themax site, but I omitted to upload the DLL in this new, .NET-only site.

Well, given the many mails I continue to receive from readers, it seems that VB6 is still alive and in good shape, and many VB6 developers continune to happily subclass their controls. For this reason I decided to make the DLL available again on this site. Here's a summary of what it does:

  • safe subclassing: can be used within the IDE and in break mode without any risk of system crashes.
  • it provides the BeforeMessage and AfterMessage events for easy event-driven programming model
  • it additionally can notify incoming messages through the IMsgHookEvents secondary interface, for better performance and easier debugging (in some cases, events are inhibited in the IDE)
  • highest flexibility: you can decide to call the original window procedure yourself from within the BeforeMessage event/method and/or cancel the default processing for the message. You can also browse and modify the value that will be returned to the operating system.
  • the DLL's type library includes the definition of over 300 symbolic constants that define the most common Windows messages, so you don't have to use the API Viewer to include them in your applications.

Have fun with it, but - please, please! - turn to VB.NET as soon as you can, if you haven't yet.

4/29/2006 9:09:29 AM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 

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

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

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

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

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

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

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

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

I am reorganizing my MP3 collection and found that I needed to rename a large quantity of files. Of course, there are many free utilities that allow this operation - and that can use MP3 tags in the process - but I thought that I might write one myself. Thanks to regular expressions, the task shouldn't be that hard. In fact, in a few minutes I came up with the following console application. As you see, most of the code is used to extract and validate arguments on the command line:

Imports System.Text.RegularExpressions
Imports System.IO

Module Renx

  
Function Main(ByVal args() As String) As Integer
     
Console.WriteLine("RENX (C) Francesco Balena / Code Architects Srl")

      Dim recurse As Boolean = False
     
Dim renameMode As Boolean = False
     
Dim oldNamePattern As String = Nothing
     
Dim newNamePattern As String = Nothing

      ' analyze each argument
     
For Each arg As String In args
        
Select Case arg.ToLower()
           
Case "/s", "-s"
              
recurse = True
            
Case "/r", "-r"
              
renameMode = True
           
Case "/h", "-h"
              
Return ShowHelp(0)
           
Case Else
              
If oldNamePattern Is Nothing Then
                 
oldNamePattern = "^" & arg & "$"
              
ElseIf newNamePattern Is Nothing Then
                 
newNamePattern = arg
              
Else
                 
Return ShowHelp(1)
              
End If
        
End Select
     
Next

      ' check that we have both mandatory arguments
     
If oldNamePattern Is Nothing OrElse newNamePattern Is Nothing Then
        
Return ShowHelp(1)
     
End If
     
' create the regex and check that pattern syntax is ok
     
Dim reSearch As Regex
     
Try
        
reSearch = New Regex(oldNamePattern, RegexOptions.IgnoreCase)
        
' test the replace pattern as well
        
Dim tmp As String = reSearch.Replace("a dummy string", newNamePattern)
      
Catch ex As Exception
         Console.WriteLine(
"SYNTAX ERROR: {0}", ex.Message)
        
Return 3
     
End Try
     
Console.WriteLine()

      ' iterate over all files in current directory (and its subdirectories, if recurse mode)
     
Dim searchOpt As SearchOption = SearchOption.TopDirectoryOnly
     
If recurse Then searchOpt = SearchOption.AllDirectories

      Dim parsedFilesCount As Integer = 0
     
Dim renamedFilesCount As Integer = 0
     
Dim errorsCount As Integer = 0
     
For Each oldFile As String In Directory.GetFiles(Directory.GetCurrentDirectory(), "*.*", searchOpt)
         parsedFilesCount += 1
        
' the regex applies to name only
        
Dim oldName As String = Path.GetFileName(oldFile)
        
Dim ma As Match = reSearch.Match(oldName)
        
If ma.Success Then
           
' this is the new name
           
Dim newName As String = ma.Result(newNamePattern)
            Console.WriteLine(oldFile)
            Console.Write(
" => {0}", newName)
            renamedFilesCount += 1
           
' proceed with rename only if not in simulation mode
           
If renameMode Then
              
Try
                 
Dim dirName As String = Path.GetDirectoryName(oldFile)
                 
Dim newFile As String = Path.Combine(dirName, newName)
                  File.Move(oldFile, newFile)
              
Catch ex As Exception
                  Console.Write(
" -- ERROR: {0}", ex.Message)
                  errorsCount += 1
              
End Try
           
End If
           
Console.WriteLine()
        
End If
     
Next

      ' Display a report
     
If renameMode Then
        
Console.WriteLine("Summary: {0} parsed files, {1} renamed files, {2} errors", parsedFilesCount, renamedFilesCount, errorsCount)
     
Else
        
Console.WriteLine("Summary: {0} parsed files, {1} files affected", parsedFilesCount, renamedFilesCount)
         Console.WriteLine()
         Console.WriteLine(
"NOTE: Running in simulation mode. Specify the /R option to actually rename files.")
     
End If
     
' Return an error code
     
If errorsCount = 0 Then
        
Return 0
     
Else
        
Return 2
     
End If
  
End Function

   Function ShowHelp(ByVal exitCode As Integer) As Integer
     
Console.WriteLine()
      Console.WriteLine(
"Syntax: RENX <oldnamepattern> <newnamepattern> [/R] [/S] [/H]")
      Console.WriteLine(
" oldnamepattern : regex that selects the files to be renamed")
      Console.WriteLine(
" newnamepattern : regex that specifies how files must be renamed")
      Console.WriteLine(
" /R : rename files")
      Console.WriteLine(
" /S : iterate over subdirectories")
      Console.WriteLine(
" /H : display this help")
      Console.WriteLine(
"NOTE: By default the program runs in simulation mode, and just displays how files would be renamed.")
      Console.WriteLine(
" You must specify the /R option to actually rename the files.")
     
Return exitCode
  
End Function

End Module

At the very minimum, the RENX utility requires two arguments: a regex that specifies which files in the current directory (and its subdirectories, if you add the /S option) must be renamed, and a second regex that specifies how to rename the files that are matched by the first regex. The power of RENX is the fact that the first regex can (actually, must) specify one or more groups of characters, and these groups are then referenced in the second regex. For example, let's suppose that I have a folder with the following files:

        01 Speak to Me.mp3
        02 On the Run.mp3
        03 Time.mp3
        04 The Great Gig in the Sky.vb3
        05 Money.mp3
        06 Us and Them.mp3
        07 Any Colour You Like.vbr
        08 Brain Damage.mp3
        09 Eclipse.vb3

and that I want to rename them as follows:

        01 - Speak to Me - The Dark Side of the Moon.mp3
        02 - On the Run - The Dark Side of the Moon.mp3
        03 - Time - The Dark Side of the Moon.mp3
        04 - The Great Gig in the Sky - The Dark Side of the Moon.vbr
        05 - Money - The Dark Side of the Moon.mp3
        06 - Us and Them - The Dark Side of the Moon.mp3
        07 - Any Colour You Like - The Dark Side of the Moon.vb3
        08 - Brain Damage - The Dark Side of the Moon.mp3
        09 - Eclipse - The Dark Side of the Moon.vbr

Here's the RENX command that does it:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon.${3}"

Notice that the first regex creates three groups by enclosing them in parenthesis: (\d\d) matches the song number, (.+?) matches the song title, and (\..+) matches the file extension, dot included. The second argument can then reorder these three groups, using the ${N}, where N is the position of the group as specified in the first regex. It is therefore to insert a dash after the song number, and the albumname after the song title.

Because the RENX utility is quite dangerous, by default it doe NOT rename the files, and it just lists how files would be renamed. To actually proceed with the rename operation, you must specify the /R option:

        RENX "(\d\d) (.+?)(\..+)"    "${1} - ${2} - The Dark Side of the Moon${3}" /R

That's all. You can play with the source code to extend the RENX utility as you prefer, and maybe turn it into a Windows Form application, or you can download the binary version from this link: Renx.zip (5.51 KB)

3/6/2006 5:30:10 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Tuesday, February 28, 2006

A few minutes ago I made all the code samples for Programming Microsoft Visual Basic 2005: The Language available on the book home page. It's a 7.5M download that contains several thousand lines of carefully crafted, optimized, reusable source code, including the following:

  • a VB class that works with fractions
  • a base form for simplified data entry
  • a class to cache text files
  • several custom iterators for better For Each loops
  • tons of examples with generics
  • two expression evaluators, the former built on regexes, the latter implemented via on-the-fly compilation
  • a utility to display project statistics (a demo for regexes)
  • a RegexTester stand-alone utility, to help you build, test and compile regular expressions
  • an example of custom provider for My.Settings
  • an example of how you can intercept ANY event from ANY set of Windows Forms controls
  • attribute-based benchmarks
  • a complete infrastructure for writing Windows Forms plug-ins
  • an attribute-based library to write data-centric N-tier applications
  • several Visual Studio macros
  • Visual Studio visualizers for files, regex, and images
  • .... and a lot more

On the same page you can also find an errata document for typos and mistakes found after the book went to print.

Enjoy the code (and buy the book if you like it! :-) )

2/28/2006 7:10:13 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [4]  | 
 Thursday, February 23, 2006

The longer I work with generics, the more I like them, and I continue to discover new ways to simplify my code by using them. More specifically, I like the ability to write type-safe code that is also more concise, efficient, and (above all) readable, because I don't have to use tons of CType and DirectCast operators. Today I gathered the generic methods I use more frequently in the following module. They are tiny and simple, yet they save me a lot of time and code.

Module GenericFunctions

   ' Swap two variables
  
Public Sub Swap(Of T)(ByRef var1 As T, ByRef var2 As T)
     
Dim tmp As T = var1
      var1 = var2
      var2 = tmp
  
End Sub

   ' Type-safe version of the IIF function
  
' returns valueOnTrue if expression is True, else returns valueOnFalse
  
Public Function IIf(Of T)(ByVal expression As Boolean, ByVal valueOnTrue As T, ByVal valueOnFalse As T) As T
     
If expression Then
        
Return valueOnTrue
     
Else
        
Return valueOnFalse
     
End If
  
End Function

   ' Type-safe version of the Choose function
  
' returns the N-th element of a list of values, or the default value for T if index
  
' is less than 0 or higher than the number of values
  
Public Function Choose(Of T)(ByVal index As Integer, ByVal values() As T) As T
     
If index >= 0 AndAlso index < values.Length Then
        
Return values(index)
     
Else
        
Return Nothing
     
End If
  
End Function

   ' Return an array of the specified type
  
Public Function NewArray(Of T)(ByVal ParamArray values() As T) As T()
     
Return values
  
End Function

   ' Return the min value of a list
  
Public Function Min(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) > 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return the max value of a list
  
Public Function Max(Of T As IComparable)(ByVal firstValue As T, ByVal ParamArray values() As T) As T
     
Dim result As T = firstValue
     
For Each value As T In values
        
If result.CompareTo(value) < 0 Then result = value
     
Next
     
Return result
  
End Function

   ' Return True if a value is in specific range
  
Public Function InRange(Of T As IComparable)(ByVal testValue As T, ByVal minValue As T, ByVal maxValue As T) As Boolean
     
Return testValue.CompareTo(minValue) >= 0 AndAlso testValue.CompareTo(maxValue) <= 0
  
End Function

   ' Retrieve a dictionary element of a given type, or the provided default value if the element isn't found
  
' (two overloads)
  
Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Hashtable, ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
If dict.ContainsKey(key) Then
        
Return CType(dict(key), TValue)
     
Else
        
Return defaultValue
     
End If
  
End Function

   Public Function GetDictionaryValue(Of TKey, TValue)(ByVal dict As Dictionary(Of TKey, TValue), ByVal key As TKey, ByVal defaultValue As TValue) As TValue
     
' If the key is in the dictionary, the following statement stores the corresponding value
     
' in defaultValue, else it leave defaultValue unchanged
     
dict.TryGetValue(key, defaultValue)
     
Return defaultValue
  
End Function

End Module

Most methods are self-explanatory. One of the most useful ones is NewArray, which lets you create an array and pass it on-the-fly to a method. Let's say the the DoSomething method takes an array of Integers. These are the options you have in VB2005:

    ' 1. Create the array first, than pass it
    Dim values() As Integer = {1, 2, 3, 4, 5}
    DoSomething(values)

    ' 2. Create the array on the fly using the nearly-undocumented syntax
    DoSomething(New Integer() {1, 2, 3, 4, 5})

I often use the second syntax, but I noticed that relatively few developers know it. My code is much more readable with the NewArray method

    ' 3. Use the NewArray generic function to create the array on the fly
    DoSomething(NewArray(1, 2, 3, 4, 5))

The NewArray method proves to be quite useful also to build For loops whose index can take any sequence of values:

    ' Test whether "number" is a prime number in the range 1-1000
    For Each n As Integer In NewArray(2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31)
       If (number Mod n) = 0 Then Console.Write("{0} is not prime", number): Exit For
    Next

2/23/2006 8:08:53 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, February 15, 2006

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

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

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

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

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

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

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

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

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

Great news for yours truly.

Practical Guidelines and Best Practices for Visual Basic and Visual C# Developers won an Excellence Award from the Society of Technical Communication, which selects the best books, sites, and coursewares published in the year. Not just programming-related or computer-related publishing, but technical publishing in the wider sense. As a matter of fact, among the books that won the award, only about ten have to do with software. And the book I co-authored with Giuseppe Dimauro (the other Italian RD) is the only book from Microsoft Press that won the award. I guess it's something we can be quite satisfied of :-)

It's an important acknowledgement and I am especially happy because it's the first book I wrote that isn't part of the "Programming Visual Basic" series, which now counts four editions (VB6, 2002, 2003, and 2005). The "Practical Guideline" book is a truly unique book, the first and only coherent collection of guidelines for .NET developers.

 

2/12/2006 8:51:08 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Sunday, January 29, 2006

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

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

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

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

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

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

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

' Base class

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

' One dimensional array of type T

Public Class VBArray(Of T)
  
Inherits VBArrayBase
  
Implements IEnumerable

   Dim items() As T

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

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

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

End Class

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

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

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

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

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

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

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

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

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

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

1/29/2006 3:30:03 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Monday, January 16, 2006

If I could get istantaneous results for the following simple two-question survey

  1. Is Visual Studio the application that you use most often?
  2. Did you ever use regex searches in the VS Find dialog box?

I'd bet that 80% of you would answer YES to the first question, but 99% of you would answer NO to the second question, which would be a rather weird result. Regex searches are among the most powerful VS features, yet few developers use them or even know that they exist.

IMHO, the real problem is that VS regex's syntax is completely different from the syntax you use with the Regex class, therefore using this feature requires that you learn yet another regex dialect. This is a bit too much for most developers. Microsoft should allow the standard regex syntax in this dialog: they could implement this change very easily and in a short time, without caring about backward compabibility issues.

While waiting for Microsoft to offer this little-big innovation, you can have fun with what you have today. Here are a few examples, excerpted from my new book Programming Microsoft Visual Basic 2005: The Language:

:i = :z   Search assignments of an integer constant (:z) to a variable (:i). In VB, but more rarely in C#, it can deliver false matches, when the = operator is used in an expression.

:i = :q   Search assignments of a quoted string constant (:q) to a variable.

(Dim|Private|Public) :i As String   Search for variable declarations of string type (VB only). You can easy adapt it to other data types.

Dim <(:Lu(:Ll)*)+> As   Search for local VB variables that use a PascalCase naming convention and therefore violate Microsoft's guidelines. (Local variables should use the camelCase convention.)

^:b*'.+\n   Search for comment lines in VB, that is, lines that begin with an apostrophe. (It doesn't consider the REM keyword.) You can replace the apostrophe with // to use this search pattern in C# as well.

Dim {:i} As (.|\n)#<\1>    Highlights the portion of code between the declaration of a local variable and the first occurrence of that variable in the method. You can repeat this search for all the local variables in a method and check whether you should refactor your code by moving the declaration closer to where the variable is used for the fist time. (See effect in figure below.)

1/16/2006 5:13:02 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Saturday, January 07, 2006
In a week or two my new book, Programming Microsoft Visual Basic 2005: The Language will be available on Amazon and other online stores. Towards the end of January it should be available in most bookstores in the US.

In the meantime, you can get a taste of how the book looks like by reading two online chapters: Chapter 11, "Generics", and Chapter 18, "Reflection".

1/7/2006 4:53:16 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Wednesday, December 28, 2005

Today I spent a few minutes trying to understand the behavior of nullable types in C#, in particular how comparison operators evaluate their result. An asimmetry exists between the == and != operators and all the other comparison operators. In fact, the equal and not-equal operators work perfectly even when the two operands are both null, as in the following code:

int? x1 = null;
int? x2 = null;
Console.WriteLine(x1 == x2);    // => True
Console.WriteLine(x1 != x2);     // => False

The remaining comparison operators, however, don't deal with null values in the same correct way. If either operand is null, these operators always return false. This inconsistent behavior brings to weird situations, in which two values can satisfy the "equal to" relation but not the "equal to or greater than" relation:

int? x1 = null;
int? x2 = null;
Console.WriteLine(x1 == x2);    // => True
Console.WriteLine(x1 >= x2);    // => False

To compare nullable values in a coherent way, you should use the Nullable.Compare static method, which considers null as less than any other value:

switch (Nullable.Compare(d1, d2))
{
  
case -1:
     
Console.WriteLine("d1 is null or is less than d2");
     
break;
  
case 1:
     
Console.WriteLine("d2 is null or is less than d1");
     
break;
  
case 0:
     
Console.WriteLine("d1 and d2 have same value or are both null.");
     
break;
}

In some cases, you can compare two nullable values by using the lowest value in the range in lieu of the "unknown" state, as in:

Console.WriteLine(x1.GetValueOrDefault(int.MinValue) >= x2.GetValueOrDefault(int.MinValue));

(Of course, you can use this approach only if you're sure that operands can be assigned the int.MinValue value.) Besides being available in VB2005 as well, a minor advantage of these techniques is that they generate fewer IL code that is also slightly faster than the code produced by comparison operators. These operators, in fact, always invoke the GetValueOrDefault AND the HasValue property behind the scenes.

C#
12/28/2005 6:46:25 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
 Friday, December 16, 2005

As I anticipated yesterday, here is the TOC of my newest Programming Microsoft Visual Basic 2005, to be available in stores in early January.

Part I : The Basics
 1. Introducing the .NET Framework
(12 pages): a quick overview of basic concepts in .NET programming.
 2. Basic Language Concepts (70 pages): modules, classes, variables, arrays, operators, etc. plus what you need to know about inheritance and attributes so that you can read next chapters, before chapter 8 and 19.
 3. Control Flow and Error Handling (40 pages): If, Select, For, For Each and other basic statements; error handling, with many not-so-obvious techniques to improve code efficiency and programming style.

 4. Using Visual Studio 2005 (56 pages): the many new features of VS2005 IDE, plus many old features that not all developers know; how to write a code snippet for VS2005; templates, refactoring, and a brief but intense tutorial on VS macros.

 5. Debugging and Testing (56 pages): breakpoints and tracepoints, data tips, how to write a cusom visualizers, trace commands and trace listeners (including custom listeners), benchmarks and profiling, unit testing, and code coverage.

Part II : Object-Oriented Programming
 6.
Class Fundamentals (42 pages): the "usual" story about classes, methods, properties, etc. plus the new partial classes and operator overloading, all peppered with the description of relatively unknown programming techniques.
 7. Delegates and Events (26 pages): a small chapters with many details and secrets on how to use these VB features in real-world apps. It includes the new custom events.
 8. Inheritance (34 pages): inheritance at its best, including visual (form) inheritance and many real examples.

 9. Object Lifetime (28 pages): everything you might need to know about garbage collection, the Dispose/Finalize pattern, weak references, GC generations, object resurrection, and other advanced techniques that can take your app to the next level
10. Interfaces (28 pages): how to define a custom interface and, above all, how to leverage those that .NET provides you with, such as IComparer and IEnumerable.
11. Generics (40 pages): half of what you want to know about this new great .NET 2.0 feature (the second half is in chapter 13), including constraints, nullable types, and many examples of programming techniques that are based on generics.

Part III : Working with the .NET Framework
12. .
NET Basic Types (50 pages): working with strings, numbers, and dates at their best, including many little/big new features of .NET 2.0
13. Arrays and Collections (53 pages): arrays, jagged arrays, "traditional" and generics collections, plus many tricks for writing less code that runs faster.
14. Regular Expressions (40 pages): a reference of regex syntax, plus many practical examples on data validation, data parsing, and even code parsing. If you aren't familiar with regexs you are missing a great occasion for writing better code in less time.
15. Files and Streams (42 pages): an overview of all the types in System.IO and the many new features in .NET 2.0, including ACL support, compressed streams, and the TextFieldParser type.
16. The My Namespace (48 pages): how to use the My namespace and how to extend it as you need.
17. Assemblies and Resources (44 pages): despite of their importance, resources (either simple or localized) are used rarely and unproperly by most developers; this chapter includes a complete description of the many important features added to NGEN.

Parti IV : Advanced Topics
18. Reflection