 Friday, January 11, 2008
|
Many readers asked whether I was about to release a book about VB 2008 and, more in general, how come I have reduced my writing activity so dramatically. The answer is in this post.
Immediately after publishing my C# 2005 book I started working on a very ambitious software project. Now, after exactly two years of hard work, VB Migration Partner is in beta test stage and is quickly approaching the public release. |
The name of this tool says it all: in a nutshell, it is a converter of VB6 applications to VB.NET. I won’t waste your time trying to emphasize how weak the Upgrade Wizard that comes with Visual Studio is: if you’ve never tried it out, just google for “vb6 migration” and read what developers and bloggers have to say about it. Our goal at Code Architects was to build a better mouse trap than that.
A word on the migration-vs-rewriting debate: Many industry experts suggest that you should rewrite your VB6 from scratch rather than trying to port it to .NET automatically using a conversion tool, because the two environments are just too different. In theory this argument makes a lot of sense; in practice, however, when you have a real-world business app with some hundreds thousands lines of code – or even millions of lines! – a rewrite from scratch is simply too expensive, especially when the developers who wrote it originally aren’t working with the company any longer, the documentation is poor, remarks are scarce, etc. When facing the perspective of investing many years/man in this daunting task, it’s no surprise that many companies have postponed the migration to “some other time.” Now they are realizing, however, that the migration step can’t be postponed any longer, either because a few VB6 apps don’t work under Microsoft Vista – possibly because they use 3rd-party controls that are incompatible with the new Microsoft operating system – or, above all, because Microsoft declared that VB6 support ends on March 2008.
The problem is, automatic code converters don’t have a good reputation and rarely keep their promises, regardless of the language you are migrating from/to. In most cases, they manage to convert 95% of existing code, or less. With a 100,000 line application – a medium-size application of average complexity, then – having to manually fix the remaining 5% means that you have to fix about five thousand lines. It doesn’t sound too bad, until you realize that most of these “fixes” might take hours or even days. For example, consider that VB6 and VB.NET have two vastly different (and incompatible) programming models for drag-and-drop. Therefore one OLEDrag method counts as an individual problematic statement, yet it forces you to rewrite a large portion of your code. Drag-and-drop isn’t the only example, because the same concept applies to graphic statements, printing, data-binding, object finalization, component initialization, and many others.
We tested our VB Migration Partner on hundreds of VB6 apps – for well over one million lines – and found out that it delivers code that compiles and runs correctly most of the times, with an average of one compilation error every 1,100 lines of code. This corresponds to an accuracy higher than 99.9%. Not bad, uh? Of course the actual accuracy depends on how well the VB6 code is and on the features that the application uses, thus you can get a better idea of how good our tool is by having a look at the VB6 features that itsupports ...and that are out of reach for most other conversion tool on the market:
- arrays with nonzero LBound
- Gosub, On...Goto, and On ...Gosub
- auto-instancing variables (Dim x As New Person), which keep their lazy-instantiation feature even under VB.NET
- As Any parameters and callback addresses in Declare parameters (e.g. EnumWindows)
- default properties resolved correctly even for late-bound variables
- deterministic finalization for objects such as Connection and Recordset, to ensure that the connection is correctly closed when the variable is set to Nothing or the current scope is exited
- (limited) support for Variant variable and null propagation in expressions
- all 60+ controls in the VB6 toolbox, with the only exceptions of OLE container and Repeater, plus the support for common ActiveX controls and components such as WebBrowser, ScriptControl, Scripting runtime, and the MSWLess library
- control arrays, including arrays of 3-rd party controls
- popup menus
- the Controls.Add method and the VBControlExtender object, to dynamically create data-entry forms
- graphic methods (Line, Circle, PSet, PaintPicture, etc.), with support for any ScaleMode, including custom user modes
- the Printer object and the Printers collection
- OLE drag-and-drop
- UserControl classes, with support for advanced features such as the Ambient and Extender objects
- data-binding to DAO, RDO, and ADO Data controls, ADO recordsets, DataEnvironment objects
- non-hierarchical DataEnvironment objects
- ADO data source and simple data consumer classes and user controls (e.g. custom ADO Data controls)
- MultiUse, PublicNotCreatable, GlobalMultiUse classes
- persistable classes, MTS/COM+ classes
The number of supported features is so high that it is simpler to list the features that are not supported: UserDocument, PropertyPage, WebClass and DHTML Page components; undocumented methods (VarPtr, ObjPtr, StrPtr); “classic” drag-and-drop, and little else.
VB Migration Partner can convert an entire VB6 project group in one operation, in which case it typically delivers better quality code because it can see “inside” a DLL that belongs to the group. The tool also includes a very sophisticated code analyzer that can spot unused members: a large project that has evolved over many years can easily include 5-10% of “dead code”, thus this feature alone can save you a lot of precious time because it allows you to focus on just the code that is really important.
One of the secrets for such high error-free conversion factor is the support for migration pragmas. A migration pragma is a special remark that you can add to the original VB6 code and that teaches VB Migration Partner how a given portion of code must be translated, if many alternatives exist. For example, the following ArrayBounds pragma tells VB Migration Partner that all the arrays in the current projects must have their lower bound index forced to zero: '##project:ArrayBounds ForceZero Pragmas can have project, class, method, or variable scope. A pragma scoped at the variable- or method-level always have higher priority than pragmas scoped at the project- or class-level. For example, you can override the previous pragma inside a method, and then override this latter pragma for a given array variable: Sub Test() '## ArrayBounds Shift '## arr.ArrayBounds ForceZero '## names.ShiftIndexes 1 Dim names(1 To 10) As String Dim values(1 To 10) As Long Dim arr(1 To 10) As Integer names(1) = "John": names(2) = "Ann" '... End Sub The Shift option tells VB Migration Partner that the LBound and UBound indexes must be shifted towards zero, so that the total number of elements in the array is preserved. The ShiftIndexes pragma –applied only to the names array – ensures that constant indexes are correctly shifted too: Sub Test() Dim names(9) As String Dim values(9) As Integer Dim arr(0 To 10) As Short names(0) = "John": names(1) = "Ann" End Sub VB Migration Partner supports over 50 pragmas. For example, the SetName pragma changes the name of a control or variable during the migration, SetType changes its type, AutoNew implements the auto-instancing behavior of As New variables, AutoDispose implements the deterministic finalization when the variable is set to Nothing or the current scope is exited, and so forth.
One of the main defects of a “traditional” conversion tool is that – after the first migration – there is no relationship between the original VB6 project and the new VB.NET project. If the migration process takes weeks or months – which is common for large projects – and if the VB6 application must evolve in the meantime with new features and bug fixing, then at the end of the migration process the VB.NET code is already outdated and must be manually patched. The margin for mistakes in this phase is very high.
VB Migration Partner allows you to work around this issue, by means of pragmas and what we call the convert-text-fix cycle methodology. In a nutshell, you can iteratively migrate a given VB6 project by reaching the zero compilation errors stage and then the zero runtime error stage by simply inserting pragmas in the original project, but without modifying any VB6 executable statement. Thanks to the convert-test-fix cycle you can face complex migration tasks and keep the VB6 and VB.NET versions of your app always in sync.
I am especially proud of VB Migration Partner’s refactoring engine, which is able to apply many optimization techniques the result of the “raw” conversion. For example, it can merge variable declarations and the initializations, to generate Return statements, and to leverage the compound assignment operators (e.g. +=), and more:
Function GetValue() As Long => Function GetValue() As Integer Dim x As Long Dim x As Integer = 123 x = 123 ... ... If x > 0 Then If x > 0 Then Return x GetValue = x ElseIf x = 0 Then Exit Function Return -1 ElseIf x = 0 Then End If GetValue = -1 ... Exit Function x += 1 End If ... ... End Function x = x + 1 ... End Function
Other refactoring techniques can be achieved by means of pragmas. For example, you can move the declaration of a variable inside a For or For Each method, as in this example:
Dim i As Integer => For i As Integer = 1 To 100 For i = 1 To 100 ... ... Next Next
On top of the cake: in spite of all these features, VB Migration Partner is up to 8 times faster than the Upgrade Wizard that comes with Visual Studio!
You can learn all about VB Migration Partner on our new site www.vbmigration.com. The web site hosts a Resource section where we dissect each and every difference between VB6 and VB.NET – most of which have never been documented anywhere – thus you can find many useful tips even if you don’t plan to use our tool. I also opened a new blog, entirely devoted to migration techniques.
That's it for now. Stay tuned!
 Saturday, December 22, 2007
When you migrate code from VB6 - regardless of whether you are using an automatic conversion tool - chances are that string-intensive code will actually run slower under VB.NET, if it uses a lot of string concatenation operations. For example, this code takes 2.8 seconds when it runs in VB6 and 27 seconds after its conversion to VB.NET on my 3GHz system:
Dim s As String = "" For i As Integer = 1 To 100000 s = s + "*" Next
The solution is of course trivial: just replace the string variable with a StringBuilder object. Alas, this fix requires that you completely revise your source code, because you need to replace all + and & operators with the Append method, not to mention cases where the StringBuilder is used as an argument to string functions such as Trim or Len.
Is there a way to speed up the previous code with a minimal impact on the code itself? The answer is yes and the solution is actually very simple: you just need to create a VB.NET class that is based on the System.Text.StringBuilder object, that redifines the + and & operators, and that supports the implicit conversion to and from the System.String type. Authoring such a StringBuilder6 class is a matter of minutes:
' a wrapper for the StringBuilder object, with support for + and & operators
Public Class StringBuilder6
Private buffer As New System.Text.StringBuilder
' return the inner string
Public Overrides Function ToString() As String Return buffer.ToString() End Function
Public Shared Operator +(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6 op1.buffer.Append(op2) Return op1 End Operator
Public Shared Operator &(ByVal op1 As StringBuilder6, ByVal op2 As String) As StringBuilder6 op1.buffer.Append(op2) Return op1 End Operator
' convert to string
Public Shared Widening Operator CType(ByVal op As StringBuilder6) As String Return op.ToString() End Operator
' convert from string
Public Shared Widening Operator CType(ByVal str As String) As StringBuilder6 Dim op As New StringBuilder6() op.buffer.Append(str) Return op End Operator
End Class
Once the StringBuilder6 class is in place, you just need to replace the type of the String variable:
Dim s As StringBuilder6 = ""
After this edit, the loop runs in 0.008 seconds, that is about 2000 times faster!!! Not bad, for such a simple fix 
Regardless of whether you are migrating code from VB6 or you've written VB.NET or C# code from scratch, the StringBuilder class gives you a quick and simple way to check whether your string concatenations can be optimized by resorting to a StringBuilder.
 Wednesday, May 31, 2006
Visual Basic 6's App object exposes a useful property named PrevInstance, which allows you to determine whether there are other instances of the same application running. This property has never been implemented in VB.NET, even though a VB2005 application can use the StartupNextInstance event for this purpose.
' to display this code, open the Application page of the My Project designer and click the Application Events button
Namespace My Partial Friend Class MyApplication Private Sub MyApplication_StartupNextInstance(ByVal sender As Object, ByVal e As Microsoft.VisualBasic.ApplicationServices.StartupNextInstanceEventArgs) Handles Me.StartupNextInstance ' another instance of this application has been launched End Sub End Class End Namespace
The problem of this approach is that the event is raised in the first instance of the application, not in the new instance, therefore when the VB.NET application starts you can't determine for sure whether it is the only instance in the system. In other words, you can learn only whether and when another application is launched, not if the current applicatin is the first and only running instance.
.NET developers have solved this problem in a variety of ways, for example using Mutexes or by iterating over the collection returned by the Process.GetProcesses method. In .NET we have a new and more elegant alternative, based on the System.Threading.Semaphore type. A semaphore is a counter which can be incremented and decremented. If its value becomes zero, a thread can't decrement it and must wait for another thread to increment it to a value >0. Interestingly, if the semaphore has a name it becomes a system-wid object which can be shared by all the applications that ask for a reference to a semaphore with that specific name. It is therefore sufficient that the app creates a semaphore with a unique name just after its launch (e.g. it can be a name that includes the executable's path) to have all the instances of a given app share the same semaphore and therefore the same counter. The only problem left to solve is ensure that the semaphore's counter be correctly resotred when the application terminates, but this is easy to achieve by means of a Finalize method.
In addition to the PrevInstance property - which returns False if the application was the only running instance when the application was launched - the following VB6App class exposes also the InstanceCount property, which returns the total number of instances in that moment (the current app is included in the count). Here's the VB2005 version of this class:
Class VB6App ' the default instance Private Shared DefValue As New VB6App
' the system-wide semaphore Private semaphore As System.Threading.Semaphore ' initial count for the semaphore (very high value) Private Const MAXCOUNT As Integer = 10000
Private Sub New() Dim ownership As Boolean = False ' create a unique name, but strip invalid characters Dim name As String = "VB6App_" & System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\", "") semaphore = New System.Threading.Semaphore(MAXCOUNT, MAXCOUNT, name, ownership) ' decrement its value semaphore.WaitOne() ' if we got ownership, this app has no previous instances m_PrevInstance = Not ownership End Sub
' the PrevInstance property returns True if there was a previous instance running ' when the default instance was created Private Shared m_PrevInstance As Boolean
Public Shared ReadOnly Property PrevInstance() As Boolean Get Return m_PrevInstance End Get End Property
' return the total number of instances of the same application that are currently running Public Shared ReadOnly Property InstanceCount() As Integer Get ' release the semaphore and grab the previous count Dim prevCount As Integer = DefValue.semaphore.Release() ' acquire the semaphore again DefValue.semaphore.WaitOne() ' eval the number of other instances that are currently running Return MAXCOUNT - prevCount End Get End Property
Protected Overrides Sub Finalize() ' increment the semaphore when the application terminates semaphore.Release() End Sub
End Class
Notice that this class has a Finalize method without implementing IDisposable. It is one of those special cases when it is OK to violate the Dispose-Finalize pattern.
To understand how the class works, just keep in mind that the Semaphore.Release method increments the internal counter, whereas the WaitOne method decrements it. You must test the VB6App.PrevInstance property as soon as the application starts, for example in the Sub Main method or in the Load event hander for the main form, as to let the VB6App class store the boolean value internally. The same form might then test the value of the InstanceCount property on exit, for example if you need to run some cleanup code when the last application instance is about to terminate:
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load If Not VB6App.PrevInstance Then ' open the common log file ' ... End If End Sub
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing If VB6App.InstanceCount = 1 Then ' close the common log file. ' ... End If End Sub
Here's the C# version of the class. (I fixed the original version to fix two syntax errors mentioned in one of the comments below.)
public class VB6App { // the default instance private static VB6App DefValue = new VB6App(); // the system-wide semaphore private System.Threading.Semaphore semaphore; // initial count for the semaphore (very high value) private const int MAXCOUNT = 10000;
private VB6App() { // create a named (system-wide semaphore) bool ownership = false; // create the semaphore or get a reference to an existing semaphore
string name = "VB6App_" + System.Reflection.Assembly.GetExecutingAssembly().Location.Replace(":", "").Replace("\\", ""); semaphore = new System.Threading.Semaphore( MAXCOUNT, MAXCOUNT, name, out ownership); // decrement its value semaphore.WaitOne(); // if we got ownership, this app has no previous instances m_PrevInstance = !ownership; }
// the PrevInstance property returns True if there was a previous instance running // when the default instance was created
private static bool m_PrevInstance ;
public static bool PrevInstance { get { return m_PrevInstance ; } }
// return the total number of instances of the same application that are currently running
public static int InstanceCount { get { // release the semaphore and grab the previous count int prevCount = DefValue.semaphore.Release(); // acquire the semaphore again DefValue.semaphore.WaitOne(); // eval the number of other instances that are currently running return MAXCOUNT - prevCount; } }
~VB6App() { // increment the semaphore when the application terminates semaphore.Release(); } }
 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.
 Saturday, April 29, 2006
I am working at the migration of a large VB6 project and at one point I needed to drop a few methods in the VB6 app, in such a way that the methods would be "invisible" to VB.NET. This action was necessary because these methods performed similarly to a .NET native method. Obviously, I had the option to manually delete the methods after the migration, but if their number is high and if you need to run the wizard more than once on the same VB6 app (as it happens frequently, in the process of preparing the app for the migration), then deleing these portions of code each time becomes a nuisance.
Apparently, you can't instruct the migration wizard to ignore one or more pieces of code. Nevertheless, the solution is quite simple: you just need to bracket these pieces of code - entire methods or just individual statements - in a #If Win32 ...#End If block. The Win32 compilation constant - that is probably ignored by most VB developers today - was introduced when Visual Basic 4 was released, and was recognized also by VB5 and VB6 (but not by any version of VB.NET). Visual Basic 4 is the only version of this language that is available in 16-bit and 32-bit edition, and this compilation constant (together with Win16) allowed to define blocks of code that were compiled under only one of those versions, while allowing to mantain a single source code for both. Therefore, the following VB6 code:
#If WIN32 Then Private Sub Do Something(ByVal n As Integer) ' ... End Sub #End If
is correctly migrated to VB.NET by the migration wizard as follows:
#If WIN32 Then Private Sub DoSomething(ByVal n As Short) ' ... End Sub #End If
but this piece of code will be skipped over by the VB.NET compiler because the Win32 constant isn't defined under VB.NET.
This "feature" can actually create a problem if you are migrating a VB6 app that has been evolved from an older application that was originally written in VB4. In this case, in fact, it is possible that its source code contains one or more #If Win32 blocks. In most cases you *want* to migrate this code, but these portions will be ignored after the migration to VB.NET. If this is the case, you should then locate all the the occurrences of #If Win32 statements in the code and delete them before the migration.
 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.
 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! 
|
|
Get RSS/Atom Feed
Search in the blog
Archive
| | Sun | Mon | Tue | Wed | Thu | Fri | Sat | | 28 | 29 | 30 | 1 | 2 | 3 | 4 | | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | 12 | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | 25 | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Categories
Powered by: newtelligence dasBlog 1.8.5223.1
|