Professional Documents
Culture Documents
Navigating Your Way Through Visual Basic 6.0 To Visual Basic - NET Application Upgrades
Navigating Your Way Through Visual Basic 6.0 To Visual Basic - NET Application Upgrades
TABLE OF CONTENTS
Introduction.................................................................................................................................................. 4
1. Dealing with structures passed to P/Invokes containing strings ................................................................ 7
2. Dealing with structures passed to P/Invokes not containing strings .......................................................... 7
3. Dealing with the loss of the VB6 BackStyle property ................................................................................ 7
4. Dealing with passing strings directly to P/Invokes .................................................................................... 8
5. Dealing with VarPtr, ObjPtr, StrPtr, VarPtrArray, and StrPtrArray ............................................................. 8
6. More on StrPtr......................................................................................................................................... 8
7. Dealing with passing strings ByRef to P/Invokes ...................................................................................... 9
8. Dealing with fixed-length strings............................................................................................................. 10
9. Dealing with fixed-length arrays ............................................................................................................. 13
10. Dealing with RichTextBox Property Renaming ....................................................................................... 14
11. Dealing with recovering the LenB() function ........................................................................................... 15
12. Dealing with passing parameters to P/Invokes “As Any”......................................................................... 16
13. Dealing with StrConv: converting between Unicode and ANSI strings .................................................... 17
14. Dealing with AddressOf/Missing Delegate issues................................................................................... 19
15. Dealing with Dir() function warnings....................................................................................................... 21
16. Dealing with Item issues in Collections .................................................................................................. 21
17. Dealing with late-bound Object references............................................................................................. 22
18. Dealing with VB6 parameterless defaults ............................................................................................... 22
19. Dealing with VB6 Null Propagation ........................................................................................................ 23
20. Dealing with referencing Objects before they are initialized .................................................................... 23
21. Dealing with TextChanged and Resize events firing before the Form Load event ................................... 23
22. Dealing with renamed properties............................................................................................................ 24
23. Dealing with the loss of the ListCount property....................................................................................... 24
24. Dealing with changed MousePointer warnings ....................................................................................... 24
25. Dealing with changes to RECT structures .............................................................................................. 25
26. Dealing with the loss of the Initialized and Terminate events .................................................................. 26
27. Dealing with changes to Enumeration references................................................................................... 27
28. Dealing with VB6 Namespace Twips conversions .................................................................................. 27
29. Dealing with user-defined Twips constants............................................................................................. 27
30. Speeding code by removing references to the VB6 Compatibility Library................................................ 28
31. Speeding returned VB6 Namespace List Item values............................................................................. 28
32. Dealing with changed Date/Time shortcut format options ....................................................................... 29
33. Dealing with Date value conversions...................................................................................................... 29
34. Speeding Format command use in VB.NET ........................................................................................... 30
35. Dealing with Screen properties .............................................................................................................. 30
36. Dealing with On Iexpr GOTO ................................................................................................................. 31
37. Dealing with On Iexpr GoSub................................................................................................................. 34
38. Dealing with updating VB6 error trapping ............................................................................................... 34
39. Dealing with destroying Objects............................................................................................................. 35
40. Dealing with changes to Common Dialogs ............................................................................................. 36
41. Dealing with VB6.CopyArray.................................................................................................................. 38
42. Dealing with the loss of the ItemData List Object property...................................................................... 39
43. Dealing with changes to Font manipulation ............................................................................................ 40
44. Dealing with changes to Form commands.............................................................................................. 44
45. Dealing with VB6’s automatic Boolean conversions ............................................................................... 46
46. Dealing with Option Strict On issues ...................................................................................................... 47
47. Dealing with Image and Picture Object upgrades ................................................................................... 48
48. Dealing with the loss of VB6 Control Lists and how to recover their functionality..................................... 49
49. Dealing with changes to MouseMove parameter list changes................................................................. 52
50. Dealing with changes to remotely firing Button clicks ............................................................................. 52
51. Dealing with no AVI animation control in VB.NET and how to easily add a free one................................ 52
52. Dealing with changes to Resources Management .................................................................................. 54
53. Dealing with the loss of the App statement............................................................................................. 57
54. Dealing with updating default ByRef and ByVal method parameters flags .............................................. 58
Page –2–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Page –3–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Introduction
When a Visual Basic 6.0 (VB6) application is upgraded to Visual Basic .NET (VB.NET; to at least
VB2008) using the Visual Basic Upgrade Wizard, chances are that, once the upgraded application comes
up in Visual Studio or Visual Basic Express, its Task List will present you with a number of, or typically
quite a number of alerts, upgrade issues, to-dos, warnings, notes, global warnings, run-time warnings,
and design issues. DO NOT PANIC! It is not as bad as you might at first think it is. Most of them are
notices, and are only that: Notices. And virtually all of these added tagged comments can be safely
reviewed, ignored, and then deleted without fanfare. Of those that actually do require attention; most can
be solved in very short order using quick and easy edits. The numbered solution points provided in this
document will solve almost every one of your upgrade woes. These solutions will work when you are
upgrading to either the full Visual Studio.NET editions, or to the Visual Basic .NET Express Editions.
But before diving into those solutions, you should also understand exactly why all these upgrade issues exist
in the first place; why the current generation of Visual Basic has just a little bit of trouble upgrading from the
previous generation (VB6). It is not, as many detractors will most vehemently (but without any merit)
contrarily shout, because VB is now somehow broken (from my own personal point of view, I believe that it
is the other way around – VB6 was a patchwork of hacks, fixes, and upgrades that did not seem to always
follow uniform standards of syntax). But the best answer is that it is because VB.NET is now what those VB6
programmers desperately wished for VB to be (so be careful what you wish for).
When the majority of VB6 developers demanded the ability to build VB code in a common language IDE
(Integrated Development Environment), have 100% unfettered cross-language interoperability, and expecting
no less than 100% unrestricted object oriented programming language capabilities, most of them did not
have the first clue, not the slightest understanding, of the earth-shattering impact their passionate, relentless,
spittle-laced howling would have on their beloved VB. An immense host of them, mostly amateur
programmers and hobbyists, naively believed that after such a necessarily monumental undertaking, they
could simply continue on their merry little way, writing VB code exactly as they had done before, using the
very same often non-uniform syntax that they had been using before, and, oh yes, they expected there would
be a few additional commands here and there to address full class inheritance, and also allow seamless access
to methods whose source code was written in some other programming language, such as C++. They did not
grok the fact that in order to provide them with exactly what they were keenly expecting would also clearly
necessitate colossal changes to their beloved VB so that it would be a fully integrated, object oriented
environment that would also interoperate with and act exactly like the other Visual Studio languages (or other
.NET-compliant languages). This requires perfect synchronicity between all those languages; that each of
them could clearly understand and use objects from each other without the slightest misconstruction.
Though most VB6 users feel they are alone in this upgrade quandary, they most certainly are not. They have
it easy compared to some language upgrades. Try translating between C (C89; K&R C) and C99, or C++85
and C++95, or better, between upgrades of the very first high level language, FORTRAN (Formula
Translation), between FORTRAN57 and FOTRAN77, or even FORTRAN95. Upgrades bite everyone.
Regardless of whether you write something in VB.NET, C#, C++, Delphi.NET, Google Chrome, or
even IL (Intermediate Language – Microsoft’s brilliant cross-platform assembly language), their syntax
must always sync up perfectly. In fact, code written in one language should be instantly translatable into
one of the other languages based solely on the compiled IL instructions from any of those languages.
For example, suppose you wrote the following VB.NET code within Form1 so that you could emulate
the old VB6 command “ZOrder(Me, 0)”, to bring your form to the front of the window display stack:
Public Sub ZOrder(ByVal frm As Form, ByVal Position As Integer)
If (Position = 0) Then
frm.BringToFront()'use VB.NET function to bring the form to the top of the z-order
Else
frm.SendToBack() 'use VB.NET function send the form to the back of the z-order
End If
End Sub
Page –4–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This code is compiled into Microsoft Intermediate Language (MIL) as the following:
method public instance void ZOrder(class [System.Windows.Forms]System.Windows.Forms.Form frm, int32 Position) cil
managed
{
.maxstack 8
L_0000: ldarg.2
L_0001: ldc.i4.0
L_0002: bne.un.s L_000c
L_0004: ldarg.1
L_0005: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::BringToFront()
L_000a: br.s L_0012
L_000c: ldarg.1
L_000d: callvirt instance void [System.Windows.Forms]System.Windows.Forms.Control::SendToBack()
L_0012: ret
}
The above machine independent language can then be immediately translated into C#:
public void ZOrder(Form frm, int Position)
{
if (Position == 0)
{
frm.BringToFront();
}
else
{
frm.SendToBack();
}
}
Or into C++:
public: void __gc* ZOrder(Form __gc* frm, Int32 __gc* Position)
{
if (Position == 0)
{
frm->BringToFront();
}
else
{
frm->SendToBack();
}
}
Or into Delphi.NET:
procedure Form1.ZOrder(frm: Form; Position: Integer);
begin
if (Position = 0) then
frm.BringToFront
else
frm.SendToBack
end;
Page –5–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Having said that, am I a fan of VB6? Yes, I am. But will I continue to use it? I doubt it, except for
maintenance purposes, to support my customer base who have invested heavily in VB6 software and either
have no desire to upgrade, or do not currently wish to incur the expense or can afford the time of an upgrade.
But I like too much the colossal power and the magnificent freedom I enjoy while using VB.NET to want to
turn back to VB6. In fact, I have become so comfortable in VB.NET that I may now have trouble writing
original code in VB6 again, mainly because of the conflicting syntax between controls and commands that
VB6 permitted, or even required, is more complicated than the smoothly consistent syntax of VB.NET.
I have always stressed the point that, because VB6 was a serious RAD platform, a developer could write in
two hours what would take two weeks to match it in robustness using C++. I stand firm that this declaration
is now even more true of VB.NET (this explains why I have finally made a full migration from C++). By the
time VB2005 came out, VB.NET was clearly coming into its own as the new RAD platform of choice. The
fact that its free version, VB2005 Express, was a full VB.NET compiler, with its only limitations being that it
did not support direct interoperability with other .NET languages (which most developers do not bother with,
anyway), it was missing a few professional-level templates, and it lacked support of a number of other
features made available only to the full Visual Studio environment, such as the free Code Rush! Express
editor enhancements (see www.devexpress.com/Products/Visual_Studio_Add-in/CodeRushX/). It also
featured much stronger compatibility to VB6 code than VB2003 offered. VB2008 and VB2010 exhibit far
greater compatibility to VB6, and they are in fact the only VB.NET platforms that I will recommend to VB6
users who have yet to migrate to the .NET domain. Like VB2005, free VB2008 and VB2010 Express editions
are also available (see www.microsoft.com/express/download), plus Microsoft has also provided a number of
excellent, previously pricey books regarding VB6 to VB.NET migration on hand for free download (see the
end of this document for details and their web links).
On top of that, the many numbered points listed in this document will make addressing pesky warnings
and upgrade issues a snap to deal with. Not only will these points help you address those issues, but they
will also hopefully give you a glimpse into the VB.NET approach to application development, and with
any luck I will also be able to convey my excitement for this extremely potent and much more
empowering rendition of Visual Basic, which is now a true force to be reckoned with; it finally being
just as capable, just as powerful, but can be developed much faster and can be executed just as fast as the
other contenders in the development world, such as VC++.
The lists of notes in this document were initially compiled from personal references I had jotted down as I
made my own migration over to VB.NET from VB6. It started out as a 6-page hodge-podge of terse cuff
notes, but continued to grow as I learned new and better ways of adapting VB6 code to the .NET
environment, first to VB2002 (a painful experience), then VB2003 (a little less painful), VB2005 (a rather
pleasant upgrade), VB2008 (a very easy upgrade), and now VB2010. Prior to the release of VB2005,
transitioning VB6 code to the .NET platform was in most cases, except for the simplest of applications, a
dreadfully rocky path, fraught with aggravation and dead ends that often required major re-writing for
segments of code. When VB2005 arrived on the scene, I noticed that massive pages of personal upgrade
notes were now no longer required, and so they were duly discarded. When VB2008 was released, far greater
amounts of my upgrade notes were no longer relevant. Indeed, VB2008 is probably the very first edition of
VB.NET that I would even consider recommending to people who were thinking about making the transition
over from VB6. VB2005 and VB2008 were focused strongly on the language itself and upon VB6
compatibility. VB2010 is geared mostly toward ADO.NET, web development, and business transactions.
With just a little more tweaking, and I think these notes will shrink back to their original 6 pages, if not
eliminate their need entirely. We can only dream.
And now: on to those solutions…
VERY IMPORTANT NOTICE: Every solution involving GCHandles, embracing angle brackets denoting Attributes “<>”, or have
Marshaling instructions will simply assume that you will have also placed the following line in the heading of any file they are involved in;
below any Option statements, if any, but before the Class or Module declaration:
Imports System.Runtime.InteropServices
Page –6–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: You will want to change the ‘Auto’ CharSet to ‘Unicode’ if the P/Invoke instead expects 16-bit Unicode text.
Under VB6, the BackStyle was by default set to 0 (Transparent). If set to 1, its background was set to
Opaque, which allowed the color set in the control’s BackColor property to be displayed. Under
VB.NET, these two properties were combined; it was such a waste of resources to have a BackColor
property that was in most cases unused. This added to an overall heavier code overhead. Therefore, the
BackColor property, when set to a color, determined the background color of the object, and logically
forced the back style to be opaque. By setting the BackColor to the new color value Color.Transparent,
the background and back style would be logically rendered transparent.
To correct the above code, we simply delete the line setting BackStyle, because by setting the
BackColor property under VB.NET to a non-Transparent color, we have in fact set it to opaque. If,
however, you want to set it to transparent, simply set “frmMain.lblLoc.BackColor = Color.Transparent”,
though you would still delete the BackStyle setting line: “frmMain.lblLoc.BackStyle = 0”.
NOTE: Even with the BackColor set to Transparent, its background assumes the color of its parent; the form (its default parent). To
make it transparent to a control beneath it, at runtime you must set the object’s Parent to the control it is over, and adjust its Left and
Top properties to those relative to its new parent. For example, on a form you have a label (Label1) and a PictureBox (PictureBox1).
To display the label over the PictureBox with the background transparent on the image, in the Form_Load event, enter this:
'Set Label1 over Picture1, relative to their current positioning
With Me.Label1
.Parent = Me.PictureBox1 'set Label1 parent to Picture1
.BackColor = Color.Transparent 'set Label1 background color to transparent
.Left = .Left - .Parent.Left 'adjust to relative coordinates of new parent
.Top = .Top - .Parent.Top
End With
Page –7–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This alternative declaration using the Auto verb (and no Alias) is also valid:
Private Declare Auto Function GetTempPath Lib "kernel32" _
(ByVal nBufferLength As Integer, _
ByVal lpBuffer As String) As Integer 'Valid using Auto (or Unicode or ANSI)
But this declaration, specifying both Auto and Alias, is not, returning non-recognizable text:
Private Declare Auto Function GetTempPath Lib "kernel32" Alias "GetTempPathA" _
(ByVal nBufferLength As Integer, _
ByVal lpBuffer As String) As Integer 'This will generate invalid text data
NOTE: The execution of this erroneous code will not blow up or crash the computer, but your text will look as though it
had gone through a kitchen blender set on Liquefy.
6. More on StrPtr
If your VB6 code used the StrPtr() function, be aware that strings are no longer stored in BSTR
format, and if one passes a string ByVal to a function (this is true in both VB6 and VB.NET), you are
performing the exact same function as StrPtr(). Therefore, in most cases the StrPtr() function is not
actually needed in VB.NET, just as under VB6. However, if you do need to pin down and pass or
manipulate the actual address of the string, then of course refer back to the previous point.
NOTE: This addition feature of ByVal was introduced in VB2005 in order to enhance compatibility to VB6 and simplify
conversion from VB6. Previous to this version, under VB.NET you had to pass a string ByRef and include a special
VBByRefStr attribute, which will be discussed in the next point.
Page –8–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Upgrade it so that a string ByRef actually works for a P/Invoke in VB.NET like this:
<MarshalAs(UnmanagedType.VBByRefStr)> ByRef myString As String
NOTE: Passing strings ByRef, as a rule, does pass them By Reference, such as when passed to a VB.NET method, but
an interop P/Invoke is a special exception to ByRef processing, because it passes string data to an unmanaged memory
method, which VB.NET endeavors to protect itself from, and so an additional attribute is required to allow it.
Consider the following VB.NET example using a typical ByRef string:
'this function returns with lpBuffer containing the System directory and the function result is set to the string length
Declare Auto Function GetSystemDirectory Lib "kernel32" ( _
<MarshalAs(UnmanagedType.VBByRefStr)> ByRef lpBuffer As String, _
ByVal nSize As Integer) As Integer
...
Dim Sd As String = New String(Chr(0), BufSize) 'init string as a receiving buffer to get API return data
Dim I As Integer = GetSystemDirectory(Sd, BufSize) 'now get system directory to String and its length to I
Dim S As String = Left(Sd, I) 'aquire returned text as a string in variable S
In the above example, we marshaled a ByRef string using the VBByRefStr attribute.
Using A StringBuilder
As an alternative to using a ByRef string, you can instead use a StringBuilder, an object introduced
in VB2008 that is one of the fastest (200 times faster than standard string manipulation), most
powerful, and most amazing string manipulation objects you may never have even heard of. A
StringBuilder is a class defined in the System.Text namespace. One of the great thing about it, other
than awfully fast string manipulation, is that you can pass it to a P/Invoke ByVal and it will be
handled as a string. Even though sent ByVal, its member string will still receive data.
A native VB.NET StringBuilder does not require any special marshaling tags, which internet gurus
typically recommend, as shown below:
'this function returns with lpBuffer containing the System directory and the function result is set to the string length
Declare Auto Function GetSystemDirectory Lib "kernel32" ( _
ByVal lpBuffer As System.Text.StringBuilder, _
ByVal nSize As Integer) As Integer 'note the ByVal for StringBuilder. An exception results if sent ByRef
...
Dim Sd As New System.Text.StringBuilder(nSize) 'use a StringBuilder as a receiving buffer to get API return data
GetSystemDirectory(Sd, Sd.Capacity) 'get system dir to StringBuilder and its length (result no longer needed)
Dim S As String = Sd.ToString 'aquire returned text as a string in variable S
NOTE: This may seem to go against logic, but we must pass our StringBuilder ByVal to the P/Invoke, not ByRef, even
though it will receive data. The reason for this is that the Common Language Runtime (CLR) has special knowledge of
StringBuilder objects. Passing them ByVal will in fact pass a pointer to its internal string buffer. Were we to pass a
StringBuilder ByRef, it would actually be treated as passing a pointer to the pointer, because we are passing a pointer
(ByRef), and the CLR will in turn pass a pointer to it, ultimately resulting in an exception error being generated.
Using A ByVal String
A substitute to using a StringBuilder or a ByRef String is to actually use a ByVal String, as you may
have surmised from reading Point 4 (but mind you that this additional approach has only been valid since
the release of VB2005, and so it would have been flagged as an exception error under VB2002 and
VB2003). As indicated under Point 6, sending a string ByVal will in fact pass a 32-bit Integer pointer to
its base address. This is also what would happen under VB6 (passed as what it would call a 32-bit Long).
When VB.NET passes a 16-bit Unicode string to an 8-bit ANSI string and it knows that it must convert
between the string formats (either informed through the Auto modifier verb or by specifying a P/Invoke
name that VB.NET recognizes as using ANSI strings; either through an Alias or by the declared method
name, which .NET keeps track of in an internal reference library), it will set aside additional temporary
space for the converted string, pass the address of that string to the method, and then translate the ANSI
string (modified or not) back to the original Unicode string variable. Interestingly enough, this is also the
same process that passing strings ByRef and using the marshalling tags performed, as described earlier.
Page –9–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
For example, consider the following working declaration for the above GetSystemDirecty() P/Invoke
that will pass a string ByVal , but can still receive valid string data:
Declare Auto Function GetSystemDirectory Lib "Kernel32" ( _
ByVal Path As String, _
ByVal nSize As Integer) As Integer
...
'*************************************************
' GetSystemDir(): get SYSTEM directory
'*************************************************
Public Function GetSystemDir() As String
Dim sd As New String(ChrW(0), nSize) 'set aside space for string
Dim I As Integer = GetSystemDirectory(sd, nSize) 'now get system directory to string and its length to I
Return Left(sd,I) 'return system directory path
End Function
Please note that the above GetSystemDirectory() P/Invoke could have been alternately declared as:
Declare Function GetSystemDirectory Lib "Kernel32" Alias "GetSystemDirectoryA" ( _
ByVal Path As String, _
ByVal length As Integer) As Integer
The greatest advantage of this is that everything is done for you. No mess. No fuss. The disadvantage of
this is that it is more complex than we actually need it to be. Remember, VB.NET does not actually
support fixed-length strings. Though we used a FixedLengthString() method, the string is still just a
VB.NET dynamic string (it is simply a protected string, preventing accidental alteration of its size).
Alternatively, we can easily 1) dramatically reduce code overhead, 2) gain a more string-test-friendly
object, 3) make the program code execute faster, and 4) you will not have to resort to using the object’s
ToString() method just to obtain its actual string text. And that is to, in its place, simply do this:
Dim FixedLengthTest As New String(ChrW(0), 128) 'Test Code – ChrW() accepts 16-bit UShort-range values (You can also just use Chr())
You may notice that the VB.NET upgrade to the above VB6 code will have been rewritten into
something like the following Char array (the following is actually a single line, but you can trim off the
highlighted command path qualifiers if you also imported “system.Runtime.InteropServices”):
<VBFixedString(128),System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.ByValArray,SizeConst:=128)>P
ublic szCSDVersion() As Char 'Fixed-length maintenance version string
You can simplify it much further, while still provide the very same functionality and ignoring the above
conversion from a string to a fully compatible array of type Char (I want my reviewers to specifically
know that we are dealing with text here; not with array elements), by instead defining it as follows:
<VBFixedString(128)> Public szCSDVersion As String 'Fixed-length maintenance version string
Or, employing a simpler version of the above upgraded Char array, you can also use the following:
<VBFixedString(128)>Public szCSDVersion() As Char 'Fixed-length maintenance version string
Page –10–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
What is really important here is that if we read the documentation for VBFixedString, it states: “The
VBFixedStringAttribute is informational and cannot be used to convert a variable length string to a fixed string. The
purpose of this attribute is to modify how strings in structures and non-local variables are used by methods or API calls
that recognize the VBFixedStringAttribute. Keep in mind that this attribute does not change the actual length of the
string itself. The VBFixedStringAttribute specifies the length of a string in bytes, not characters.”
What does this mean to us? Mainly, it means that the VBFixedString attribute is 1) Informational and 2)
It does not allocate space. If we are new to this sort of thing and had just read the above documentation,
it may make us pause to wonder if we will need to afterward manually allocate real space for this string
after we have instantiated a copy of the structure. The quick, fast answer to that is: No, we will not. We
must understand that when the term “allocate” is used, it actually refers to initialized space. The fact is,
VB.NET will set aside space for each fixed-length string member of a structure (by proxy, essentially,
because the next pointer will point beyond where that “unallocated” string data will be located). It would
have been better to qualify that by saying that space for the string will be set aside, though it will not be
initialized space. VB.NET has all the information it needs to compute these string sizes, making any
additional allocation unnecessary, except in the rare cases when we will be required to pre-fill the string
with some sort of default text data. But as it is, VB.NET will initialize it to an empty string. Hence, we
never have to do any post-allocation, allowing us to process it exactly as we had done it under VB6.
On the other hand, if you would feel more comfortable initializing them, even though it is not necessary,
here are four very good options:
I. The first and quickest best option is to simply assign the structure to a reference variable using the New
keyword, such as “Dim Struct1 As New MYSTRUCT”. The New modifier verb will force all scalar members
(numeric fields) to be initialized to zero, and all others, such as strings, to Nothing.
II. The next option is to assign the structure to a reference variable and then initialize its string member(s).
For example, were the string variable a member of the MYSTRUCT Structure (see the structure
declaration in sub-point IV, below), then we could perform our declaration and initialization in two
lines:
Dim Struct1 As New MYSTRUCT 'declare structure variable (NEW required due to next line)
Struct1.szText = New String(Chr(0), 128) 'initialize new blank buffer (16-bit Chars will become 8-bit)
III. If you want to initialize multiple members after assigning a reference variable to your structure, you
should consider writing a method that you would be able to pass the structure variable you just declared
to, and initialize it that way. Send it ByRef, so that the members of the structure can be accessed and
updated. Otherwise, if passed ByVal, only a copy of the data will be passed, leaving the original data
unchanged. Consider this code:
'initialize fixed strings in MYSTRUCT structure after the MYSTRUCT reference is declared
Private Sub InitStructStrVar(ByRef stVar As MYSTRUCT)
With stVar
.szText = New String(Chr(0), 128)
'place other initializations here...
End With
End Sub
IV. Lastly, you may want to consider a solution based on the above method, but merging it with the
solution that the Upgrade Wizard provides for the VBFixedArray attribute (discussed in the next point,
below), and that is to take advantage of the fact that a VB.NET Structure is, in Object Oriented
Programming terms, an Abstract Class1. An Abstract Class can be seen as a class with exceptions.
Significantly, because it is a class type, it can contain methods and properties, in addition to data fields.
1
An Abstract Class would be used to declare such objects as scalars (value-type variables), and, of course, structures. Unlike the classes
that you are likely familiar with from VB6, an Abstract Class can only be assigned to one variable that can reference its actual data. When
assigning an Abstract Class to another variable, it passes a copy of itself, giving the target variable a replica of the original (much like
using the Clone() method, discussed much later in Point 41). This is juxtaposed to a Concrete Class, which is the standard class type in
VB.NET that can have instances instantiated, and a single instance can be pointed to by multiple reference variables (copying a reference
variable to another reference variable would simply copy the referenced address in the absence of the Clone method).
Page –11–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
What all of this boils down to is that after all our member variables are declared in our
structure; we can also adjoin structure-embedded methods. I typically name an imbedded
helper method Initialize(), for consistency. Consider the following:
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)> _
Public Structure MYSTRUCT
Dim dwInfoSize As Integer
Dim dwEntryId As Integer
<VBFixedString(128)> Dim szText As String 'Fixed reference string
'
' Initialize the 'fixed-length' string to the proper size
'
Public Sub Initialize()
szText = New String(Chr(0), 128)
'place other initializations here...
End Sub
End Structure
With the above, you can declare and initialize a new copy of the structure with these two lines:
Dim Struct1 As New MYSTRUCT 'The New keyword also makes sure that member variables are in default states;
Struct1.Initialize() ' meaning scalars are 0, and all else, such as strings, are set to Nothing
Do not panic if you must also provide this structure to a P/Invoke. Since a Structure is passed
ByRef to a P/Invoke (or to one’s own methods), the P/Invoke process will pass a pointer to its
data in the same old way it had done so reliably under VB6. Further, internally, data and
program code are also stored unconnectedly in separate system memory locations, so there is
no danger of this program code being passed right along with the data fields.
As an aside, if you were to take a look at the length of the structure declared in Option IV, above,
such as by using “Len(OSver)” (see Point 11 regarding VB6 and its LenB() method, which would
have been used here, instead of Len()), you will find that the returned length would report 136
(bytes). This is the size of 2 Integer variables (32-bit/4-byte), allocating 8 bytes, plus the length of
the string, which is 128 bytes long, yielding a total of 136 bytes.
Notice that I stated that the length of the string was 128 bytes. This is something very important to
remember! It is because most Win32 P/Invokes use 8-bit ANSI characters instead of VB’s 16-bit
Unicode characters. Even more, the documentation for the GetVersionEx P/Invoke I extracted this
example from is actually the GetVersionExA (ANSI) version (as declared in its Alias modifier).
Further, if you re-read the previously-stated documentation, the very last line states: “The
VBFixedStringAttribute specifies the length of a string in bytes, not characters.” Had I been using the Unicode
(Wide) version of the Kernel32.DLL function, GetVersionExW, I would have had to have stated
256 (bytes) as the designated width of the VBFixedArray attribute, in order to fully accommodate
128 16-bit Unicode characters. However, even so, do not be wracked with dread because we had
initialized our string to just 128 bytes, yet we, within our code, may be, by proxy, anyway,
associating it with a string of 128 Unicode characters. This is because the CharSet.Auto property
will tell the VB.NET interop marshaler to automatically handle the Unicode/ANSI conversion for
us, providing the required 128 8-bit ANSI characters needed during interop from the structure by the
P/Invoke.
This material seemed complicated the first time I encountered it, but it quickly became quite simple.
My personal solution is to only apply special attributes to the body of the structure when I will be
performing interop processes with it, which is to say that I will be invoking methods declared in
unmanaged or non-.NET modules. Also, I prepend any structure strings that must be of a fixed size
with “<VBFixedString(xxx)>”, specifying the number of bytes the string will occupy, or doubling it if I
am dealing with Unicode, and I typically declare structure variables using the “New” verb so that I
will know that its data fields will be initialized. When specifying fixed-size arrays in my structures, I
must also perform the additional easy step of adding a simple initialization method to my structure,
which is outlined in the following point.
Page –12–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
In the above (also remembering to marshal the structure as outlined in point 1 or 2), what we see is
that our fixed array, lfFaceName, appears to be declared to the initial size we require, which is a
value defined by a constant named LF_FACESIZE. We can leave this code alone and simply
invoke the structure’s Initialize() method after we assign the structure to a reference variable.
If we need to supply this structure to a P/Invoke, then some of you may feel that you cannot safely
keep the Initialize() method present. But rest assured that only the structure’s data fields is passed
(ByRef) to a P/Invoke. Further, a Len() function performed on the above structure will return only its
data’s byte length. But if you feel really nervous about it, try something really goofy, like deleting
the Initialize() method (I strongly advise against it).
Is it OK to not bother with or delete the Initialize() method, assuming that array initialization can be
ignored, just like simple string members can be? ABSOLUTELY NOT! The VBFixedArray
documentation clearly states “The VBFixedArrayAttribute is informational and does not allocate any storage. The
purpose of this attribute is to modify how arrays in structures and non-local variables are used by methods or API calls
that recognize the VBFixedArrayAttribute. Keep in mind that this attribute does not convert a variable length array to a
fixed array and that you must still allocate array storage using Dim or ReDim statements (underlining mine).”
Meaning? It means that the VBFixedArray attribute, just like the VBFixedString attribute, does not
actually allocate space. However, in the case of arrays, it boils down to us being required to declare
dimensions for the array after we have assigned the structure to a new field or variable, and hence;
the upgrade’s automatic addition of the Initialize() method. That means to us that after we declare a
variable to be of type LOGFONT, such as “Dim lfLogFont As New LOGFONT”, we are required to then
also dimension the lfFaceName array to size LF_FACESIZE. If we had retained the Initialize()
method, as I hope you did, then we can simply invoke it using “lfLogFont.Initialize()”. Or, if we
had suffered a brain fart and deleted it, we must manually resize our dynamic array in this form:
“ReDim lfLogFont.lfFaceName(LF_FACESIZE)”.
Page –13–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
' Declare LOGFONT structure and initialize manually (yuck; more typing for me)
Dim lfLogFont As New LOGFONT 'NEW causes all data members to be initialized to their default values.
ReDim lfLogFont.lfFaceName(LF_FACESIZE) 'initialize manually.
NOTE: A good rule of thumb to follow is this: If a structure contains an Initialize() method, then always invoke it after
you instantiate it. If it does not contain one, then no worries. But remember that in applications you later develop under
VB.NET, it is strongly recommended that you also create an Initialize() method within a structure containing any
dimensioned arrays in order to properly dimension those array members, because VB.NET will not do that for you.
NOTE: The reason that we separately allocate a fixed-sized array and not a fixed-sized string all has to do with
VB.NET’s inability to predetermine preset array pointers (which, goofy me, I think is easy, because I create them all the
time). With fixed-length strings declared with the <VBFixedString(nnn)> attribute, it can easily determine, reserve, and
point to the base of the data in a structure or class for that string. A Fixed-size array is a different thing entirely,
typically being a list or a matrix of pointers to data. Therefore, simply allocating space and pointing to its base for
arrays is not acceptable, because the structure is expecting a list or matrix of pointers, even though VB.NET can and will
reserve the space required to store all those many pointers. Call me silly, but I do not understand why VB.NET does not
simply initialize that set-aside pointer space to a list or matrix of null pointers. Some may argue that multi-dimensional
arrays is where this simplicity ends, because in such a matrix, the lower dimension arrays must always contain pointers
to lists of the array dimension immediately higher than it (in multidimensional arrays, even blank ones, all lower array
specifications in fact do contain pointer data, because it is the final dimension that specifies the actual data we typically
access). However, even so, since VB.NET already has the dimensions required, why could it not simply perform this
initialization for us, or at least auto-invoke an Initialize method during instantiation, if an Initialize method exists?
NOTE: If you require multiple dimensions, such as 32x32, adjust pertinent lines in the structure as demonstrated here:
<VBFixedArray(32,32)> Dim strMatrix(,) As String 'set aside space for 32 rows of 32 columns of strings
'(note the rank setting (,) in strMatrix)
...
ReDim StrMatrix(32,32) 'note that these parameters MUST match the VBFixedArray parameters, above
NOTE: As noted above, you can specify multiple dimensions, such as “<VBFixedArray(1023, 63)> Dim I(,) As
Integer”. Notice that we should match the values in the VBFixedArray parameter in the ReDim statement. Note also
that the dimension Rank must be included in the declared un-dimensioned variable (the commas). For example, a 3-
dimensional array such as “Dim I(20, 40, 128) As Integer” would be declared as “<VBFixedArray(20, 40, 128)>
Dim I(,,) As Integer”. Notice that the rank for “I” changed to 2 commas, implying 3 dimensions.
Notice that the source “TextRTF” property has been properly upgraded to “Rtf”, but the destination
“TextRTF” was upgraded instead to “Text”. This statement will have to be fixed by you by further
changing the “Text” property to correctly reflect “Rtf”.
Page –14–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Public Sub Initialize() 'Initialize the 'fixed-length' string to the proper size
szText = New String(Chr(0), 128) 'This method occupies no space in regard to the size of the structure
End Sub
End Structure
On the other hand, VB.NET’s Len() function now fully supports Structures and will return its full
allocated size, to include the allocated lengths of any string members.
NOTE: Allocated lengths of String members in structures refers to an actual declared size. For normal string
declarations, this would be a length of 4 (the size of an IntPtr). But if the string is declared with a prefix, such as
“<VBFixedString(128)>”, then this alters its initial footprint size to the byte size that is declared.
For individual Unicode Strings, such as “Dim S As String = "abc" : Dim I As Integer = Len(S)” you
can either double the value of the string’s length, or you can use the GetByteCount property of the
System.Text namespace’s Unicode.Encoding class.
Consider the following upgraded VB6 code:
'UPGRADE_ISSUE: LenB function is not supported. 'Click for more: 'ms-help://MS.VSCC.v90/... BLAH BLAH
CopyMemory(MnMxInfo, lParam, LenB(MnMxInfo)) 'get structure byte size
'UPGRADE_ISSUE: LenB function is not supported. 'Click for more: 'ms-help://MS.VSCC.v90/... BLAH BLAH
Dim iLen As Integer = LenB(myString) 'get string byte size
We can manually fix the above two lines by updating them to this:
CopyMemory(MnMxInfo, lParam, Len(MnMxInfo)) 'get structure byte size
Dim iLen As Integer = System.Text.Encoding.Unicode.GetByteCount(myString) 'get string byte size
'Or:
Dim iLen As Integer = 2 * Len(myString) 'get string byte size
Or better, the above fixes can be eliminated and the original code will become once again viable
simply by adding our own overloaded LenB functionality. In a module (here named modLenB), just
add these two functions:
Module modLenB
'********************************************************
' Provide the VB6 LenB Functionality – Type-Safe approach
'********************************************************
Public Function LenB(ByVal ObjStr As String) As Integer
'Note that ObjStr.Length will fail if ObjStr was set to Nothing, so use Len()
If Len(ObjStr) = 0 Then Return 0
Return System.Text.Encoding.Unicode.GetByteCount(ObjStr)
End Function
Page –15–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This method was often used to send string text as the lParam value to another control in another
application running on the computer, or to a different process, or to a different control. If there was
no text, then a value of zero was sent as lParam. Using “As Any” allowed both text and values to be
passed by the same parameter.
Under VB6, if you wanted to break this API up to support values and strings separately without
resorting to As Any, you did it by declaring two separate APIs and giving them separate names, such
as SendMessageByNum and SendMessageByStr, and manually invoking the appropriate method,
depending on what type of data you needed to send. For example:
Declare Function SendMessageByNum Lib "user32" Alias "SendMessageA" ( _
ByVal hwnd As Long, ByVal wMsg As Long, ByVal wParam As Long, _
ByVal lParam As Long) As Long
With VB.NET, you are now able to declare two separate methods, but provide them with the same
name. This way, you do not have to think about which method to use, because they will be the same,
though the compiler will be able to tell by examining the parameters, and it will select the proper
method for you. Consider the following declarations, both named SendMessage:
Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, _
ByVal lParam As Integer) As Integer
However, if you are one who still feels as though you cannot live without it, or simply feel that using
it will get around having to learn how to write overloaded methods, what are actually quite simple,
as shown above, you can “upgrade” them as follows. If the VB6 parameter was declared:
lpKeyName As Any 'not type-safe, so do not just throw any ol' thing into this stew pot
Page –16–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
13. Dealing with StrConv: converting between Unicode and ANSI strings
Normally you can use the StrConv() method as you had used it under VB6, and the upgrade will
adjust parameter-naming changes. But when converting to/from ANSI and Unicode text, you will
have to do this differently, because Unicode/ANSI conversion is not presently supported by
VB.NET’s StrConv, which now seems to be geared more toward needed foreign language handling.
To convert an ANSI Byte array (Bytes) to a Unicode String (strText), upgrade these VB6 statements:
Dim strText As String
strText = StrConv(Bytes, vbUnicode) ' convert ANSI text to Unicode text
To convert a Unicode String to an ANSI Byte array, you can upgrade the following VB6 statements:
Dim Bytes() As Byte
Bytes = StrConv(strText, vbFromUnicode) ' convert Unicode text to ANSI text
To VB.NET using the following equivalent statement (you can also use UTF7/8 instead of ASCII):
Dim Bytes() As Byte = System.Text.Encoding.ASCII.GetBytes(StrText) ' equivalent to VB6 StrConv(strText, vbFromUnicode)
NOTE: Some online techs have widely reported different VB.NET functions to achieve these ends, but their solutions
most often maintain 16-bit encoding, which is not what many users asked for (but in some cases we do need that). Let us
take a quick look at this. To convert each two consecutive byte array elements into one Unicode character, use this:
Dim strText As String = System.Text.Encoding.Unicode.GetChars(Bytes) 'convert each 2 consecutive byte array elements to Unicode Chars
Conversely, to convert a Unicode String to an equivalent Byte array, you should use this:
Dim Bytes() As Byte = System.Text.Encoding.Unicode.GetBytes(strText) 'convert each Unicode Char into 2 consecutive byte array elements
The above two methods both maintain 16-bit encoding, the first combining every two consecutive byte array elements
into one consecutive Unicode character, and the other breaking each 16-bit Character into two consecutive Bytes.
What follows is a practical example for user-side conversion between Unicode text and ANSI text:
One thing that has driven many VB.NET developers crazy has been the process of converting a Unicode
String into an ANSI string required by some WIN32 P/Invokes. In certain situations we will need to set
aside a string area in unmanaged space, copy a string to that memory area, next provide the address to
that stored text to a structure, then finally provide that structure to a P/Invoke (SHBrowseForFolder() is
the example I see most often used to exemplify this problem). Consider the following partial code clip:
' ---------------------------Declare P/Invokes-------------------------
'copy memory P/Invoke (do you see the error in the following P/Invoke declaration? It will be discussed in the text)
Private Declare Auto Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal pDest As Integer, _
ByVal pSource As String, _
ByVal dwLength As Integer)
' Allocation P/Invoke to reserve a specified byte count of unmanaged local memory space
Private Declare Function LocalAlloc Lib "kernel32" (ByVal uFlags As Integer, ByVal uBytes As Integer) As Integer
' Consider using System.RunTime.Interopservices: Dim GCptr As GCHandle = GCHandle.Alloc(x) in place of the above (DRG)
'
Private Const LMEM_FIXED As Integer = &H0 'allocated buffer location will be locked in place
Private Const LMEM_ZEROINIT As Integer = &H40 'allocated buffer will be initialized to zeros
Private Const Lflgs As Integer = (LMEM_FIXED Or LMEM_ZEROINIT) 'combine constants
'
' Deallocation P/Invoke to release unmanaged local memory space set aside by LocalAlloc P/Invoke
Private Declare Function LocalFree Lib "kernel32" (ByVal hMem As Integer) As Integer
' Consider using GCptr.Free in place of the above (DRG)
'
' This API displays a browser dialogbox, enabling the user to select a file or folder.
Private Declare Function SHBrowseForFolder Lib "shell32" (ByRef lpbi As BrowseInfo) As Integer
...
Dim udtBI As BrowseInfo 'set up structure reference variable
Dim lpmt As Integer 'used to store pointer to allocated space
'
' Build the BrowseInfo structure assigned to udtBI
With udtBI
.hwndOwner = hwndOwner 'owner handle (Me.Handle, or the handle of the form to 'parent' the dialog)
.pidlRoot = 0 'Address of an ITEMIDLIST structure, or use NULL (0) if it is not used
Dim Pmt As String = sPrompt & Chr(0) 'set local prompt copy and include a trackable null terminator in its size
lPmt = LocalAlloc(Lflgs, Len(Pmt)) 'set aside space for text (use char count of the prompt for its 8–bit ANSI length)
CopyMemory(lPmt, Pmt, Len(Pmt)) 'copy string (Pmt) to set-aside space (lPmt) for the char count of Pmt
.lpszTitle = lPmt 'set address of string stored in unmanaged memory to structure member
... 'other initializations go here...
End With
'
' browse...
Dim lpIDList As Integer = SHBrowseForFolder(udtBI) 'browse and automatically allocate resources for aquired data
'
' free set-aside unmanaged memory space created by LocalAlloc()
LocalFree(lPmt) 'free space we allocated for our custom prompt
...
Page –17–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
What we are going to quickly discover is that the prompt being displayed will consist of only the first
character of the prompt (displaying “S”, when it should display “Select Source Path”, for example).
Why? Because most developers automatically placed an Auto verb in the declaration of the
CopyMemory() P/Invoke, remembering only that we will be processing a string (Private Declare Auto
Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"). To clarify, refer to Point 4 concerning the
warning against including both the Auto and Alias verbs in a P/Invoke declaration. Further, because
VB.NET has special knowledge of RtlMoveMemory(), it will enforce Unicode to ANSI text translation if
any string is used in this method. What results is our Unicode string remains intact, and so, in binary
terms, the string begins with the Unicode character “S” (x’0083’), which is actually stored internally in
memory as x’83’ and then x’00’ (the ‘high’ byte x’00’ is listed after the low x’83’ because, internally,
memory progresses, relatively, from left to right; from low to high – even bit sequences are reversed
from how we typically imagine them as right to left – but this is all hidden from you), and because text is
processed internally as ANSI by the P/Invoke, it finds an 8-bit null text terminator following the “S”.
NOTE: Even though the Auto verb had been present, be aware that if it was retained, but no parameter contained a
string type, then the method would have worked perfectly, because strings would not be a factor in those situations.
Even though the actual solution to this problem is to simply remove the Auto verb from the
CopyMemory() declaration, those developers apparently did not figure that out. Even so, we can still
explore the useful alternative method they developed for passing 8-bit ANSI strings to P/Invokes.
One idea they came up with was to pass MoveMemory() a pre-converted ANSI string as a Byte
array instead of a Unicode string. I will also assume that the declared CopyMemory() P/Invoke will
not be used elsewhere in the current module, so I will simply redefine it to a version that will accept
a Byte array (I could have easily just added an overloaded method). Since the P/Invoke now does not
contain strings, we will remove the Auto verb (but you watching at home know that we have just
fixed the actual bug in the program, because apparently those developers were not yet aware of the
rule warning against using both Auto and Alias in a P/Invoke declaration).
'copy memory API (note that if Auto were still present in this type of declaration, it
' (would still work perfectly, because string parameters are not present)
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByVal pDest As Integer, _
ByVal pSource() As Byte, _
ByVal dwLength As Integer)
And to pass ANSI text to the CopyMemory() method, I will need to translate the Unicode text to a
Byte Array. I would therefore update my code with the following darker shaded segments:
With udtBI
.hwndOwner = hwndOwner 'owner handle (Me.Handle, or the handle of the form to 'parent' the dialog)
.pidlRoot = 0 'Address of an ITEMIDLIST structure, or use NULL (0) if it is not used
Dim Pmt As String = sPrompt & Chr(0) 'set local prompt copy and include a trackable null terminator in its size
Dim Byt() As Byte = System.Text.Encoding.ASCII.GetBytes(Pmt) 'convert Unicode String to ANSI Byte Array
lPmt = LocalAlloc(Lptr, Len(Pmt)) 'set aside space for text (use char count of the prompt for its 8-bit ANSI length)
CopyMemory(lPmt, Byt, Len(Pmt)) 'copy byte array (Byt()) to set aside space (lPmt) for the char count of Pmt
.lpszTitle = lPmt 'set address of string stored in unmanaged memory to structure member
... 'other initializations go here...
End With
NOTE: By the way, on NEW projects that involve folder and file selection dialogs, you should take the E-Z route and
simply employ the FolderBrowserDialog or OpenFileDialog controls available in the VB.NET IDE Toolbox. With these
you can set custom prompts and starting paths easily and quickly, and they require absolutely no interop code from you.
For reference, what follows is a module that will provide Unicode/ANSI string conversion:
Module modUnicodeANSI
'******************************************************
' StrConv_Uni2ANSI
' Provide conversion from Unicode string to ANSI string
'******************************************************
Public Function StrConv_Uni2ANSI(ByVal StrUni As String) As Byte()
If Len(StrUni) = 0 Then Return Nothing
Return System.Text.Encoding.UTF8.GetBytes(StrUni) 'convert Unicode string to ASCII bytes
End Function
'******************************************************
' StrConv_ANSI2Uni
' Provide conversion from ANSI string to Unicode string
'******************************************************
Public Function StrConv_ANSI2Uni(ByVal Bytes() As Byte) As String
Return System.Text.Encoding.UTF8.GetChars(Bytes) 'convert ASCII bytes to Unicode string
End Function
End Module
Page –18–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
First, click on that line to place the cursor in it, then either hit the Toggle Bookmark toolbar icon, or
select Edit / Bookmarks / Toggle Bookmark from the menu, to place a bookmark at that location.
Next, right-click “BrowseCallbackProcStr” and select “Go To Definition”. At the definition, we will
need to select the full function heading, copy it to the clipboard, then go to the beginning of the class
or module, but still within the class or module, and paste the function heading to a new line.
What we need to do with this copied heading is to edit it in order to convert it into a Delegate. For
example, if the copied function heading looked like the following:
Public Function BrowseCallbackProcStr(ByVal hwnd As Integer, _
ByVal uMsg As Integer, _
ByVal lParam As Integer, _
ByVal lpData As Integer) As Integer
To convert it to a Delegate, simply insert the term Delegate before Function (or Sub, if a subroutine),
and change the name of the method. I simply add the text “Delegate” to the end of the name, like so:
Public Delegate Function BrowseCallbackProcStrDelegate(ByVal hwnd As Integer, _
ByVal uMsg As Integer, _
ByVal lParam As Integer, _
ByVal lpData As Integer) As Integer
After that, select the text “BrowseCallbackProcStrDelegate” and copy it to the clipboard.
Next, go back to our book-marked location, right-click “lpfnCallback”, and then select “Go To
Definition”. For this example, it took me to the following line:
Dim lpfnCallback As Integer 'Address of Callback
Page –19–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
In another case, in instances where you hook and unhook window/form subclasses, intercepting the
Windows Message Queue, this requires using the SetWindowLong P/Invoke. You may want to
simplify the delegate handling process by defining multiple versions of this P/Invoke. Consider the
following code clipping, which must invoke SetWindowsLong in two different ways; one using a
Delegate (notice the use of AddressOf), and the other using a simple Integer value, as shown below:
'**************************************
' Declare our P/Invoke to hook/unhook our code to the Windows Message Queue
'**************************************
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Integer, _
ByVal nIndex As Integer, _
ByVal dwNewLong As Integer) As Integer
'***********************************************
' HookWin(): Subclass hwnd to szWndProc: Insert a hook in the Windows Message Queue to divert queue processing to szWndProc
'***********************************************
Public Sub HookWin(ByVal hWnd As Integer, ByRef PrvhWnd As Integer)
'UPGRADE_WARNING: Add a delegate for AddressOf SzWndProc. Click for more: BLAH BLAH
PrvhWnd = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf szWndProc) 'insert our hook, and save the previous one
End Sub
'***********************************************
' UnhookWin(): remove subclass hook to szWndProc: remove our function diversion and stuff the old saved invocation back in
'***********************************************
Public Sub UnhookWin(ByVal hWnd As Integer, ByRef PrvhWnd As Integer)
If CBool(PrvhWnd) Then 'if somethinging (the old windproc) to unhook...
Call SetWindowLong(hWnd, GWL_WNDPROC, PrvhWnd) 'reset previous hook by over-writing ours
PrvhWnd = 0 'indicate no held hooks set aside
End If
End Sub
'***********************************************
' Our custom subclassing method to insert into the Windows Message Queue.
'***********************************************
Private Function szWndProc(ByVal hWnd As Integer, _
ByVal uMsg As Integer, _
ByVal wParam As Integer, _
ByVal lParam As Integer) As Integer
...
After defining the needed Delegate using the technique previously outlined, say szWndProcDelegate, to
prototype our subclass method, szWndProc, some creative people have attempted to dance the Delegate
name around, which resolves to an IntPtr/Integer, and declared PrvhWnd in HookWin, the PrvHwnd
parameter for UnhookWin, and so on, to this Delegate. We might try fraught things like this as we blink
excessive sweat from our eyes while struggling desperately to stop VB.NET from incessantly barking at
us after each compile attempt with it badgering us about improper data typing; this stemming from our
myopic attempts to resolve common addressing to our P/Invoke, SetWindowLong.
But the above is making a simple problem a lot more complicated than it needs to be. Instead of
trying to adjust variable typing to suit our P/Invoke, why not think broadly and simply massage our
P/Invoke into addressing the two different ways that HookWin and UnhookWin need to invoke it?
So, after thinking peaceful Zen thoughts about fluffy puppies and kittens, and having the full power of
object oriented programming at hand, we should realize that all we really need to do is take advantage of
VB.NET’s capacity to support overloaded methods. With that in our bag of tricks, all we must do to
correct our problem is 1) create the needed Delegate for szWndProc, and then 2) define another, but
overloaded, SetWindowLong method to support the Delegate. This way we will have one version of
SetWindowLong with an Integer type to handle parameter dwNewLong, and another version of
SetWindowLong that uses our Delegate as the type for dwNewLong. With just that, suddenly this
otherwise potentially very complicated problem (well, it is if we think too narrowly) is completely
solved, as the darker shaded areas demonstrate below:
'***********************************************
' Declare our Delegate – declare it before it is referenced. This delegate resolves to a simple Integer/IntPtr
'***********************************************
Public Delegate Function szWndProcDelegate(ByVal hWnd As Integer, _
ByVal uMsg As Integer, _
ByVal wParam As Integer, _
ByVal lParam As Integer) As Integer
...
'***********************************************
' Declare our overloaded P/Invokes to hook/unhook our code to/from the Windows Message Queue
'***********************************************
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Integer, _
ByVal nIndex As Integer, _
ByVal dwNewLong As szWndProcDelegate) As Integer
' "dwNewLong As szWndProcDelegate" will be treated at the machine level as simply an Integer/IntPtr
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Integer, _
ByVal nIndex As Integer, _
ByVal dwNewLong As Integer) As Integer
Page –20–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
If you are using an actual Collection, consider changing the collection object itself, such as into one
of the System.Collections.Generic strongly-typed zero-based Collection classes. If you will only be
working with text data, then define it as a List, or, if you also require an associated Key, try using a
Dictionary (or a SortedDictionary if you would like to auto-sort the dictionary based on the Key).
For example, you could change your VB6 declaration from something like this:
Dim myCol As New Collection
To something more robust and strongly typed, like this VB.NET declaration:
Dim myCol As New Collections.Generic.List(Of String)
You can of course separate the definition from the instantiation of the collection. You can also use a
structure or class instead of String as the type of object to strongly tie this collection to. Or, you can
simplify editing even more by redefining the Collection control in your app in just 3 lines of code:
Public Class Collection 'make this OF a custom Class or Structure if you want to add more features, such as keys.
Inherits System.Collections.ObjectModel.Collection(Of String) 'replace 1-based Collection with 0-based string-typed collection.
End Class 'or you can use the KeyedCollection or ReadOnlyCollection classes of ObjectModel.
NOTE: Because a generic collection’s Remove() method will expect an object (used like a Key) to identify what object
to delete, use instead the RemoveAt() method if you wish to specify an index.
NOTE: Remember, the warning about an Item’s default property will probably be the most frequently encountered
warning in an upgrade if the VB6 project did much of anything with collections. Keep in mind that using the Item
object’s ToString() property will be the safest, easiest solution, especially if you are instead using a ListBox or a
ComboBox, but which can also use custom classes for data instead of just strings (as it had been for VB6). However, be
mindful that these new collections are zero-based, not 1-based as the default ‘classic’ Collection. If you need it to be
1-based, stuff a dummy entry as the first item, but remember that the Count property will always be one higher.
Page –21–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
When this is upgraded to VB.NET, the wizard will not know, when processing the last line, that the
object “o” is associated with a Label control, because its scope of knowledge of its surroundings is
confined strictly to each statement as it is being upgraded. As such, even though in the second line a
Label control is assigned to “o”, the Upgrade Wizard has discarded that knowledge as it rolls up its
sleeves and clears the table to begin work on the next line. The only thing it knows for sure that is
that “o” is declared as type Object. Hence, during the upgrade, it will not upgrade Caption to the
new and now-uniform property Text in VB.NET. There is no sure way that it can assume that the
actual object stored within “o” is a Label control, because it might be a user-defined class that will in
fact continue to use a Caption property. These will have to be manually, though easily repaired.
Are we clear as to what is actually passed into “obj”? Is it the Item object itself, which “obj” will
accommodate, or is it its Text data, its default property, to which “obj” is equally accommodating?
Perhaps the most commonly-used default property is the Item list when referring to a member of a
Collection. Item allows an index or key property, which fully qualifies it to be declared a default
property. You can specify “Items.Item(Index)” or “Items(Index)”, but its intent is still clear.
But having said all that, keep in mind that default properties are often processed late-bound, meaning
that processing them and determining them is a process-slowing, time-consuming practice. For the
fastest possible program execution, always early bind your code as much as possible. Also, as is
probably made evident in the previous example, you should avoid using default properties with the
Object and Variant data types in your VB6 code, because these can be difficult to resolve during an
upgrade to VB.NET, and you will likely have to further modify the code yourself after the upgrade.
Page –22–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Each of the above expressions yields a NULL result under VB6. However, under VB.NET, the statement
1+Null generates a type mismatch. Also, where VB6 had two versions of the Right function — Right$
returning a string, and Right returns a variant that could be Null — VB.NET only has one version, Right,
that always returns a string. Further, V is declared as a variant by default in VB6 (you could also use “As
Variant”). The Upgrade Wizard will change this to be cast as type Object. Also, Null, a variant type, is
not supported in VB.NET, though a database-oriented System.DBNull is.
And speaking of databases, Null propagation is commonly used in database applications, where you
need to check if a database field contains Null. In these cases you should check results against
System.DBNull, or by using the function IsNull() and performing the appropriate action based upon
the result of the test, because Null propagation is still supported in Databases.
21. Dealing with TextChanged and Resize events firing before the Form Load event
If your application controls handle TextChanged or Resize events, then you will receive a warning in
your upgrade that the TextChanged or Resize event may fire when the form is initialized (trust me; there
is no “may” about it – they WILL fire). You should check to see if these event firings will affect how
your program operates, such as causing application errors becase of resizing before their form is loaded.
It has been my experience that these events (including various Selection Change events) will always
fire before the form’s Load event executes. See point 43 concerning VB.NET’s TRLAP event firing
sequence; TextChanged, Resize, Load, Activate, Paint; Trouble Really Loves A Programmer –
though I wish Microsoft could change the sequence to a more logical LTRAP, or even LAP,
ignoring TextChanged and Resize events before the Load event is fully processed. Even if the
TextChanged and Resize event code is not fired during a Load event, it will not adversely affect
overall form size when certain controls resize due to content changes. I grant that there may be
exceptions, but the answer would still be through the solution offered in the paragraph, below.
I have found that I can eradicate all frustration resulting from the early firing of TextChange and
Resize events by declaring a Boolean flag at the procedure-level of the form, named mFormLoaded
(Private mFormLoaded As Boolean = False) that would be set to True (mFormLoaded = True) at the very
bottom of the form’s Load event. Then, as the first statement in every existing TextChanged and
Resize event I have on my form, I insert the following as each method’s first statement:
If Not mFormLoaded Then Return 'do not process this event until the form load method has completed
Page –23–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Can be corrected to (note that in a form, the System.Windows.Forms spec is not actually required):
Dim OldPointer As Cursor 'Fix it by changing the old VB6 Integer into a Cursor object reference variable
OldPointer = System.Windows.Forms.Cursor.Current
Setting the Mouse Cursor has changed a bit, but I think for the better; it has been compacted and is
now much easier to use. For the most part, it has been upgraded to use a more convenient
enumerator, Cursors (technically, System.Windows.Forms.Cursors), from which the old VB6
standbys can be set, such as Cursors.Default, Cursors.Arrow, Cursors.Cross, Cursors.AppStarting
(new; the Aero Circle), and so on.
The most significant change that you will notice is how you assign standard or custom cursors to an
object, such as the form – you now set all cursors, both custom and system-provided, to a single and
more logical Cursor property, and the old MouseIcon and MousePointer properties have
disappeared, being moot. Now to set the form cursor to the Hourglass, you would submit “Me.Cursor
= Cursors.WaitCursor” (note the new name) rather than the VB6 “Me.MousePointer = vbHourglass”.
Moreover, thanks to method overloading, you will also load it (set it) with either an existing custom
Cursor object, such as “Me.Cursor = OldPointer” (declared above), or create a new Cursor object for
it. What this means is that when you load it with a cursor object, VB.NET knows that you are setting
it to a Custom cursor, and when you set it to one of the standbys, you are setting it to an enumerator
value. Previously, in VB6, you would load a custom cursor to the current form like this:
Me.MouseIcon = LoadPicture("c:\Icons\EW_06.CUR") 'use an external file resource
Me.MousePointer = vbCustom 'use the new custom cursor object
But now, with VB.NET, you would instead use this single line:
Me.Cursor = New Cursor("c:\Icons\EW_06.CUR") 'use an external file resource
NOTE: Even though you can assign enumerator values to the Cursor property, you should still obtain the cursor object
through the Cursor.Current property. This is because even if you are using standard enumerators, such as
Cursor.WaitCursor, and you can also test for it using something like“If Me.Cursor = Cursors.WaitCursor Then”, and
although it is possible to assign the Cursor property to an Integer variable (it is returned as an IntPtr), the value will
have little apparent meaning, regardless of some texts reporting that their VB6 and VB.NET values are equivalent.
Page –24–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This presents a problem in VB.NET forms, because the Left and Right members conflict. VB.NET
upgrades resolve this by renaming the structure members Left_Renamed and Right_Renamed. As a
result, I have gotten into the habit of defining my RECT structures like this in VB6 code:
Type RECT
iLeft As Long
iTop As Long
iRight As Long
iBottom As Long
End Type
This change to VB6 code makes so much difference, because a RECT UDT upgrades cleanly to:
Structure RECT
Dim iLeft As Integer
Dim iTop As Integer
Dim iRight As Integer
Dim iBottom As Integer
End Structure
Page –25–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Otherwise, you may need to burrow through your code and correct these afterward, unless you do
not mind having items that have been tagged “_Renamed”.
26. Dealing with the loss of the Initialized and Terminate events
If you have classes containing Initialized and/or Terminate events, then after they are upgraded you
will find that the Initialized event is renamed Initialized_Renamed, and the Terminate event is
renamed Terminate_Renamed. You will also notice that a New (constructor event), and a Finalize
(destructor event) have been added to your class code. The New event will in turn invoke the
Initialized_Renamed code, and the Finalize event will invoke the Terminate_Renamed code.
Regardless of the fact that this is a workable strategy for class code upgrades, to me it looks a bit
goofy, and is too much work than just leaving the original function names alone and invoking them
from New and Finalize. As a bit of extra self-imposed work, I simply cut the code from within the
body of the Initialized_Renamed code and paste it over the top of its invocation line (replacing it)
for Initialized_Renamed within the Sub New code. I do likewise for Terminate_Renamed and Sub
Finalize. I then of course delete the empty bodies Initialize_Renamed and Terminate_Renamed.
For Example, the following VB6 Initialized method:
Public Sub Initialized()
LnkPrev = Nothing 'init links to nothing for now
LnkNum = 0 'assume base of new array list (at least for now)
Init() 'reset potential variable data
End Sub
Page –26–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The above code will compute the container height of the form (Me.Height - Me.ScaleHeight), add the
top location of the Animation1 control to set the client area height, and then add 60 twips as a buffer.
When the code is upgraded to VB.NET, it becomes the following confusing line of code:
Me.Height = VB6.TwipsToPixelsY((VB6.PixelsToTwipsY(Me.Height) - VB6.PixelsToTwipsY(Me.ClientRectangle.Height)) +
VB6.PixelsToTwipsY(Me.Animation1.Top) + 60) 'set visible height of form
This code uses the VB6 namespace (a child of the Microsoft.Windows.Compatability namespace).
Although we could leave this code as-is; all this conversion back and forth between twips and pixels
eats precious loads of time. Consider modifying it to use just pixels. First some simple math: 60
twips / 15 twips per pixel = 4 pixels. Following is our pixel-only conversion:
Me.Height = (Me.Height - Me.ClientRectangle.Height) + Me.Animation1.Top + 4 'set visible height of form
Notice that the only real differences from the original VB6 statement is that we added a 4-pixel
buffer instead of an exactly equivalent 60 twips, and since the ScaleHeight property does not exist
(the old and really confusing name given to the VB6 Client space height), we read the Height
property from the more logical ClientRectangle structure.
Page –27–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
But, knowing that my code now operates with pixels (which I prefer, and not only because of my C++
development work or from constantly interfacing from VB6 to the Win32 API), I would divide this by 15
(15 twips per pixel), and change this line to:
Public Const TWIPS60 As Integer = 4 'set twip offset for 4 pixels
To the following:
SaveSetting(My.Application.Info.Title, "Settings", "History" & CStr(Idx), VB6.GetItemString(Me.cboRecent, Idx))
Apart from the requisite parentheses that must surround all .NET method parameters, notice that the
“App.Title” property was upgraded to use “My.Application.Info.Title”, but more important to our
point, the “Me.cboRecent.List(Idx)” was upgraded to “VB6.GetItemString(Me.cboRecent, Idx)”.
This last fix ends up fully functional, but it is more work than we require. Remember that Strings are
also Objects, and in VB6 we had always supplied String text to the collection in Listboxes and
ComboBoxes. We can therefore simplify the last change to “Me.cboRecent.Items(Idx).ToString”.
NOTE: You may have noticed that ListBox and ComboBox controls are now syntactically aligned with Collections, all
implementing System.Collections.IList. As such, all collection-type controls have an Items array that accepts data of
generic type Object, and have a Default property of Item. Hence, we no longer have collection-type controls that have
an Items() collection in one control and a List() collection in another.
2
DLL Hell: a term referring to DLL versioning nightmares, where some un-cautious installers, or unwitting users, do not
check versioning and overwrite newer COM-based DLLs with older, less feature-filled versions of the DLL.
Page –28–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The first two lines emulate the VB6 method. The third emulates this in VB.NET without any helper
functions (I have also seen this same solution written in some on-line VB.NET code as “netDate =
Date.FromOADate(Fix(netDate.ToOADate()))”, which accomplishes the same task, but eats more time).
Page –29–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
However, we can simplify this code in 3 ways: 1) if we are in a form, then the
System.Windows.Forms namespace is already loaded, so we can cut that from the code. 2) We are
now working in Pixels, so we can remove all Twip conversions. 3) In VB.NET we can combine
declaration with assignment. All these options give us this much shorter and simpler code:
Dim ScrHeight As Double = Screen.PrimaryScreen.Bounds.Height 'you may want to set this to an Integer
NOTE: In case you were not aware of it, adding or removing “System.Windows.Forms” in the above does not lengthen
or shorten the final compiled code one bit. This framework mapping simply allows the compiler to zero in on target
methods. Once a target is determined, the compiled code does not need to calculate the address of a method or class or
enumerator each time it is accessed; it is already known, so when the preprocessing is finished and the actual output
code is generated, absolute addressing has already been established.
Page –30–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
By moving the scattered code to within this block, we have contained it. Also, when each routine
completes its tasks under its Case heading, it immediately transfers control, not falling into the next
Case block as it does in C/C++, but directly to the End Select, where program flow continues (VB
code contains an invisible embedded C/C++ Break command at the end of each case block, ultimately
translating to either a hidden Goto or jump statement).
If you cannot easily adapt your code to this format, then you are guilty of
writing the infamous “spaghetti code”, which was an old software engineering
term used to describe procedural programs whose logic went all over the
place, like spaghetti on a plate. The advent of languages such as ADA, C, and
Pascal were supposed to provide developers with the means to avoid writing
spaghetti code. It was not until .NET Framework’s introduction of VB.NET
and C# where everything is virtually forced to be encapsulated and
modularized (managed), and likewise (hopefully) forced developers to write
modular, logical code for them to even work on these platforms (I say
virtually, as you will very soon understand).
Many amateur programmers have often made the claim to me that it is
impossible for some spaghetti code be written any other way. It has been my
long experience that their excuse is a load of horse pucks, and that by
rewriting the code in a modular fashion it will also make debugging that code
easier. Therefore, if you do not want to re-write the code cleanly, then you
should consider leaving it in VB6. A good example that is frequently cited is
the Shell-Metzner Sort algorithm. I am shown code, similar almost to an
instruction, to that which I had once found in a Creative Computing Magazine
in the 1970s, back in the day when TRS-80 was King and a 36-bit PDP-10
was the envy of university computer science departments. It was described as shown to the above-right:
The program written to support it was like the following, though here is code that, believe it or not,
even VB.NET will accept and executes flawlessly, and all without a single complaint (provided that
Option Strict is turned off, Option Explicit is turned off, and Option Infer is turned on):
Page –31–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This subroutine is simply the previous program flowchart translated to old DOS BASIC as much as
possible. Back then, you could specify variables at a whim, never having to DIM them or define
their type (the origin of Variants in VB), as we have emulated with Option Infer On. Lines
beginning with “L” create labels for what would have been line numbers. Now you might appreciate
why flowcharts were so popular back then, because the actual program code is convoluted and hard
to follow without doing what I did back then, which was to take a pencil and draw dividing lines, as
well as arrowed lines from the GOTO locations to their destinations.
I have been told repeatedly that this routine cannot be structured because the GOTO instructions go
in too many directions and places that cannot be logically blocked. But when I look at the flow chart,
I am seeing loops and blocks and IF…ELSE segments of code. I first wrote the following structured
translation long ago in FORTRAN, then in C++, then in QuickBASIC (predecessor to VB1), then in
VB6, and finally in VB.NET. Following is a segment of a string sort, comparing the original with
the current (some variables have already been declared by this time, which we will ignore for now):
' sort initialization Original Algorythm (1-Based)
' ----------------------------
NumberofItems = m_MyCount 'get number if items to sort (N=Number of Items)
HalfDown = NumberofItems 'number of items to sort (M=N)
'
' perform the sort
'
Do While CBool(HalfDown \ 2) 'while counter can be halved A: IF(M\2)=0 THEN STOP
HalfDown = HalfDown \ 2 'back down by 1/2 (M=M\2)
HalfUp = NumberofItems - HalfDown 'look in upper half (K=N-M)
IncIndex = 0 'init index to start of array (J=1)
Do While IncIndex < HalfUp 'do while we can index range
IndexLo = IncIndex 'set base B: I=J
Do
IndexHi = IndexLo + HalfDown 'if (IndexLo) > (IndexHi), then swap C: L=I+M
If StrComp(StrAry(IndexLo), StrAry(IndexHi), _
CompareMethod.Text) = CompFlag Then ' IF D(I)>D(L) THEN GOTO D
Tmp = StrAry(IndexLo) 'swap string items T=D(I)
StrAry(IndexLo) = StrAry(IndexHi) ' D(I)=D(L)
StrAry(IndexHi) = Tmp ' D(L)=T
IndexLo = IndexLo - HalfDown 'back up index I=I-M
Else ' IF I>=1 THEN GOTO C
IncIndex += 1 'else bump counter D: J=J+1
Exit Do ' IF J>K THEN GOTO A
End If ' GOTO B
Loop While IndexLo >= 0 'while more things to check
Loop
Loop
Page –32–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: Most VB.NET lists and arrays already have a built-in sort method that employs the QuickSort algorythm that
you can invoke by selecting Array.Sort(strArray), for example. The advantage here is that a sort method is already
present and easy to access. However, the above Shell-Metzner sort is much faster than QuickSort, significantly so in
large lists, and uses fewer replacements. The above version can also sort in descending order, if you wish it.
For your amusement, here, though incomplete, is the guts of the QuickSort used by .NET:
Friend Sub QuickSort(ByVal left As Integer, ByVal right As Integer)
Do
Dim low As Integer = left
Dim hi As Integer = right
Dim median As Integer = Array.GetMedian(low, hi)
Me.SwapIfGreaterWithItems(low, median)
Me.SwapIfGreaterWithItems(low, hi)
Me.SwapIfGreaterWithItems(median, hi)
Dim y As Object = Me.keys.GetValue(median)
Do
Try
Do While (Me.comparer.Compare(Me.keys.GetValue(low), y) < 0)
low += 1
Loop
Do While (Me.comparer.Compare(y, Me.keys.GetValue(hi)) < 0)
hi -= 1
Loop
Catch exception1 As IndexOutOfRangeException
Throw New ArgumentException(Environment.GetResourceString("Arg_BogusIComparer", _
New Object() {y, y.GetType.Name, Me.comparer}))
Catch exception As Exception
Throw New InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), exception)
Catch obj1 As Object
Throw New InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"))
End Try
If (low > hi) Then
Exit Do
End If
If (low < hi) Then
Dim obj3 As Object = Me.keys.GetValue(low)
Me.keys.SetValue(Me.keys.GetValue(hi), low)
Me.keys.SetValue(obj3, hi)
If (Not Me.items Is Nothing) Then
Dim obj4 As Object = Me.items.GetValue(low)
Me.items.SetValue(Me.items.GetValue(hi), low)
Me.items.SetValue(obj4, hi)
End If
End If
If (low <> &H7FFFFFFF) Then
low += 1
End If
If (hi <> -2147483648) Then
hi -= 1
End If
Loop While (low <= hi)
If ((hi - left) <= (right - low)) Then
If (left < hi) Then
Me.QuickSort(left, hi)
End If
left = low
Else
If (low < right) Then
Me.QuickSort(low, right)
End If
right = hi
End If
Loop While (left < right)
End Sub
Page –33–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
As you can see, invoking a selection of subroutines is a natural choice for a Select…Case block.
When a subroutine returns, its control will not fall into the next Case block, but will go directly to
the End Select, where program flow will continue.
The preceding can easily be adapted to the following Try…End Try block:
Private Function ReadFile(ByVal FilePath As String) As String()
Dim fso As New FileSystemObject 'use COM object IWshRuntimeLibrary. We should upgrade this to a StreamReader
Dim ts As TextStream 'We really should upgrade all this to a faster System.IO.StreamReader
NOTES: You cannot mix VB6-style error trapping and Try…End Try error trapping within the same block of code (one
type in one place, and the other in another place). Choose one or the other for the block. Also, the optional Finally block
segment can precede the End Try statement, but following the last, or only Catch block, holding code that will follow
Try and Catch, regardless of there being errors or not.
There is a lot more to the Catch statement than meets the eye. You can also catch multiple
exceptions by applying multiple Catch phrases, each Catch phrase encapsulating its own type of
error. For example, by adding a ‘When’ clause to a Catch phrase we can narrow down errors. This
way we could specifically trap “File Not Found” errors if we wanted to, and trap all other errors in
another generic block. With that in mind, we can replace the above Catch block with:
Page –34–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Each Catch phrase should have its own unique Catch exception filter variable (use a blank Catch line
–a Catch phrase without an exception parameter– if it should catch everything, but you will not need
to process an exception variable). Also, always place the generic “catch-all” trap as the last in the
list, otherwise it might execute before any narrower traps that might also be present are checked.
You can also add an optional Finally block to the bottom of the Try block (before “End Try”). A
Finally block is always executed when execution leaves any part of the Try statement, regardless if
there were errors or not. Although in my examples I used “Return Nothing” to exit from the traps, I
did this because there was nothing else to do. However, a trapped error in no way whatsoever means
that continued processing is not possible.
You can also have Try statements that have a blank Catch block, because it might not matter if
errors were generated or not. However, remember that the Try statement must contain at least one
Catch block, even if the Catch block is empty.
For example, you could have something like this:
Try
'Insert commands you do not care what happens in here
Catch 'blank Catch block, which is immediately transferred to during any exception error in -Try-
End Try
This would be like using “On Error Resume Next” at the beginning of such code, and is also like ending
the block with “On Error Goto 0” in VB6.
Page –35–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
After an upgrade to VB.NET, our code block looks something like this mini-nightmare:
'UPGRADE_WARNING: CommonDialog variable was not upgraded
With frmSpellCheck.CommonDialog1
'UPGRADE_ISSUE: Constant cdlOFNLongNames was not upgraded.
'UPGRADE_ISSUE: MSComDlg.CommonDialog property CommonDialog1.Flags was not upgraded.
.Flags = MSComDlg.FileOpenConstants.cdlOFNLongNames
'UPGRADE_ISSUE: Constant cdlOFNExplorer was not upgraded.
'UPGRADE_ISSUE: MSComDlg.CommonDialog property CommonDialog1.Flags was not upgraded.
.Flags = MSComDlg.FileOpenConstants.cdlOFNExplorer
'UPGRADE_WARNING: MSComDlg.CommonDialog property frmSpellCheck.CommonDialog1.Flags was upgraded to
frmSpellCheck.CommonDialog1Open.CheckFileExists which has a new behavior.
'UPGRADE_WARNING: MSComDlg.CommonDialog property frmSpellCheck.CommonDialog1.Flags was upgraded to
frmSpellCheck.CommonDialog1Open.CheckPathExists which has a new behavior.
.CheckFileExists = True
.CheckPathExists = True 'MUST EXIST
.DefaultExt = "txt"
.FileName = vbNullString
'UPGRADE_WARNING: Filter has a new behavior.
.Filter = "Text File (*.txt)|*.txt"
.Title = "Open an Existing Text File"
'UPGRADE_WARNING: The CommonDialog CancelError property is not supported in Visual Basic .NET.
.CancelError = True
On Error Resume Next
.ShowDialog()
If CBool(Err.Number) Then Exit Sub
On Error GoTo 0
TxtFile = Trim(.FileName)
If Len(TxtFile) = 0 Then Exit Sub
End With
Although this may look messy, it is actually quite easy and pain-free to clean up. First, the single
CommonDialog control available to VB6 users has (finally) been broken up into five separate
dialogs (which I think they should have been in the first place, considering that the system interface
had kept them separate since, if I recall, Windows 3.0), called OpenFileDialog, SaveFileDialog,
ColorDialog, FolderBrowserDialog, and FontDialog. By default, the Upgrade Wizard will add
“Open” to the end of the CommonDialog control’s name, and the CommonDialog control will be
upgraded to a FileOpenDialog type control. In my case, my CommonDialog1 control (now an
OpenFileDialog object) is now named CommonDialog1Open.
To fix the above problems, I first rename the erroneous CommonDialog1 as CommonDialog1Open.
The reason that this name was not automatically changed in the code to the new control by the
Upgrade Wizard was because the wizard was not sure if you actually wanted one of the other four
dialog controls. This way you will have to apply your personal touch to the control before your
application can actually run. By the way, if you do want to save the file, here or elsewhere, you will
need to add a SaveFileDialog control to your form. If it is an upgraded VB6 project, you might
consider some naming uniformity, so you would rename the new SaveFileDialog1 to
CommonDialog1Save, for instance. This also brings the dialog controls up sequentially on an
Intellisense dropdown list.
Page –36–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The next thing you should be aware of is that the Flags property has been replaced by individual
Boolean properties; and each enumerator you had applied to it are now set on separate lines; the
actual Flags property replaced by the fitting property allied with the control it is being used with.
But if an applied flag is no longer recognized for that application of the control, then it is simply set
to the now-non-existing Flag property, plus an error warning is issued for each one. If a property is
still being assigned to Flags, it is a safe bet that you can just delete them. That clears another hurdle.
You are next told that the properties CheckFileExists and CheckPathExists have been upgraded to a
new name and a new behavior. The new names are obvious, but the only new behavior is that they
are now individual Boolean properties and they are set to True or False instead of having an
enumerated constant applied. You can ignore the warnings, so simply delete the warning comments.
The next thing we look at is that the filter has a new behavior. This one originally had me confused,
because the formatted text we place into it is clearly the same as we did before in VB6, so I simply
ignore this warning and have yet to have trouble with it. By the way, in case you are new to dialog
filters, we can add filters in pairs, where the first half is purely descriptive, having nothing to do
with actual filtering, such as “Text File (*.txt)”, and the second part is the actual filter pattern, such
as “*.txt”. We combine these by separating them with a pipe “|” character, rendering “Text File
(*.txt)|*.txt”. You can also select alternative selection filters, which the user can select from a
dropdown list, by appending more Description | Filer pairs, separating them from the others with a
pipe character as well. You can even combine multiple filters into a single choice, such as “Image
Files (BMP, JPG, PNG)|*.bmp;*.jpg;*.png”. We separate the patterns in the filter pattern portion with a
semicolon (the descriptive portion is as you choose, but a comma separator is traditional).
The last warning we run into tells us that the CancelError properties is not supported in VB.NET.
The way we deal with it in VB6 is pretty much as shown in our VB6 example. I use the On Error
Resume Next, because when CancelError was set to True, if the user selected Cancel, a Cancel
Exception Error was thrown. The On Error Resume Next prevented the program from aborting on
this error, and then all we had to do is check if Err.Number was non-zero (under VB6, a non-zero
value would be automatically cast to a Boolean, eliminating the need for Cbool(Err.Number), but
that is actually a very bad habit to get into, and VB.NET does not like it when Option Strict is turned
on). We then clear the error trap with On Error Goto 0. Although this type of error trapping is still
accepted, one really should get into using VB.NET’s much superior Try…Catch…Finally format (I
have been using it since VC++ in Visual Studio 6, and it is a treasure).
However, we can completely eliminate the error trapping or setting the now unsupported
CancelError flag. All we need to do is catch the returned value directly from the ShowDialog()
function. If we were testing for multiple replies, such as checking for Abort, Cancel, Retry, Ignore,
Yes, No, OK, or None, I might consider placing it in a Select…Case block, such as:
Select Case .ShowDialog()
Case Windows.Forms.DialogResult.Abort 'if we are doing this within a form, we can shorten this
'Do something here 'to DialogResult.Abort
Case Windows.Forms.DialogResult.Retry 'can be shorted to DialogResult.Retry
'Do another thing here
Case Windows.Forms.DialogResult.Cancel 'can be shorted to DialogResult.Cancel
Exit Sub
End Select
But since we are simply checking for a user Cancel, we can replace all this VB6 code:
.CancelError = True
On Error Resume Next
.ShowDialog()
If CBool(Err.Number) Then Exit Sub
On Error GoTo 0
Page –37–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
With all that quick work (which you will find easier and faster to deal with as you get a few under
your belt), we will quickly end up with the following block of problem-free code:
With frmSpellCheck.CommonDialog1Open
.CheckFileExists = True
.CheckPathExists = True 'MUST EXIST
.DefaultExt = "txt"
.FileName = vbNullString 'in VB.NET, this is the same as 'Nothing'
.Filter = "Text File (*.txt)|*.txt"
.Title = "Open an Existing Text File"
If .ShowDialog() = DialogResult.Cancel Then Exit Sub
TxtFile = Trim(.FileName)
If Len(TxtFile) = 0 Then Exit Sub
End With
When this code is upgraded, 3 lines are changed (marked with a darker shading, above):
sNds = VB6.CopyArray(.Item(Index).GetAllMarked()) 'recurse through each for all marked
Return 0
These work fine, until you set Option Strict to On (I am a strict typing nut from my many years as a
FORTRAN/C/C++ software engineer). Afterward, they are all flagged in error. The last one simply
has to be changed to return Nothing in order to fix it. The other two report that “Option Strict On
disallows implicit conversions from System-Array to '1-dimensional array of DynamicNodes.dynNode'”.
Page –38–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
If we look at the disassembly of the VB6.CopyArray() function (thanks to the excellent utility .NET
Reflector from RedGate Software (http://Reflector.Red-Gate.com)), we find this simple code:
Public Shared Function CopyArray(ByVal SourceArray As Array) As Array
If (SourceArray Is Nothing) Then
Return Nothing
End If
Return DirectCast(SourceArray.Clone, Array)
End Function
The way to address this issue is simple: in cases where we are passing a freshly constructed array, as
we are in our recursion example, you can simply remove the VB6.CopyArray() function from our
upgraded VB code, but keeping its parameter contents (“sNds =.Item(Index).GetAllMarked()” and
“Return Nds”). However, if you are indeed passing a copy of an existing array, then apply the “.Clone”
method to the source array being passed and cast it to the proper type (i.e., “DirectCast(Nds.Clone,
dynNode)”).
The Clone method would be required because all arrays are reference types in VB.NET. Thus, the
debug output from the following small program will yield “Zero”, not “First” (To fix it, change “Dst
= Src” to “Dst = DirectCast(Src.Clone, String())”):
Module modTestStringCopy
Sub Main()
Dim Src() As String = {"First", "Second", "Third"} 'init first string
Dim Dst() As String 'declare second
Dst = Src 'get src data to dst
Src(0) = "Zero" 'change src array
Debug.Print(Dst(0)) 'see if Dst also changed
End Sub
End Module
NOTES: The Clone method returns a generic type Object. Also, be aware that although simple strings are arrays of
type Char, and are therefore reference types, copying a simple string from one variable to another would normally
require the Clone method, but VB.NET will take care of this for us internally, eliminating our need to address it.
Consider this unnecessary, but working VB.NET code:
Dim Src As String = "123" 'initialize simple source string
Dim Dst As String = DirectCast(Src.Clone, String) 'apply copy to Dst (more work than required. Just use 'Dim Dst As String = Src')
Src = "ABC" 'change source data
Debug.Print(Dst) 'check result; it will report "123", even if we used 'Dim Dst As String = Src'
Because VB.NET actually handles the cloning of simple strings, you can change the declaration of Dst to “Dim Dst As
String = Src”, emulating what is done in VB6. Further, be aware that the Clone method greatly simplifies what is
required in C++. Be aware that all this is actually done in VB6, but “behind the curtain”. It had to be brought more out
into the open for reasons of cross-language operability, but the process is still simplified enough in VB.NET to not
remove its RAD (Rapid Application Development) moniker. Copying strings in C/C++ can be a big bother, and almost a
nightmare to copy complex arrays. VB.NET still has it way too easy.
42. Dealing with the loss of the ItemData List Object property
VB.NET no longer supports the simple ItemData property of a ListBox or ComboBox. In VB6, the
ItemData property could be set at design time in the Properties window to associate an Integer with
a ListBox or ComboBox item, often used to associate an ID number with a text string. In VB.NET,
the ItemData property no longer exists, but this is due to the more powerful functionality given to
Listboxes/Comboboxes, where list items are now able to be of any object, not just as simple text.
In the upgrade from VB6 to VB.NET, you may notice that the upgrade uses the VB6.SetItemData()
method to initialize any design time ItemData information, usually invoked in the constructor of the
parent form (Public Sub New). To access the ItemData information, it uses the VB6.GetItemData()
method to emulate the functionality of the lost VB6 ItemData property. For example, “Dim I As
Integer = VB6.GetItemData(List1, List1.SelectedIndex)”. To obtain the text from ItemData, it used
VB6.GetItemString(), as in “Dim Result As String = VB6.GetItemString(List1, List1.SelectedIndex)”.
Because I personally want to avoid using code-heavy helpers and get rid of the VB6 Compatibility
Library reference altogether (the reference to the Microsoft.VisualBasic.Compatibility namespace),
and I would simply rather write my applications in native VB.NET code, I would instead extract my
string using “Dim Result As String = List1.Items(List1.SelectedIndex).ToString()”. However, it does
Page –39–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
not emulate all that the VB6 helper did, which, if you looked at the VB6 class’s source code using
Red-Gate Software’s .NET Reflector (http://Reflector.Red-Gate.com), it is quite extensive, and eats
up a lot of precious time (though I will give it kudos for providing a solution to the issue).
So what I want to do, at the cost of only a little (re-usable) work, but with the benefit of involving
significantly less overall code and overhead, I would first create a simple ItemData class, such as the
following, which is based on the ListBoxItem class used within the VB6 helper:
Public Class ListItem
'-------------------------------
' Field Data - You can store more than just these fields here
'-------------------------------
Public ItemString As String 'Text data for Item
Public ItemData As Integer 'You can also declare this as String, if you want a string key.
'-------------------------------
' Custom constructor; used to actually add data to a Listbox or Combobox.
' You are not limited to adding just one or two items, or of just these types
'-------------------------------
Public Sub New(ByVal ItemString As String, ByVal ItemData As Integer)
Me.ItemString = ItemString ' You may want to add more string parameters
Me.ItemData = ItemData ' and then combine them with a space separator in ToString
End Sub ' or even via a custom reporting method. Limitless possibilities!
'-------------------------------
' Constructor for assigning just text and no ItemData
'-------------------------------
Public Sub New(ByVal ItemString As String)
Me.New(ItemString, 0)
End Sub
'-------------------------------
' Provide a text data property to override the useless default in the Item() object.
' you can also use this to combine stored items when more than one text item is added.
'-------------------------------
Public Overrides Function ToString() As String
Return Me.ItemString
End Function
End Class
I would assign new text and indexes to my Listbox named ListBox1 using something like this:
Me.ListBox1.Items.Add(New ListItem("David", 50159))
But extracting the index trades off with slightly more work, but it is still simple enough:
Dim Idx As Integer = DirectCast(Me.ListBox1.Items(Index), ListItem).ItemData
Because Items(Index) returns a generic object, we have to take the extra step of directly casting it to
the type that we know it contains, which is a ListItem object. But if you have a lot of these to punch
into the keyboard, this can result in a lot of typing. But even so, we can still employ less typing and
much less code overhead than using VB6.GetItemData() and VB6.GetItemString() simply by
writing our own little helper function, which we can add to a small module at the end of the class:
Public Function ExtLI(ByRef Obj As Object) As ListItem
If TypeOf Obj Is ListItem Then 'if the object is type ListItem
Return DirectCast(Obj, ListItem) 'return the object as a ListItem
End If
Return Nothing 'else return a null object
End Function
Using the above helper function, you can now obtain the item data using the following:
Dim Idx As Integer = ExtLI(Me.ListBox1.Items(Index)).ItemData
And to get the item string, we can use either of the following two methods:
Dim SName1 As String = Me.ListBox1.Items(Index).ToString 'Method 1 example
Dim SName2 As String = ExtLI(Me.ListBox1.Items(Index)).ItemString 'Method 2 example
VB.NET code over to C# or C++, the transition is much smoother, because this process is exactly
what you would have to do in those environments. The same now applies when transitioning C# and
C++ code to VB.NET. As a C++ developer, I am very much used to and happy with this approach.
When we upgrade a VB6 program to VB.NET, the VB6 Compatibility Library takes care of these
processes for you. But you may actually be wondering how would you do it in a new program? It is
surprisingly simple. All you have to really do is create a new font that can be based on the old one
you are replacing, and then apply a new state to it. For example, suppose Label1 has a size of 10
points, and you want to simply set it to 12 points. You might do something like the following:
Me.Label1.Font = New Font(Me.Label1.Font.Name, 12, Me.Label1.Font.Style, GraphicsUnit.Point, Me.Label1.Font.GdiCharSet) 'NOTE:
Overloaded methods actually allow the last 2 parameters to be dropped
What we did was simply create a new font using the old font’s name, style, character set, as well as
set it to using Points (default), and with a new size (12). Of course, you may not want to change it if
it is already set to the desired size. We can accommodate this with a single function (be sure to also
import the System.Drawing namespace in the heading of the file):
'***************************************************************************************************************
' FontChangeSize - Set/reset Selected Font Size
' CurrentFont = font reference to change
' NewSize = point size to set
'***************************************************************************************************************
Public Function FontChangeSize(ByVal CurrentFont As Font, ByVal NewSize As Single) As Font
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) = Math.Round(CDbl(NewSize), 2)) Then
Return CurrentFont
End If
Return New Font(CurrentFont.Name, NewSize, CurrentFont.Style)
End Function
NOTE: In case you did not notice it, we can borrow font settings from other controls that are similarly set, or already
set to the desired format. We can even do this: “Me.Label1.Font = New Me.TextBox1.Font”.
To change the fonts Bold, Italic, Underline, or Strikethrough state is even easier, due to other
overloaded prototypes for Font. Suppose I wanted to set Label1 to have a Bold style, I might try this:
Me.Label1.Font = New Font(Me.Label1.Font, FontStyle.Bold) 'base on an existing font, but alter style
This works great, but toggling is a bit of a problem, especially if we want to turn on or off a single
style option, but leaving others alone. The best approach is to test if a change is needed, and then if it
is, to apply the appropriate state. We can do that with another simple function, like the one below:
'****************************************************************************************************
' FontChangeStyle - Support method for Bold, Italic, Underline, StrikeThrough set/reset
'****************************************************************************************************
Private Function FontChangeStyle(ByVal CurrentFont As Font, _
ByVal StyleFlag As System.Drawing.FontStyle, _
ByVal SetStyle As Boolean) As Font
'mask desired style against current (do this in case multiple selections)
Dim fntStyle As FontStyle = (CurrentFont.Style And StyleFlag)
'set flag to true if selected style is set, or if all selected styles are already set
Dim flag As Boolean = (fntStyle = StyleFlag)
'if not EXACT match because something IS different, then FORCE change
If Not flag AndAlso (fntStyle <> FontStyle.Regular) Then flag = Not SetStyle
If (flag = SetStyle) Then 'if nothing will change, then simply return the current font
Return CurrentFont
End If
'define a new style value minus the current selection(s), based on current font style settings
Dim newStyle As FontStyle = DirectCast(CurrentFont.Style And Not StyleFlag, FontStyle)
If SetStyle Then 'are we setting the new style(s)?
newStyle = (newStyle Or StyleFlag) 'yes, so set new style(s) to new font
End If
Return New Font(CurrentFont, newStyle) 'return new font based on current, w/selection set/reset
End Function
In the above function, we set the state of flag to True if the style we selected, such as FontStyle.Bold,
is already set. We then check it against the SetStyle flag. If we want to set bold (SetStyle = True), but
bold is already set (flag = True), then there is nothing to do and the old font is returned. If the state of
SetFlag does not equal flag, then we have something to do. We first define a local variable that will
contain the style flags without our selected style, in case we will be toggling it off (SetStyle = False).
We then check the SetStyle value, and if it is set to True then we will apply the selected style to the
Page –41–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
flag value. Finally, we define a new font based on the old one, and apply the new style change to it.
The great thing about this flag is that we can set multiple styles at once. Suppose we want to set both
Bold and Italic styles to the label. We can do this by using this command “Me.Label1.Font =
FontChangeStyle(Me.Label1.Font, FontStyle.Bold Or FontStyle.Italic, True)”. To toggle a selected style
off, we simply change the state of the SetStyle parameter to False.
The last thing we might want to do is to change the actual font. Suppose we wanted to change the
font of our label to Courier New. We could do this:
Me.Label1.Font = New Font("Courier New", _
Me.Label1.Font.Size, _
Me.Label1.Font.Style)
But this is a lot of work if we have to do a lot of it. We can instead create a simple little function that
will do all the dirty work for us, such as this:
'****************************************************************************************************
' FontChangeName - Set/reset Selected Font Name
' CurrentFont = font reference to change
' NewName = new font family to change it to
'****************************************************************************************************
Public Function FontChangeName(ByVal CurrentFont As Font, ByVal NewName As String) As Font
If (CurrentFont.Name = NewName) Then
Return CurrentFont
End If
Return New Font(NewName, _
CurrentFont.SizeInPoints, _
CurrentFont.Style)
End Function
With this function, all we have to do to change the font to Courier New is execute this statement:
Me.Label1.Font = FontChangeName(Me.Label1.Font, "Courier New")
But suppose we want to set an entirely new font, or change more than one property, such as the font size
and the style. We could write, as I have, a function to do all this, but in the end, probably the easiest
method is to simply define a new font on the spot, just as had been demonstrated at the beginning of this
point. For example, suppose I want to change the font of Label1 to Courier New, the point size to 12, and
the style to Bold and Italic. All I would have to do is this:
Me.Label1.Font = New Font("Courier New", 12, FontStyle.Bold Or FontStyle.Italic)
Or, if we will be doing a lot of this, perhaps a full function is in order after all. Here is my complete
module, which is based on the font support provided by the VB6 Compatibility Library:
Imports System.Drawing
Module modFontChanges
'***************************************************************************************************************
' FontChangeBold - Set/reset Selected Font Bold
' CurrentFont = font reference to change
' Bold = True:Set, else reset
'***************************************************************************************************************
Public Function FontChangeBold(ByVal CurrentFont As Font, _
ByVal Bold As Boolean) As Font
Return FontChangeStyle(CurrentFont, _
FontStyle.Bold, _
Bold)
End Function
'***************************************************************************************************************
' FontChangeItalic - Set/reset Selected Font Italic
' CurrentFont = font reference to change
' Italic = True:Set, else reset
'***************************************************************************************************************
Public Function FontChangeItalic(ByVal CurrentFont As Font, _
ByVal Italic As Boolean) As Font
Return FontChangeStyle(CurrentFont, _
FontStyle.Italic, _
Italic)
End Function
'***************************************************************************************************************
' FontChangeUnderline - Set/reset Selected Font Underline
' CurrentFont = font reference to change
' Underline = True:Set, else reset
'***************************************************************************************************************
Public Function FontChangeUnderline(ByVal CurrentFont As Font, _
ByVal Underline As Boolean) As Font
Return FontChangeStyle(CurrentFont, _
FontStyle.Underline, _
Underline)
End Function
Page –42–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
'***************************************************************************************************************
' FontChangeStrikeout - Set/reset Selected Font Strikeout
' CurrentFont = font reference to change
' Strikeout = True:Set, else reset
'***************************************************************************************************************
Public Function FontChangeStrikeout(ByVal CurrentFont As Font, _
ByVal Strikeout As Boolean) As Font
Return FontChangeStyle(CurrentFont, _
FontStyle.Strikeout, _
Strikeout)
End Function
'***************************************************************************************************************
' FontChangeSize - Set/reset Selected Font Size
' CurrentFont = font reference to change
' NewSize = point size to set
'***************************************************************************************************************
Public Function FontChangeSize(ByVal CurrentFont As Font, _
ByVal NewSize As Single) As Font
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) = Math.Round(CDbl(NewSize), 2)) Then
Return CurrentFont
End If
Return New Font(CurrentFont.Name, _
NewSize, _
CurrentFont.Style)
End Function
'***************************************************************************************************************
' FontChangeName - Set/reset Selected Font Name
' CurrentFont = font reference to change
' NewName = new font family to change it to
'***************************************************************************************************************
Public Function FontChangeName(ByVal CurrentFont As Font, _
ByVal NewName As String) As Font
If StrComp(CurrentFont.Name, NewName, CompareMethod.Text) = 0 Then
Return CurrentFont
End If
Return New Font(NewName, CurrentFont.SizeInPoints, CurrentFont.Style)
End Function
'****************************************************************************************************
' FontChangeStyle - Support method for Bold, Italic, Underline, StrikeThrough set/reset
'****************************************************************************************************
Private Function FontChangeStyle(ByVal CurrentFont As Font, _
ByVal StyleFlag As System.Drawing.FontStyle, _
ByVal SetStyle As Boolean) As Font
'mask desired style against current (do this in case multiple selections)
Dim fntStyle As FontStyle = (CurrentFont.Style And StyleFlag)
'set flag to true if selected style is (or all selected styles are) already set
Dim flag As Boolean = (fntStyle = StyleFlag)
'if not EXACT match because but something IS different, then FORCE change
If Not flag AndAlso (fntStyle <> FontStyle.Regular) Then flag = Not SetStyle
If (flag = SetStyle) Then 'if nothing will change, then simply return the current font
Return CurrentFont
End If
'define a new style value minus the current selection(s), based on current font style settings
Dim newStyle As FontStyle = DirectCast(CurrentFont.Style And Not StyleFlag, FontStyle)
If SetStyle Then 'are we setting the new style(s)?
newStyle = (newStyle Or StyleFlag) 'yes, so set new style(s) to new font
End If
Return New Font(CurrentFont, newStyle) 'return new font based on current, w/selection set/reset
End Function
'***************************************************************************************************************
' ChangeFont - Support changing multiple properties
'***************************************************************************************************************
Public Function ChangeFont(ByVal CurrentFont As Font, _
Optional ByVal NewName As String = vbNullString, _
Optional ByVal NewSize As Single = 0, _
Optional ByVal StyleFlag As System.Drawing.FontStyle = FontStyle.Regular, _
Optional ByVal SetStyle As Boolean = False) As Font
If NewSize <> 0 Then 'if we will change the point size, and diff. is big enough...
If (Math.Round(CDbl(CurrentFont.SizeInPoints), 2) <> Math.Round(CDbl(NewSize), 2)) Then
Siz = NewSize 'set new size
Changes = True
End If
End If
Page –43–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
'define a new style value minus the current selected type, based upon the current font style settings
Styl = DirectCast(CurrentFont.Style And Not StyleFlag, FontStyle)
If SetStyle Then 'are we setting the new style?
Styl = (Styl Or StyleFlag) 'yes, so set new style to flag
End If
Changes = True
End If
End If
'if there are changes, try returning new font
If Changes Then
Try
Return New Font(Nam, _
Siz, _
Styl)
Catch 'on error, fall through
End Try
End If
Return CurrentFont 'return current if nothing to change, or error
End Function
End Module
The vbModal option allowed the new form to be displayed as a dialog box, where control of the
application cannot continue until the new form is closed. The vbModeless parameter told it to
display like a regular form, not hogging focal control. VB.NET now has two display methods that
clearly reflect these principles: Show(), which shows the form as an ordinary window (exactly like
vbModeless), and ShowDialog(), which shows the form as a dialog form, controlling program flow
until it closes (exactly like vbModal).
Page –44–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The Close() method is like VB6’s Unload. Although you probably could get by without invoking the
Dispose() method, using only Close(), but if you know anything about any class that features a
Dispose() method, then you would know that it is a very good idea to invoke it, because it causes all
of its resource to be removed now, not later, when the randomly running garbage collector
encounters it and figures out that it is not being used anymore. If you look at the documentation for
the Close() method (search Help for Form.Close), it states “When a form is closed, all resources created
within the object are closed and the form is disposed. You can prevent the closing of a form at run time by handling the
Closing event and setting the Cancel property of the CancelEventArgs passed as a parameter to your event handler. If
the form you are closing is the startup form of your application, your application ends. The two conditions when a form
is not disposed on Close is when (1) it is part of a multiple-document interface (MDI) application, and the form is not
visible; and (2) you have displayed the form using ShowDialog. In these cases, you will need to call Dispose manually to
mark all of the form's controls for garbage collection.” Elsewhere, it states the point more directly: “Dispose
will be called automatically if the form is shown using the Show method. If another method such as ShowDialog is used,
or the form is never shown at all, you must call Dispose yourself within your application.”
NOTE: If you invoke Dispose() before Close(), the FormClosing event will not fire. You can also invoke Dispose() from
within the FormClosed event if you wish to (good idea). Further, you can invoke Dispose() after Close() even if it was
not opened by ShowDialog(), though in this case it will do nothing, because the form had already been disposed.
We can emulate the VB6 Show method easily, if we cannot live without it, using a simple module:
Module modShowForm
Public Enum ShowFormConst As Integer
Modal = 1 'as dialog box
Modeless = 0 'as normal window
End Enum
'*************************************************************************
' ShowForm emulates the VB6 method of displaying a form, specifying a
' parent, and selecting a display mode (Modal or Modeless)
'*************************************************************************
Public Sub ShowForm(ByVal ThisFrm As Form, _
Optional ByVal Modal As ShowFormConst = ShowFormConst.Modeless, _
Optional ByVal OwnerFrm As Form = Nothing)
ThisFrm.Owner = Nothing 'ensure no parent
If (OwnerFrm IsNot Nothing) Then 'new owner exists
ThisFrm.Owner = OwnerFrm 'set new owner
End If
If (Modal = ShowFormConst.Modeless) Then 'modeless?
ThisFrm.Show() 'yes, so show normally (automatically invokes Dispose() method)
Else
ThisFrm.ShowDialog() 'else show as dialog
ThisFrm.Dispose() '(we must invoke Dispose manually. It will not be invoked by ShowDialog because
End If ' it may be returning a result to the invoker, which would be lost by Dispose)
End Sub
End Module
To use it, we provide the method with our form, the optional modal setting (default is Modeless), and
the optional parent window, in the form: “ShowForm(frm, FormShowConstants.Modal, Me)”.
The weird thing about the Load command was that it was not really necessary with forms, unless
you wanted to load a form without displaying it. This way you could load the form, initialize some
control fields, and then display the form once it is set up. The documentation indicated that the Load
command simply instantiated the form. So basically the less obvious VB6 “Load Form2” command
could have been used in place of the much clearer “Set Form2 = New Form2” command.
This brings us to an issue I have with VB. During the Load event, we could initialize components,
which is a handy place to do that sort of thing, but then we could also display it using the Show()
method while we are still executing the Load event. That never settled well with me. It is still legal
under VB.NET, sad to say. Everything must have an order to it. VB.NET should be more stringent
in this situation. I just never saw the logic in the ILRAP/TRLAP sequences being able to be
processed out of their defined orders. It makes sense to me in the new VB.NET TRLAP sequence
that TextChange events fire before Resize because of the anchoring capabilities of VB.NET. A
Page –45–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
dynamic label or textbox should reflect and resize its bounds before a form Resize event fires. And
Activate and finally Paint, in perfectly logical order, should not fire until the Load process has
finished doing its thing. The final thing to do is always painting. It is also in this last event that you
should do any special shaping commands3, such as draw form-surface lines and circles and such. It
makes much more sense to me if Microsoft had forced an LTRAP sequence, which would have
resolved every sequencing frustration we have had with VB.NET form processing. But during the
Load event, changes made to labels and fields can cause the TextChanged and Resize events to fire
again, so that would frustrate an LTRAP sequence. So we are forced to dealing with these
idiosyncrasies with a flag, such as using the Boolean mFormLoaded flag I outlined in Point 21.
One event I once thought lacking in VB.NET was the VB6 QueryUnload event. This event had fired
before the Unload event. I often used it to test if the user had chosen to close the form using either
the window frame’s menu or had selected the “X” icon in the upper right corner of the frame, or they
had pressed ALT-F4. I simply had to check the UnloadMode parameter for a value of
vbFormControlMenu. If this test was true, then the user was blowing out. You could also check in a
Multi-Document Interface form for a value of vbFormMDIForm to see if a child form was closing,
or if the form owner was closing by testing for vbFormOwner. If I decided I did not want a form to
close, I would set the Cancel parameter to a non-zero value (usually 1 or True).
Finally, I took a much closer look at the FormClosing event (Private Sub Form1_FormClosing(ByVal sender As
Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing). There was the answer in
front of me. Apart from the “ByVal sender As Object” parameter, the second parameter, e, was of type
FormClosingEventArgs. So in the body of the method I typed ‘e’ and then the dot. This offered me two
properties: “Cancel”, a flag I could use to check or set if the form should close, and “CloseReason”,
a read-only property that provides a value indicating to me why the form is being closed. These
values are defined in the following enumeration within the System.Windows.Forms namespace:
Public Enum CloseReason
None 'the closing reason could not be defined
WindowsShutDown 'the operating system is shutting down
MdiFormClosing 'an MDI child form is closing
UserClosing 'the user is closing the window
TaskManagerClosing 'Windows Task Manager is closing the window
FormOwnerClosing 'the form's owner is closing
ApplicationExitCall 'Application.Exit() method was invoked
End Enum
Therefore, if I want to cancel the form closing because the user is trying to close the form (assuming
that some data-critical application is running that must run to its end, otherwise data will be
corrupted, for example), I might write my FormClosing event like this:
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
If e.CloseReason = CloseReason.UserClosing And CriticalProcessing = True Then e.Cancel = True
End Sub
3
VB.NET lacks native shaping controls because VB.NET cannot work with windowless (lightweight) controls, but it can work
around this using Microsoft’s free Visual Basic Power Packs (http://msdn.microsoft.com/en-us/vbasic/bb735936.aspx),
which deliver new controls for your IDE toolbox, featuring line and shape controls that can draw lines, rectangles and ovals
at design time, eliminating the need to draw shapes manually in the form’s Paint event. This pack also includes a printer
control and collection, emulated printer I/O from VB6, a PrintForm component to allow you to print forms as you did in
VB6, and a really neat data repeater that allows you to display rows of data in a scrollable container.
Page –46–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The reason the compiler errors occur is due to the fact that expressions cast their component parts to
their equal or most senior member. If you let the mouse hover over any of the literal values, you will
see why we run into problems. ‘0’ and ‘1’ are by default considered to be Integer values, and “0.0”
and “1.0” are by default considered Double values. The values seem to work well enough during
initial assignment, but they run into problems when they are in multi-type expression statements. We
will always run into this problem when working with these smaller types because the default types in
VB.NET are Integer for non-floating point values and Double for floating point values.
The way around this is simple. Just append the offending integer literals with “S”, which will cast
them to type Short, and append “!” to the offending floating point literals to cast then to type Single.
More, you should also do the same during the assignments, even though there is no error reported,
because you are never really sure when the values are going to be demoted to the appropriate type. If
they are demoted during run-time, then that requires extra computer cycles to convert those values to
the proper type as a late-bound process. But even if the compiler automatically demoted them during
compilation so that their conversion was early-bound, it still does not clear up the problem for
someone reviewing your code and they see that the values are displayed in their promoted state.
Here are some more Literal Type Characters appending flags, so that you can ensure literal values
are what you would expect, or need them to be:
Value Type VB6 Symbol VB.NET Symbol(s) VB.NET Example(s)
Char Not Available c String(" "c, 128)
Short Not Available S 123S
Integer % % or I 123% or 123I
Long & & or L 123& or 123L
Single ! ! or F (Float) 123.45! or 123.45F
Double # # or R (Real) 123.45# or 123.45R
Decimal Not Available @ or D 123.45@ or 123.45D
NOTE: It may seem redundant to use a ‘C’ tag because a single character of a string is a Char, it is actually an element
in an array of 1 of type Char. For example, “Dim s As String = New String(" ", 128)” will generate the error
“Option Strict On disallows conversions from 'String' to 'Char'.” Use instead Chr(32) or ChrW(32) or CChar(" ") or " "c.
Page –47–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
'******************************************************************************************
' IcnToImage: Convert VB6 Icon picture to VB.NET Image (Bitmap)
'******************************************************************************************
Public Function IcnToImage(ByVal picProperty As stdole.IPictureDisp) As System.Drawing.Image
Return System.Drawing.Icon.FromHandle(CType(picProperty.Handle, IntPtr)).ToBitmap
End Function
End Module
NOTE: COM’s OLE Automation may have been auto-added during upgrades if you the upgrade involved ImageList controls. Also,
stdole can be added to Visual Studio .NET through the free Visual Studio Tools for Office, available from Microsoft:
www.microsoft.com/downloads/details.aspx?familyid=54EB3A5A-0E52-40F9-A2D1-EECD7A092DCB&displaylang=en. However,
before you add it, you might check your project properties and see if stdole is already installed. Just go to the References, hit Add, and
check under the .NET list for stdole. It may have been installed through the Primary Interop Assemblies. You could even find 3 or 4
different instances of stdole declared within the .NET Reference list; do not worry about which one to pick – just choose one of them).
Page –48–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
To employ these conversion functions, you can use something similar to “Me.PictureBox1.Image =
PicToImage(Me.AxImageList1.ListImages(1).Picture)” to copy a bitmap/picture, or “Me.PictureBox1.Image =
IcnToImage(Me.AxImageList1.ListImages(2).Picture)” to copy an icon.
This is fast, but if you need to process them even faster, you will have to work around this Picture
format conversion process by opening the VB5/6 ImageList control by selecting its form-top control
to reveal a little option expansion selector, and then go into its ActiveX properties and make a note of
the image size on the General tab, then on the Image tab, write down each image, index, key, and
tag, if any of the additional properties are set. Next, add a VB.NET ImageList to your form, and then
commence filling it with duplicate data. Mind you, the VB.NET ImageList is 0-based, as opposed to
the VB5/6 ImageList, which is 1-based, and so you will want to stuff a Dummy image into that zero
location so that your upgraded code will not have to otherwise be offset-adjusted. Afterward, delete
the now-unused VB5/6-control and rename your new ImageList control to that of the old one.
48. Dealing with the loss of VB6 Control Lists and how to recover their functionality
If you are trying to get rid of VB6 Compatibility Library features, you may find yourself running into
a few VB6 helper class control lists. For as much as VB6 users whined about the loss of control lists
in VB.NET, such lists are sure easy to implement. I will show you easy ways of doing it, providing
you with alternative methods and functions to make building control arrays this way easy and fast,
and allowing you to customize it to your particular tastes.
When building control arrays under VB6, you could copy a control and paste numerous copies to the
form, or give them the same name. The first had an index of zero, the next 1, the next 2, and so on.
When they are upgraded to VB.NET, each control must have a unique name, but they are also
collected into a special VB6 Compatibility Library control list. For example, suppose I have a form
with a lot of images displayed on it, and the user made a selection of an image (now a PictureBox)
to activate some option. Suppose further that we have 18 such images, lined up 6 by 3, all named
Image1, and indexed from 0 to 17. When you bring the form up in designer mode under VB.NET,
you may notice, if you tinker around with the Properties list, that the Image1 controls are now a
series of PictureBoxes named _Image1_0 through _Image1_17. You will also notice a new Image1
gearbox control on the form control’s ribbon below the form (interestingly, when I now create fresh
‘duplicate’ controls on a new VB.NET project, I tend to name them in a similar manner, trailing the
“group name” with an underscore and then an incrementing index number, starting with zero).
Delete the VB6 Compatibility Library’s Image1 control from the form ribbon. Note that this will
also remove the “Handles Image1.Click” tag from the Image1_Click event, plus from any handlers
associated with the Image1 control, such as MouseMove. It will also remove a lot of superfluous
code from the application. I say good riddance, though it is a good idea to check and tag all events
that used Image1 (searching for “Sub Image1_” works, or you can even search for “Handles
Image1.” if you have not yet deleted the Image1 gearbox control).
As a first attempt, go to each control and edit its Tag property to match its named index value (I will
later introduce a means to forego this task if you do not want to go through this extra effort, or if you
may be using the Tag property for something else). Notice that after you enter the tag for one of the
controls (0, 1, 2, 3, etc.), you can simply click on the next control and begin typing its numeric index
value; the Tag property will now be the default selected property, until you change to another.
After that, go into your form’s code and create an array beneath the declaration of the form’s class:
Dim Image1(17) As PictureBox
Page –49–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Alternatively, as of VB2008, you can use a strongly typed Generic List. Though I recommend using the
array shown above, a Generic List might be handy when using a list of CheckBoxes. Besides, you can do
a For…Each search through either implementation, when you might want to scan for those that are
checked, or which RadioButton is selected. But, for me, working with arrays simply has less overhead.
Here is how you would declare a Generic List, if you wish to use it, instead:
Dim Image1 As New System.Collections.Generic.List(Of PictureBox) 'A collection OF (Type) PictureBox
In the Form_Load event, stuff the array with the controls (this sort of thing is usually done behind
the scenes in the automatically generated code – to see such code yourself, be sure the “Show All
Files” button is selected in the toolbar of the Solution Explorer and select the form’s file that ends in
“.Designer.vb”). I cheat by entering the first complete line, then copying it and pasting copies, then
editing their index value. This is how you would add them to the array of PictureBoxes:
Image1(0) = _Image1_0 ' copy and paste, then edit sure makes this process a whole lot easier
Image1(1) = _Image1_1
Image1(2) = _Image1_2
...
Image1(17) = _Image1_17
Or, if you chose to use a strongly-typed Generic List, this is how you would add them to it:
Image1.Add(_Image1_0)
Image1.Add(_Image1_1)
Image1.Add(_Image1_2)
...
Image1.Add(_Image1_17)
Unlike regular Collection objects that will return a generic Object and that you will have to cast to
work with its result, Generic Collection objects are strongly typed and return the object type stored.
Scanning a collection of type RadioButton could go something like this:
Dim Index As Integer = 0 'init index to 1st entry (0-based)
For Each rBtn As RadioButton In RadioButtons 'check each RadioButton in the RadioButtons List (or Array)
If rBtn.Checked Then 'check its checked state
Exit For 'it is checked, so Index is set
End If
Index += 1 'else bump index by 1 and loop
Next
MsgBox("You Selected " & RadioButtons(Index).Text)
NOTE: All things considered, you may want to instead consider placing the controls on a Panel, which is a borderless
container class (hence runtime-invisible, unless you change its background color). You can loop through it with a
For…Each, just like a Generic List, but all without having to programmically stuff the container at all.
After that, go to your Image1_MouseMove event (and any other events, such as Image1_Click, that
the images had triggered), and after the event command, add “Handles _Image1_0.MouseMove,
_Image1_1. MouseMove, _Image1_2. MouseMove, etc.” (I cheat by copying one, pasting several copies, then
editing the trailing index values). Afterward, the command line might look like the following:
Private Sub Image1_MouseMove(ByVal sender As System.Object, ByVal eventArgs As System.Windows.Forms.MouseEventArgs) _
Handles _Image1_0.MouseMove, _Image1_1.MouseMove, _Image1_2.MouseMove, _
_Image1_3.MouseMove, _Image1_4.MouseMove, _Image1_5.MouseMove, _
_Image1_6.MouseMove, _Image1_7.MouseMove, _Image1_8.MouseMove, _
_Image1_9.MouseMove, _Image1_10.MouseMove, _Image1_11.MouseMove, _
_Image1_12.MouseMove, _Image1_13.MouseMove, _Image1_14.MouseMove, _
_Image1_15.MouseMove, _Image1_16.MouseMove, _Image1_17.MouseMove
If you do not want to do it this way (I don’t), which looks messy when it gets over a few handles,
you can instead dynamically add handlers right after you filled the array (above), using the command
AddHandler (a runtime alternative to the Handles clause) to add the MouseMove, Click, or whatever
event you want to process and assign the controls to the Address of the selected event method.
So if we were to strip the above line of all its trailing handlers, it would look something like this:
Private Sub Image1_MouseMove(ByVal sender As System.Object, ByVal eventArgs As System.EventArgs)
We can then programmically add event handlers to the event Image1_MouseMove (you could have
actually named the event anything, to include constructing the whole method from scratch). In the
following list, notice that we are taking each image defined, and notice also that by applying the
AddHandler command to them, the controls expose event properties (technically Delegates), and we
apply them to the address of the desired event method, such as the following:
Page –50–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
AddHandler _Image1_0.MouseMove, AddressOf Image1_MouseMove 'Add the MouseMove event of our controls
AddHandler _Image1_1.MouseMove, AddressOf Image1_MouseMove ' to the Image1_Mousemove list of
AddHandler _Image1_2.MouseMove, AddressOf Image1_MouseMove ' supported events.
...
AddHandler _Image1_17.MouseMove, AddressOf Image1_MouseMove '(NOTE: You can use RemoveHandler to remove, but not needed)
Finally, at the beginning of the event code, if you upgraded, you will notice some now-dead (actually
now error-marked) code that was used to get an Index of type Short (though you can use Integer if you
wish) from the control (it was expressed as “Dim Index As Short = Image1.GetIndex(sender)”). I simply
replace that error line with this one:
Dim Index As Short = CShort(DirectCast(sender, PictureBox).Tag)
This will grab the Tag of the control and extract a value from it, assigning it to Index.
If you are processing quite a number of control lists, you may want to employ a function to do all the
heavy work for you. Consider the following function:
'*******************************************************************************
' Subroutine Name : GetTagIndex
' Purpose : Return Index based on Tag value
'*******************************************************************************
Public Function GetTagIndex(ByVal eventSender As System.Object) As Short
If TypeOf eventSender Is Label Then
Return CShort(DirectCast(eventSender, Label).Tag) 'Handle labels
End If
If TypeOf eventSender Is PictureBox Then
Return CShort(DirectCast(eventSender, PictureBox).Tag) 'handle pictureboxes
End If
Return -1S 'flag error
End Function
With the above function, you need only enter the following line in an event to get its index like this:
Dim Index As Short = GetTagIndex(sender)
If you are using the Tag property of the controls for something else, which should be no big surprise,
you may want to instead write a function that will accept the sender parameter and return an Index
based on the naming of the control (I use the upgrade’s underscore and index naming method as a
useful personal standard in my own VB.NET projects). Consider this function, which will also
thankfully relieve you of the task of editing all the individual Tag properties of all the controls:
Module modGetNameIndex
'*******************************************************************************
' Subroutine Name : GetNameIndex
' Purpose : Return index based on Name (assume index value trails name after underscore)
' : Add as many type checks as you need. Example: Label1_13 for ID=13
'*******************************************************************************
Public Function GetNameIndex(ByVal eventSender As System.Object) As Integer
Dim Nam As String = vbNullString 'assign initial data so VB.NET will not accuse us of processing an uninitialized variable
And that is all there is to it. This simple technique will solve most old or new control array issues
without a ton of extra overhead, and the most tedious part of it being the populating of Handles
statements. But this type of thing was even done under VB6, though it was hidden from you.
Page –51–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: The backslash ‘\’ performs an integer division, tossing away any remainder, so no Float results.
Mind you, this is convenient, but most-times we will not even need to use these provided values, so
you can just delete what is not needed. However, they do reinforce to us how we can obtain them in
new VB.NET projects (note that this example is actually the one I drew from in the last point). As an
aside, the expressions for Button and Shift should be cast to Short (CShort(eventArgs.Button \
&H100000) and CShort(System.Windows.Forms.Control.ModifierKeys \ &H10000)) if Option Strict is On.
Alternatively, you can change them to Integers, which will eliminate the need to recast anything.
The Value property no longer exists under VB.NET, nor does such a property have a logical place in
an object oriented world. With VB.NET, you employ the PerformClick() method on the control:
Me.cmdHelp.PerformClick()'treat as forced button click
51. Dealing with no AVI animation control in VB.NET and how to easily add a free one
Many users have complained that VB.NET does not have an animation control to play AVI files.
Although several online solutions exist, they normally require running the application with administrator
permissions because they involve adding OCX interfaces. However, there is an even easier way to do
this, and all without permission issues, or even running the AVI as a separate process. If you have VB6
installed, or just the free VB6 SP6 Redistribution Pack (see below), this is very easy to do (note that your
application users will not ever need to go through this process at all).
First, if you have tried to install VB6 on a Vista or Windows 7 operating system, you will have been told
that there are known compatibility issues with this application. However, these issues are easy to
surmount. If you have not done so already, and you need it, go ahead and install VB6. It will report
incompatibilities, but just ignore them for now, and even tell the operating system, if it asked you, that it
installed OK. The trick to getting past the incompatibilities issue is to right-click the installed VB6.EXE
application, or any shortcut to it, select Properties, select the Compatibility tab, and then place a
checkmark in the “Run this program as an administrator” checkbox. Afterward, any time you launch
Page –52–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
VB6 under Vista or Windows 7, you will have to hit the “Allow” option in a User Account Control
dialog, but this is a very small price to pay for unfettered access to the VB6 programming environment,
especially if you need support source code that compiles under it.
However, many of these systems will not allow you to install Service Pack 6 for Visual Basic 6.0 at all
due to incompatibility issues (http://www.microsoft.com/downloads/details.aspx?familyid=9EF9BF70-
DFE1-42A1-A4C8-39718C7E381D&displaylang=en). But, you can get around this, and optionally not
need to install VB6 at all by instead installing the free Runtime Distribution Pack for Service Pack 6 for
Visual Basic 6.0 (http://www.microsoft.com/downloads/details.aspx?FamilyId=7B9BA261-7A9C-43E7-
9117-F673077FFB3C&displaylang=en). This installation may still inform you that it might not have
installed correctly, even though it actually did (this alert is in fact due to a problem it has trying to
register OLEAUT32.DLL, which is already installed and registered), so select the “This program
installed correctly” option if this message prompt comes up. If you are unsure about this, install it in Safe
Mode (Select Start / Run (or WinKey-R), then enter msconfig, select the Boot tab, and select Safe Boot).
You will have to undo this in Safe Mode to reboot normally. However, it will still report an error, but this
time it will clearly inform you that it could not register OLEAUT32.DLL (it is because of heightened
system protections that have been added that prevent this DLL file from being tampered with, and
especially by an older version of it, which the VB6 version most certainly is).
What is important for our main point, which is installing an animation control, is that the COM
support library that provides this VB6 control is now loaded onto your computer and registered. But
once it is loaded into your system, you are now able to easily add an animation control to your
VB.NET Toolbox.
To add the VB6 animation control to your Toolbox, and a new toolbox tab to contain it, do the following:
1) With any, or a new VB.NET Window Application form up in the IDE, select the Toolbox tab.
2) Right-click one of the Toolbox tabs and choose “Add Tab”. A new Tab will be added to the Toolbox, and it will wait for you
to name it (this might not at first seem apparent, but notice a new blank tab with a cursor blinking in its heading). Name it
“COM”, or to something of your own personal preference. And lock in the change by hitting Enter.
3) Next, right-click the new COM Tab and select “Choose Items…” from the popup menu. After a rather long pause to gather
its massive reserve of available system resources, a “Choose Toolbox Items” dialog window appears.
4) Click the “COM Components” tab on the dialog window (there will be another delay to format its list).
5) Locate “Microsoft Animation Control 6.0 (SP6)” in the displayed list and place a check in its checkbox.
6) Select OK to apply the selection and close the dialog window.
Notice that you now have an animation control in your new COM Toolbox Tab, which you can use just
as you had done with VB6. Notice further that when you place an animation control on a form, the
properties for the control report that it is of type AxMSComCtl2.AxAnimation. (Ax represents ActiveX)
You will also notice that once you add an animation control to a form, that two new references are added
to your project properties, both for “Microsoft Windows Common Controls-2 6.0 (SP6)”; one of them
for Interop.MSComCtl2.dll, and another for AxInterop.MSComCtl2.dll, which are required to support the
animation control. Notice further that their “Copy Local” properties are both set to True, meaning that
these will be, and must be loaded into your program directory, being non-registration versions of the
ActiveX MSComCtl2 registered controls (the system will copy them automatically for you).
Developing this solution was like a mission for me. I first figured out how a VB6 to VB.NET
upgrade did it, and then figured from that code-only solution that there had to be a way to add such a
control to my toolbox, rather than instantiate everything programmically, which the upgrade code
did. It may be simple enough to do it programmically, but it is not something that is going to be
sitting at the top of my head every time I need to use it. On the other hand, a toolbox control is easy
to find and even easier to apply to a form than it is to go about writing a bunch of instantiation and
form-placement code (I say, let the background form designer do all of that programming for me).
My only real question to Microsoft is why they could not have added a native animation control to
VB.NET long before now? I would have though it essential, even for the release of VB2002.
However, considering that AVI animation is a COM process, I think they hesitates to add it to .NET.
Page –53–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
If PlayAsync Then
flags = SND_FILENAME Or SND_SYNC Or SND_NODEFAULT
Else
flags = SND_FILENAME Or SND_ASYNC Or SND_NODEFAULT
End If
If NoWait Then flags = flags Or SND_NOWAIT 'check for NoWait flag
If PlayLoop Then flags = flags Or SND_LOOP 'check for continuous play
If PlayAsync Then
flags = SND_RESOURCE Or SND_SYNC Or SND_NODEFAULT
Else
flags = SND_RESOURCE Or SND_ASYNC Or SND_NODEFAULT
End If
If NoWait Then flags = flags Or SND_NOWAIT 'check for NoWait flag
If PlayLoop Then flags = flags Or SND_LOOP 'check for continuous play
PlayWavResource = PlaySound(SoundName, GetHInstance(), flags)
End Function
'*******************************************************************************
' PlayWavStop(): stop playing any sounds that might be playing
'*******************************************************************************
Public Sub PlayWavStop()
Call PlaySound(vbNullString, 0, 0)
End Sub
Page –54–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
This was a tried and true module that served me well for about a decade, both in VB6 and in the C++
(with my C++ version of it). However, with the introduction of the .NET Framework, the choice of
options available to us has expanded substantially, rendering API invocations totally unnecessary.
In VB.NET, from the My.Computer namespace, we now have a LOT of computer control only as far
away as our fingertips. From the My.Computer.Audio namespace, we can easily play WAV files,
stop them, or issue system sounds (the message beeps, such as alerts, warnings, etc.). From
My.Resources, we have full access to all of our embedded resources. From
My.Resources.ResourceManager, we can poke and prod and check our resource data to no end.
One loud complaint I have heard regarding this is that many programmers cannot seem to use a
string name to get access to an audio resource, so they have resorted to using a Select block to check
a text name against a list, then play the associated embedded WAV resource. Well, I can understand
that one. The first day I started playing with VB.NET resources, I thought the same. But being a
software engineer, I knew that it could not be implemented so poorly. With all that raw power at my
fingertips, Microsoft was going to just settle on some weak solution like that? So I took a look at the
resource manager and between experiments (and stopping to actually read documentation), I quickly
figured it all out. By simply paying attention to the popup tooltips, I learned that by providing the
resource manager’s GetObject method with the text name of the desired resource, it would return an
object that I could cast into the appropriate type (byte array, stream, or string), which comprises my
audio data (byte array if from VB6, or stream if VB.NET-added). I could then take advantage of the
Play method in My.Computer.Audio and play that resource, or from a file path, with equal ease.
Following is my new VB.NET version of the previous VB6 PlayWav routines:
'*******************************************************************************
' PlayWavFile(): VB.NET Play sound from a file
'*******************************************************************************
Public Function PlayWavFile(ByVal FileName As String, _
Optional ByRef PlayAsync As Boolean = False, _
Optional ByRef PlayLoop As Boolean = False, _
Optional ByRef NoWait As Boolean = False) As Boolean
'see if the audio file exists
If Not System.IO.File.Exists(FileName) Then Return False 'nope; error
'process our flags
Dim flags As AudioPlayMode = AudioPlayMode.Background
If Not NoWait Then flags = AudioPlayMode.WaitToComplete
If PlayLoop Then flags = AudioPlayMode.BackgroundLoop
'*******************************************************************************
' PlayWavResource(): VB.NET Play sound from a resource file
'*******************************************************************************
Public Function PlayWavResource(ByVal SoundName As String, _
Optional ByRef PlayAsync As Boolean = False, _
Optional ByRef PlayLoop As Boolean = False, _
Optional ByRef NoWait As Boolean = False) As Boolean
'get data for sound from resource
Dim obj As Object = My.Resources.ResourceManager.GetObject(SoundName)
If obj Is Nothing Then Return False 'bad name, so return error
'process our flags
Dim flags As AudioPlayMode = AudioPlayMode.Background
If Not NoWait Then flags = AudioPlayMode.WaitToComplete
If PlayLoop Then flags = AudioPlayMode.BackgroundLoop
'process file
If TypeOf obj Is System.Array Then 'byte array (usually upgraded VB6 files)
My.Computer.Audio.Play(DirectCast(obj, Byte()), flags)
Return True 'all is good, so return success
End If
If TypeOf obj Is System.IO.Stream Then 'typical VB.NET resource file
My.Computer.Audio.Play(DirectCast(obj, System.IO.Stream), flags)
Return True 'all is good, so return success
End If
If TypeOf obj Is String Then 'typical external wave file
Return PlayWavFile(DirectCast(obj, String), PlayAsync, PlayLoop, NoWait)
End If
Return False
End Function
'*******************************************************************************
' PlayWavStop(): VB.NET stop playing any sounds that might be playing
'*******************************************************************************
Public Sub PlayWavStop()
My.Computer.Audio.Stop()
End Sub
Page –55–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Now this is definitely easy. True, I was initially disappointed that the PlaySound() P/Invoke no longer
worked with .NET resource files, but what I got in trade has rendered the PlaySound() P/Invoke useless.
AVI Resources
AVI files are a source of frustration with VB6 users and VB.NET users alike. In VB6, it was a bit of a
bother. To store the file FileCopy.avi in a VB6 resource file you had to:
1) Add an entry, such as “101 CUSTOM "FileCopy.avi"” to a resource text file, such as myResources.rc.
2) Compile the resource from a DOS prompt, using something like “rc myResources”.
3) Finally, add the resulting myResources.RES file via Projects / Add File to your VB6.
Why many VB6 users claim this hack is easier than VB.NET’s methods, I do not know. In VB.NET, all I
do is go to Project / Properties, select the Resources tab, set the resource type to Files, hit the Add
Resource button, and navigate the browser to and select my FileCopy.avi file, and it is added to my
resources. Close the project properties and it is automatically compiled and associated to my application.
Yet reading and playing the AVI resource file can be frustrating, because I have heard all sorts of tales of
woe about how someone cannot figure out how to play it, or how they cannot convert the resource to a
string so they can use a StreamWriter to save it as a file, and various variations of this dilemma.
Unlike Audio files, which can be played directly from the resources, an AVI must currently be played
from a file (actually, there is a way to play AVI files from a VB6 resource file, by using something like
“Lresult = SendMessage(Animation1.hwnd, ACM_OPEN, ByVal App.hInstance, ByVal aviResourceID)”, where
ACM_OPEN is declared as WM_USER (&H400) + 100&). I was hoping that the new .NET architecture
would enable the Animation control to read byte arrays (byte arrays exhibiting read/write/seek features,
which is a byte array wrapped within a Stream interface) or streams directly, but sadly has yet to happen.
Therefore, our first task is to copy the resource data to a file. I will assume that we have the
FileCopy.avi file loaded in our resources. When it is added, it will automatically be saved in the
resources by its file-defined name, such as FileCopy (you can alter this by renaming the resource). I
like to play it as a file from wherever the executable program is running from, so I will do that,
though you may require a different non-locked local resource, such as a temp folder on the user’s
system if the application runs from a CD or DVD or some other similar write-protected source.
Also, the properties box reports that FileCopy has a type of System.Byte[], which is saying that it is a
Byte Array (C uses square braces to denote arrays). As such, instead of trying to convert the resource to a
string, I will treat it as a byte array and use a FileStream object so that I can write a binary file. With
VB6, all we had to do was issue the command “LoadResource 101, AviFile”, where AviFile is a string
specifying the file destination to save the ID 101 resource as a file; if we had ‘named’ our AVI resource
101 (see the above 3-step VB6 resource compiling process). Now consider the following VB.NET code:
'set up the local variable to hold the path to our FILECOPY.avi file
Dim AviFile As String = Application.StartupPath & "\FILECOPY.avi"
'first see if it already exists. If so, then we will not have to create it...
'if the AVI file does not already exist...
If Not System.IO.File.Exists(AviFile) Then
'set up our FileStream object to create it
Dim fs As New System.IO.FileStream(AviFile, System.IO.FileMode.Create)
'write byte array resource directly from resource to file from its beginning (0) for its length
fs.Write(My.Resources.FILECOPY, 0, My.Resources.FILECOPY.Length)
'close the new local AVI file copy of the resource
fs.Close()'this automatically invokes fs.Dispose(True) to release all used resources
End If
Page –56–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
A final thing you may want to do when you are leaving your application is to perhaps delete the AVI
file, although keeping it present will mean future runs will not have to start off saving another copy
to a file, but, because I like to clean house afterward, I normally do this in my application exit code:
System.IO.File.Delete(AviFile) 'delete the avi file resouce
Other standard resources you can store are Icons, Images, and String. Other than the auxiliary Files,
which I prefer to use, you may like to use Other, which is much the same as Files, but it is a way to
separately catalogue your various data. You can even set the access modifiers of your resources as
Public or Friend (default).
Following is my AppInfo class, featuring most of the old App statement’s many useful features:
Imports System.Reflection.Assembly, System.IO.Path, System.Diagnostics, System.Diagnostics.Process, System.Runtime.InteropServices
'******************************************************************************
' Class AppInfo (App)
' Used to create App class instance to access application information
' Declare this class globally using "Public App As New AppInfo"
'
' These function emulate the VB6 App command. For example, to get the current app's title,
' Dim S As String = App.Title
'******************************************************************************
Public Class AppInfo
Public Function FullPath() As String ' Support function
Return GetExecutingAssembly.Location()
End Function
Page –57–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
54. Dealing with updating default ByRef and ByVal method parameters flags
Be sure to check your ByRef and ByVal references in your method parameter lists. When a program
is updated from VB6 to VB.NET, parameter references that are not explicitly stated as flagged as
ByRef or ByVal will default to ByRef, simply because VB6 had defaulted to ByRef (a policy I had
always disagreed with, but understand due to its evolution from FORTRAN). But, most references
should be tagged as ByVal, unless they are passing objects that will be modified. ByVal makes data
safer, and so if the data should not be modified, be sure to change such ByRef entries to ByVal.
The above method is extremely fast; beat by only a few seconds in gigantic, memory-filling
collections by sending the handle of the Listbox the LB_DELETESTRING message (&H182), sent
via the SendMessage P/Invoke. This all can now be accomplished, even in gargantuan collections
with this simple and even faster command:
colHistory.Clear() 'More quickly purge the History Collection in VB.NET
NOTE: If you use a Generics collection, which also features the Clear() method, you can alternatively use the Remove()
method to remove entries by a Key, but you will have to use the RemoveAt() method to remove indexed entries.
Page –58–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The second step is to address the Build issue. One at a time, click on
a now-included file and be sure the Build Action property is set to
Content, meaning that the file will be part of the project’s content.
Another thing you may want to do is to determine how it should be supplied. If you go to the
project properties, and select the PUBLISH tab, you will notice a button named “Application
Files”. Select it and a list of application files that will be included in the publication will be
listed.
Finally, once you have gone through the Publishing Wizard at least once and made your few choices
(where to publish to, how the app will be installed, if updates can/should be checked, and from
where), you can usually process subsequent builds by simply choosing the Publisher’s Finish button.
57. Dealing with changes to Text Box SelStart and SelLen Properties
This is a very minor change. The VB6 SelStart and SelLen properties of TextBox and RichTextBox
controls has become SelectionStart and SelectionLength under VB.NET.
Page –59–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
To retrieve ToolTip text, in VB6 you would use a command such as this:
Dim ttText As String
ttText = Me.cmdContinue.ToolTipText
Page –60–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
60. Dealing with changes to Toolbar Button and Button Menu Clicks
When a Toolbar program is upgraded from VB6 to VB.NET, a couple things happen. First, both
normal and special dropdown button list click events must be handled differently than under VB6.
Second, ToolBars, though still supported by VB.NET for reasons of backward compatibility, are
automatically converted during an upgrade to the newer, more capable ToolStrip controls. These
issues are quite easy to adapt to and resolve programmically, but you really should understand what
is going on by examining the differences between the aging ToolBar and the newer ToolStrip, which
are both featured in VB6 and VB.NET. Also, even though continuing to implement the old ToolBars
might seem to be a more attractive choice from the perspective of a person upgrading from VB6 to
VB.NET, you cannot just drop a ToolBar control onto a form from
a fresh VB.NET install. The VB.NET Toolbox initially offers only
ToolStrip controls. Even so, you are able to add a ToolBar control
to the Toolbox, if you cannot seem to live without it, through its
Choose Items option (right-click a desired Toolbox group header
to access this option, and then in the dialog box select the Toolbar
entry declared in the .NET tab’s Global Assemble Cache
directory).
When a normal button is clicked on a ToolBar under VB6, its ButtonClick event will provide you
with a Button parameter that represents the clicked button on the ToolBar. If your processing code
will identify these buttons by the key you assigned to it during development, you can acquire the
Button object’s key through its Key property, or, if you process them by their assigned toolbar index
number (beginning from 1), you should instead use the Button object’s Index property, or finally, if
you process them by assigned text, you should use the Button’s Caption property. For example:
Private Sub Toolbar1_ButtonClick(ByVal Button As MSComctlLib.Button)
Select Case Button.Key 'Process by Key (use Button.Index for the button's 1-based index)
Case "Exit" 'if Exit button, then simply unload the main form
Unload(Me)
...
End Select
End Sub
When developing code for VB.NET’s ToolStrip buttons, this process is different. However, it is still
very easy to use. By default, VB.NET generates a separate click event for each button. For example:
Private Sub ToolStrip1_tbbExit_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ToolStrip1_tbbExit.Click
Me.Close() 'if Exit button, then simply unload the form (replaces VB6 Unload(Me))
Me.Dispose() 'also do this in case the form was opened as a Dialog or is an MdiChild and was not open
End Sub
However, you can consolidate all button code by creating event code named something like
ToolStrip1_ButtonClick and adding a list of button event handlers at its end. Additionally, be sure to
also define a Button object of type ToolStripItem in order to access each selected button, like so:
Private Sub ToolStrip1_ButtonClick(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles ToolStrip1_tsiOpen.Click, ToolStrip1_tsiClose.Click, _
ToolStrip1_tsiOptions.Click, ToolStrip1_tsiExit.Click
'Define Button as a ToolStripItem object (DirectCast is faster than CType() -- no actual conversion taking place)
Dim Button As ToolStripItem = DirectCast(sender, ToolStripItem)
Select Case Button.Name 'Process by the button's Name (no Key available. Use Text if the
'ImageStyle does not display the Text field, otherwise try the Tag field)
Case "tsiExit" 'if Exit button, then simply unload the main form
Me.Close() 'Close the form
Me.Dispose() 'also do this in case the form was opened as a Dialog or is an MdiChild
...
End Select
End Sub
The above example generally covers how a VB6 to VB.NET upgrade also handles it, excepting that
the original upgraded code will retain the old VB6 “Unload(Me)” command and mark it as
unsupported and request editing (see Point 61 for an explanation). I simply replace it with
“Me.Close()”, and then add “Me.Dispose()” if the form was handled as a dialog (though adding it
even if it was not does no harm).
Page –61–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: During an upgrade from VB6, the upgrade automatically generates the code to declare the Button object.
However, it will replace the original “Button.Key” with “Button.Name”, even though the VB6 button Name and Key
values were seldom, if ever identical. This can be a potential problem because it will not provide an upgrade note
informing you of this. So be aware that this will happen. I like to take the time to assign the upgraded button’s Tag
property to reflect the old VB6 Key property, or, as I have done in the above snippet, simply update the test for the
button name. Be aware that you could have also originally tested against the button’s Name property under VB6, in
which case the code would have upgraded more smoothly.
However, when developing the code under VB.NET, you can alternatively collectively process these
clicks through the ToolStrip itself in a single subroutine, and from its click event you can extract the
button information. Remember also that VB.NET button indexes begin at 0, not 1. For example:
Private Sub ToolStrip1_ItemClicked(ByVal sender As Object, ByVal e As ToolStripItemClickedEventArgs) Handles ToolStrip1.ItemClicked
'The following is how you would get the clicked Button's Name property from this ToolStrip event:
Dim ButtonName As String = e.ClickedItem.Name 'notice the type assigned to 'e', above.
'The following is how you would get the Button's definition index (from zero):
Select Case ToolStrip1.Items.IndexOf(e.ClickedItem) + 1 'optionally add 1 (compensate 0 option base if code is 1-based)
Case 4 'if Exit Button, then simply unload the form (4th button)
Me.Close() 'Close the form
Me.Dispose() 'also do this in case the form was opened as a Dialog or is an MdiChild
...
End Select
End Sub
Button Menus (VB6 ToolBar Button style 5; tbrDropdown) are menus that drop down from toolbar
buttons and offer a list of items to select from (you will notice a dropdown indicator on the right side
of the button (▼). Under VB6, such selections could be handled through the ButtonMenuClick event,
and you could acquire the index of the item in the button’s dropdown list from the menu button’s
Index property, regardless of whether you predefined the dropdown button list or dynamically added
them within the application code. Consider the following VB6 example:
Private Sub Toolbar1_ButtonMenuClick(ByVal ButtonMenu As MSComctlLib.ButtonMenu)
Call ShowListItem(ButtonMenu.Index) 'process according to the index value of the button
End Sub
But when you process a pre-defined dropdown button in VB.NET from a ToolStrip (style
ToolStripDropDownButton), event handling takes two different approaches. First, it can provide an
individual event code block for each dropdown button you care to declare code for, like this:
Private Sub Option0ToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles Option0ToolStripMenuItem.Click
'Handle code for predefined dropdown button with displayed text "Option 0"
End Sub
Of course, the above is only practical when you pre-define dropdown button values in VB.NET. If
you add them dynamically by adding entries to a DropDown button, we named, say, tsddbOptions,
through its DropDownItems collection, such as we might have done in the following example:
With Me.tsddbOptions.DropDownItems
.Add("Option 1")
.Add("Option 2")
.Add("Option 3")
.Add("Option 4")
End With
You could handle selections to those dynamically added buttons, as well as through any that had
already been predefined at development time through the dropdown button’s DropDownItemClicked
event, much like the following:
Private Sub tsddbOptions_DropDownItemClicked(ByVal sender As Object, ByVal e As ToolStripItemClickedEventArgs) _
Handles tsddbOptions.DropDownItemClicked
'The following is how you would acquire the dropdown Button's Text property (in lieu of a VB6 Key):
Dim KeyName As String = e.ClickedItem.Text 'this is typically practical, because the text data is defined.
' The following is how you would acquire the Button's index (from zero):
Select Case Me.tsddbOptions.DropDownItems.IndexOf(e.ClickedItem) + 1 'add 1 to compensate for a zero index in 1-based code
Case 4 'Option 4 (4th option)
'you must do the following command IF you are closing a form, otherwise an exception error will occur.
Me.tsddbOptions.HideDropDown() 'Close list, so exception will not fire thru the following Me.Close().
Me.Close() 'Close the form.
Me.Dispose() 'Do this also in case the form was opened as a Dialog or an MdiChild.
Exit Sub
...
End Select
End Sub
Page –62–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: Please notice the “HideDropDown()” invocation in the above code. If the dropdown is still open during a form
close, an exception will occur because the closing form will destroy form resources, such as the dropdown menu object,
which will still have code running as a background process, to handle collapsing the dropdown. Forcing the closing of
the button dropdown menu will detach that background process, which allows a clean removal of resources.
NOTE: By default, some of the above examples for the “e” parameter will have their types preceded by
“System.Windows.Forms.”, but this additional tagging is not really necessary, because that namespace has already
been loaded into the current Form’s code block, but it would be required if this code executes in an outside module.
To conclude, you can still add a ToolBar control to the Toolbox and process them much as you did
under VB6, but the newer MenuStrips are much more powerful and offer many new features, which
are far more flexible and dynamic and in keeping with current software development needs, such as
the ability to dock and undock from a ToolStrip Container, and are easier to write customization
code for, if you wish the application’s user to be able to do so. ToolBars, though still supported in
VB.NET simply for reasons of backward compatibility, are clearly by now very dated technology
that had been the initial mainstay in Visual Basic since the days of VB1 and QuickC for Windows.
Page –63–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
63. Dealing with Process Handling In the KeyPress, KeyDown, and KeyUp Events
One minor change that you may not notice is to the KeyPress, KeyDown, and KeyUp events.
Unlike VB6, KeyPress, KeyDown, and KeyUp event handling under VB.NET gives you more
control over what is done to key processing. A key down or up event is triggered when the user
pressed or releases a keyboard key. A key press event is triggered when the user simply types a key
on the keyboard. Typically, most developers concern themselves with just the KeyPress event.
Usually, the developer wants to restrict what the user types into a TextBox. For example, suppose
you wanted to ensure that the user entered only alphabetic or numeric characters into a TextBox, and
further, the alphabetic characters should always be upper case. One might use code such as this:
'*******************************************************************************
' Subroutine Name : txtName_KeyPress
' Purpose : Filter keyboard so that invalid data cannot creep in
'*******************************************************************************
Private Sub txtName_KeyPress(ByVal eventSender As System.Object, _
ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtName.KeyPress
Dim KeyAscii As Integer = Asc(e.KeyChar)
Select Case KeyAscii 'check the current key being entered into the txtName textbox
Case 1 To 31 'ignore checking control keys (let them pass through)
Case Else
Dim C As String = UCase(Chr(KeyAscii)) 'get uppercase character from code
Select Case C
Case "A" To "Z", "0" To "9", "_" 'check against the range of allowed characters
e.KeyChar = ChrW(AscW(C)) 'all is well for these, so be sure code reflects uppercase
Case Else
e.KeyChar = ChrW(0) 'out of range, so nullify it
End Select
End Select
End Sub
However, if we handle processing of a key code, we may want to prevent the system default handlers
from processing it further. For example, under VB.NET, if we set KeyChar to zero, downstream the
system will still process key code zero, because the system only knows that a key has been pressed. As a
result, the system will ring an alert bell because it determined that nil is not a valid key code.
If we want to suppress the bell, we may want to tell the system that it is not necessary to process the key
further at all. By informing it so, it will not be processed by downstream methods. To do that, you would
simply add the command “eventArgs.Handled = True” where you want further processing ignored:
Select Case C
Case "A" To "Z", "0" To "9", "_" 'range of allowed text
e.KeyChar = ChrW(AscW(C))
Case Else
e.KeyChar = ChrW(0) 'out of range, so nullify it (probably no longer necessary, but it makes me feel better)
e.Handled = True 'indicate Keypress event was handled (no need for further system processing)
End Select
NOTE: You would not add “eventArgs.Handled = True” after you updated the KeyChar property to an uppercase
state, because it would then not find its way into the textbox at all. This flag tells it to stop further processing.
An alternative is to do testing in the KeyDown event (KeyDown is handled before KeyPress, which is in
turn handled before KeyUp), and suppress processing by KeyPress if a key is invalid by setting the
e.SupressKeyPress property to True (which also sets its e.Handled property to True):
Private Sub txtName_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles txtName.KeyDown
Select Case e.KeyValue 'check the value of the ReadOnly key
Case 1 To 31 'ignore control keys
Case Else
Select Case UCase(Chr(e.KeyValue)) 'check character from code
Case "A"c To "Z"c, "0"c To "9"c, "_"c 'check range of allowed characters (note the c appendage for chars, not strings)
Case Else
e.SuppressKeyPress = True 'let no other keys reach the KeyPress event (this also sets Handled to True)
End Select
End Select
End Sub
With this, we can simplify our KeyPress event code to just this little block of code:
Private Sub txtName_KeyPress(ByVal eventSender As System.Object, _
ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtName.KeyPress
e.KeyChar = UCase(e.KeyChar) 'we can assume all values are valid from KeyDown event (Ucase will not affect controls or digits).
End Sub 'this method would not be needed AT ALL if we do not require Uppercase-only text in the textbox.
Page –64–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
'******************************************************************
' txtCommand_GotFocus
' When textbox gets focus (the user selected it, for example),
' then select entire contents.
'******************************************************************
Private Sub txtCommand_GotFocus()
With Me.txtCommand
.SelStart = 0 'set selection to start of text
.SelLength = Len(.Text) 'select entire text
End With
End Sub
'******************************************************************
' txtCommand_KeyPress
' When typing data into the textbox, if the user types ENTER,
' then automatically invoke the ADD button.
'******************************************************************
Private Sub txtCommand_KeyPress(KeyAscii As Integer)
Select Case KeyAscii
Case 13 'CR?
Call btnAdd_Click() 'Yes, so for button click (we could have also used 'btnAdd.Value = True')
KeyAscii = 0 'nullify code so it is not processed further
End Select
End Sub
'******************************************************************
' btnAdd_Click
'******************************************************************
Private Sub btnAdd_Click()
colList.Add(Me.txtCommand.Text) 'add user command to our list
Call txtCommand_GotFocus() 'reset full selection and focus to text box
End Sub
As you can see in the KeyPress() event of txtCommand, the (ByRef) KeyAscii variable is checked
for a value of 13, which is a Carriage Return, applied by the Enter/Return key. If 13 is found, then
the ADD button is invoked by calling the BtnAdd_Click() event code, and then nullifying the
KeyAscii value to prevent further processing of it. The Click() event for BtnAdd then adds the
contents of the textbox to the Collection colList, and then the GotFocus() event is manually invoked
to ensure that the contents of the TextBox are now fully selected.
For those who must still do VB6 programming, be aware that we could have invoked the Click()
event for BtnAdd by instead using the command “Me.btnAdd.Value = True” (which would upgrade to
VB.NET to a more logical “Me.btnAdd.PerformClick()”), which is the official VB6 method for forcing
the invocation of its click method, but what I am interested in is how to deal with the VB.NET side,
because all events under VB.NET will require parameters to be passed. And besides, such a change
would not work for invoking the GotFocus() event of txtCommand.
Most of you already know what I am driving at here. If you have seen a button click event under
VB.NET, it would be declared like this:
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
And the GotFocus() event for txtCommand (which is an Enter() event under VB.NET, just as a
LostFocus() event becomes a Leave() event under VB.NET), would be expressed as:
Private Sub txtCommand_Enter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtCommand.Enter
Page –65–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
But now that these events have parameters, many are wondering how to invoke them under
VB.NET? One can squeeze by on invoking a button click by instead invoking the button’s
PerformClick() method. But we would still be stuck with the GotFocus()event (now Enter()) for
txtCommand. Actually, the solution is very simple.
The first parameter of each event is the actual object being process; btnAdd for the btnAdd_Click()
event code, and txtCommand for the txtCommand_Enter() event code. Both of these have a second
argument object of an Eventargs type. As such you would simply specify the second parameter as
“New System.EventArgs()”. We use “New” because we are instantiating an new object.
However, if the method we invoke will not modify or use the Enventargs parameter, we could sneak
by cleanly by simply passing the EnventArgs parameter from the event we are invoking it from, if
we are indeed invoking it from within an event.
Following is the VB.NET upgrade of the above code:
Option Strict Off
Option Explicit On
Friend Class Form1
Inherits System.Windows.Forms.Form
'******************************************************************
' txtCommand_GotFocus (this becomes txtCommand_Enter in VB.NET -- txtCommand_LostFocus would become txtCommand_Leave)
' When textbox gets focus (the user selected it, for example),
' then select entire contents.
'******************************************************************
Private Sub txtCommand_Enter(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtCommand.Enter
With Me.txtCommand
.SelectionStart = 0 'set selection to start of text
.SelectionLength = Len(.Text) 'select entire text
End With
End Sub
'******************************************************************
' txtCommand_KeyPress
' When typing data into the textbox, if the user types ENTER,
' then automatically invoke the ADD button.
'******************************************************************
Private Sub txtCommand_KeyPress(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtCommand.KeyPress
Dim KeyAscii As Short = Asc(e.KeyChar)
Select Case KeyAscii
Case 13 'CR?
Call btnAdd_Click(btnAdd, New System.EventArgs())
KeyAscii = 0
End Select
e.KeyChar = Chr(KeyAscii)
If KeyAscii = 0 Then
e.Handled = True
End If
End Sub
'******************************************************************
' btnAdd_Click
'******************************************************************
Private Sub btnAdd_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAdd.Click
colList.Add(Me.txtCommand.Text)
Call txtCommand_Enter(txtCommand, New System.EventArgs())
End Sub
End Class
And sure enough, the upgrade performs the conversions of these invocations just as I described. All
that is left here is to tighten up the code. For example, event txtCommand_KeyPress, although it works
perfectly, could be seriously tightened up for much faster operation:
Private Sub txtCommand_KeyPress(ByVal sender As Object, ByVal e As KeyPressEventArgs) Handles txtCommand.KeyPress
If e.KeyChar = vbCr Then 'CR?
btnAdd.PerformClick() 'yes, force adding the current data
e.KeyChar = vbNullChar 'nullify the key
e.Handled = True 'tell system not to handle this key code further
End If
End Sub
Page –66–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
When you change the focus by using the mouse or by calling the Focus method, focus events occur in the following order:
1. Enter
2. GotFocus
3. LostFocus
4. Leave
5. Validating
6. Validated
NOTE: If the CausesValidation property is set to False, the Validating and Validated events are suppressed.
The reason that the Enter and Leave events are used instead of the GotFocus and LostFocus events
is because under VB.NET, GotFocus and LostFocus have been changed to low-level focus events
that are tied directly to the WM_KILLFOCUS and WM_SETFOCUS Windows messages.
A LostFocus event corresponds to the WM_KILLFOCUS system message, which is send
immediately before the focus is removed from the control. A GotFocus event corresponds to the
WM_SETFOCUS system message, which is sent when a control has gained keyboard focus. As
such, their definition has certainly changed between VB6 and VB.NET.
The VB.NET Enter and Leave events correspond more directly to the VB6 GotFocus and
LostFocus events. Hence, the Enter and Leave events should now be used for all controls except the
Form class, because the Enter and Leave events are suppressed by the Form class. The equivalent
events used by the Form class are instead the Activated and Deactivate events, which also
correspond more directly to the VB6 GotFocus and LostFocus events for Forms.
NOTE: Do not attempt to set focus from within the Enter, GotFocus, Leave, LostFocus, Validating, or Validated event
handlers. Doing so forces the thread to yield control and can cause the application to stop responding to messages.
Page –67–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Page –68–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
72. Dealing With using Icons for Menu Images (Bitmaps) under VB.NET
VB.NET Menus allow you to specify bitmaps as menu images, which will be displayed on the left
side of a dropdown menu ribbon. However, you can use icons instead, which are more plentiful and
more available, by simply invoking its ToBitmap method. For example, if we were creating a new
ToolStripMenuItem, we could declare one like this:
'create a new menu item
Dim mnuItem As New ToolStripMenuItem("&Open", My.Resources.icnOpen.ToBitmap, New EventHandler(AddressOf mnuFileOpen_Click))
Or modify the image of an existing menu item by specifying something like this:
Me.mnuFileOpen.Image = My.Resources.icnOpen.ToBitmap
Page –69–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
73. Dealing With Converting VB6 Fast FSO File I/O to Faster VB.NET Streaming Methods
Under VB6, the fastest File Input/Output processing possible was with a File System Object, available
by setting a reference to either Microsoft Scripting Runtime (Scrrun.dll) or to Windows Scripting Host
Object Model (wshom.ocx, which ultimately rerouted to the IwshRuntimeLibrary.dll).
For example, under VB6 with an FSO, you could quickly read a text file into a TextBox like this:
Dim FSO As FileSystemObject 'file system object (reference to scrrun.dll or wshom.ocx needed)
Dim Ts As TextStream 'textstream object for file I/O
If you wanted to do the same FSO operation under VB.NET, you would have to add a COM
reference to Windows Scripting Host Object Model (wshom.ocx) and then add “Imports
IWshRuntimeLibrary” to the top of the file, or you would have to add a COM reference to Microsoft
Scripting Runtime (scrrun.dll) and then add “Imports Scripting” to the top of the file. You could then
add the code above to do the same thing:
Dim FSO As New FileSystemObject 'instantiate a File System Object for File I/O
Dim Ts As TextStream = FSO.OpenTextFile(Path, ForReading, False) 'open the filepath for reading through a textstream object
'NOTE:if you use scrrun.dll instead of wshom.ocx, the above second parameter would be specified as 'IOMode.ForReading'
myForm.TextBox1.Text = Ts.ReadAll 'read its contents into a textbox
Ts.Close() 'close the file
Ts.Dispose() 'dispose of the TextStream resource
FSO = Nothing 'dispose of our file system object
As though a File System Object did not speed File I/O up fast enough, VB.NET features
StreamReaders and StreamWriter objects that kick File I/O into turbo charge. To perform the same
task as shown above, but without adding references or importing namespaces, and also do it a whole
lot faster, do the following:
Dim Ts As System.IO.StreamReader = FileIO.FileSystem.OpenTextFileReader(Path) 'open a Stream reader to the text filepath
myForm.TextBox1.Text = Ts. ReadToEnd 'read its contents into a textbox
Ts.Close() 'close the file and automatically invoke Ts.Dispose()
Page –70–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Unfortunately, this seems to do absolutely nothing, which was driving me to distraction. However,
after some unrelenting experimentation, I discovered that I could do the following instead, and it
works perfectly every time:
Me.Cursor = Cursors.WaitCursor 'show that we are busy by assigning the wait cursor to the current form
NOTE: In the above, “Me” represents the current form. Also, if you added the command “Application.DoEvents()”
afterward, which was the trick to get the busy cursor to display under VB6, that this may even prevent the cursor from
changing on the screen under VB.NET.
Of course, to reset the cursor to the default, once you are fisnished performing a long task, you
would issue the following command:
Me.Cursor = Cursors.Default
76. Dealing With Specific Changes to the KeyDown and KeyPress Events
NOTE: Although much of this material was covered in a previous point, it is worth going through again with a fine-
toothed comb to address specific changes to the KeyDown() and KeyPress() events.
Under VB6, the KeyDown event supplied you with an Integer Keycode parameter, which gabbed the
system code for the key typed, and it provided an Integer Shift property, which allowed you to check
for the Shift, Cntrl, or Alt keys being pressed. You could disable the KeyCode by setting it to 0,
which in turn caused the KeyPress event to be supressed. For example, consider this VB6 code:
'*******************************************************************************
' Subroutine Name : lstEnums_KeyDown (VB6)
' Purpose : Allow DEL key to select Delete button
'*******************************************************************************
Private Sub lstEnums_KeyDown(KeyCode As Integer, Shift As Integer)
Select Case KeyCode
Case 46 'DEL key
If Me.cmdDelete.Enabled Then 'activate delete button if it is enabled
Me.cmdDelete.Value = True 'click Delete button
KeyCode = 0 'disable KeyCode
End If
End Select
End Sub
The VB6 KeyPress event was handled after the KeyDown event, but before the KeyUp event. The
KeyPress event provides a KeyAscii parameter that was the ASCII value of the key pressed (duh).
You could disable it by setting the KeyAscii code to 0, which cancelled further system processing of
the code. For example, consider this VB6 code:
'*******************************************************************************
' Subroutine Name : txtNewName_KeyPress (VB6)
' Purpose : Filter keyboard so that invalid data cannot creep in
'*******************************************************************************
Private Sub txtNewName_KeyPress(KeyAscii As Integer)
Select Case KeyAscii
Case 1 To 31 'allow control keys
Case Else
Select Case UCase$(Chr$(KeyAscii)) 'check uppercase character code
Case "A" To "Z", "0" To "9", "_" 'allow A-Z, a-z, 0-9, and "_"
Case Else
KeyAscii = 0 'disallow others
End Select
End Select
End Sub
Page –71–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Under VB.NET, the rules have changed a bit, but I think it is for the better. Consider the following
example of a VB.NET version that merges the above VB6 KeyDown event:
'*******************************************************************************
' Subroutine Name : txtName_KeyDown (VB.NET)
' Purpose : Allow DEL key to select Delete button
'*******************************************************************************
Private Sub txtName_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles txtName.KeyDown
Select Case e.KeyValue
Case 46 'DEL key (we could have also checked ReadOnly e.KeyCode against Keys.Delete)
If Me.cmdDelete.Enabled Then 'command button enabled?
Me.cmdDelete.PerformClick() 'yes, press Delete key
e.SuppressKeyPress = True 'disable further processing in KeyPress() event
End If
End Select
End Sub
Notice that there are 3 key differences noted. First, notice that we obtained the KeyValue (the
VB.NET version of a VB6 KeyCode), from the “e” parameter, which is declared as a KeyEventsArg.
Second, we invoked the Delete key using its PerformClick() method. Third, instead of setting
e.KeyValue to 0 to supress the KeyPress event, as we might do if we were following the VB6 model,
we instead supressed the KeyPress event (and further processing) by setting the “e” parameter’s
Boolean SuppressKeyPress property to True. If we had tried to change e.KeyValue to 0, as we had
done under VB6, an error would be generated, because under VB.NET, KeyValue is ReadOnly.
Under VB.NET, there is a slight change to how the KeyPress event handles its code from the way
VB6 did it. For example, the “e” parameter is now a KeyPressEventArg, which provides not an
Integer KeyAscii code as VB6 did, but rather a KeyChar parameter that is of type Char. Also, to
cancel further processing from the KeyPress event and to prevent an invalid code from being sent to
the operating system for default processing, instead of setting the value to zero as we did under VB6,
we set the boolean e.Handled parameter to True, which tells the operating system that we have
handled the processing of the key and that no further processing is required.
Consider the following rather involved, but useful syntax parsing example:
'*******************************************************************************
' Subroutine Name : txtConstValue_KeyPress (VB.NET)
' Purpose : Filter keyboard so that invalid data cannot creep in
' : Process a command line such as "(BASE + 3) 'lower value"
'*******************************************************************************
Private Sub txtConstValue_KeyPress(ByVal sender As System.Object, _
ByVal e As System.Windows.Forms.KeyPressEventArgs) Handles txtValue.KeyPress
Dim KeyAscii As Integer = Asc(e.KeyChar) 'grab ASCII code of character
Dim S As String = Trim(Me.txtConstValue.Text) 'get contents of assignment data (a Constant value assignment)
Dim J As Integer = -1 'init location index
Dim I As Integer = InStrRev(S, "'") 'comment tag present?
If CBool(I) Then
J = Me.txtConstValue.SelectionStart + 1 'if so, check for selection point
End If
Dim AllowLC As Boolean = J >= I 'set lowercase allowance flag if selection beyond comment tag
Page –72–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Page –73–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
As you can see, the Data object contains a 1-based Collection object that holds a list of one or more
file paths, which we can easily loop through to pick up all of its entries. This, and single items that
you could grab using the Drag Data object’s GetData() method were all the capability VB6 Drag
and Drop had. Hmm. Not very impressive.
However, VB.NET comes along and it seems like a monkey wrench was thrown into the works (a
monkey wrench, or spanner in Britain, is a large adjustable wrench with a long fulcrum arm that was
popular in the nineteenth century, though now typically used in only really tough, tight
situations, or plumbing, where you need to monkey around with a stuck pipe or large nut – it
also made a good, but sloppy hammer).
When I upgraded my application, I discovered that the Drag and Drop code did not likewise
upgrade. So I tried to take a look at what I needed to do. I dutifully enabled the EnableDrop
property on my upgraded lstFiles object by setting it to True.
I then added a DragDrop()event to the lstFiles object, but I was not quite sure what to do with it. I
did notice that my “e” parameter was of type DragEventArgs, and that it had a Data object, but this
Data object did not have a Files collection. So I took a look at the documentation to see what I could
glean from that. What I gathered was that I needed to use the Data object’s GetData() method to
acquire data (duh). All I had to do was provide GetData with a Format string. All the examples I saw
were for basic types, like “String” and such. But I was not getting anywhere with that, so I thought I
would use the GetFormats() method, which returned a string array, listing all the acceptable
formats, and I put them into a message box. But I was still not getting any results. Plus, the mouse
cursor looked like this when the mouse was dragged over my lstFiles control:
More reading revealed that I had to parse the data being dragged and decide if it could be dropped
onto my target object, and to set the proper copy effect for that object, which was accomplished
through the object’s DragEnter()event. Though I was so far getting nowhere on the format, I really
liked this new DragEnter()event idea a whole lot better than the really limited support we had in
VB6. So I cheated, for the time being, by simply setting my Effects property to Copy, just so I could
finally fiddle around in the DragDrop() event:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs) Handles lstFiles.DragEnter
e.Effect = DragDropEffects.Copy
End Sub
With that, my message box in my DragDrop event popped up with the list of formats
available. Well, the last three formats looked promising, especially FileDrop and
FileName. I recognized FileDrop from working with Clipboard I/O, using the
DataFormats class, which featured a number of string members specifying,
surprisingly enough, data formats. FileDrop was used in clipboard I/O to handle
copying a number of files. As such, I assumed (rightly so, as it turned out) that I could
use Filename or FileDrop interchangeably, though I prefer FileDrop simply because I
can avoid misspellings by using the DataFormats class.
So with that, I changed my above DragEnter() event to:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs) Handles lstFiles.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in
e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Parameter TRUE allows other compatible
Else 'formats to be converted to the specified target format.
e.Effect = DragDropEffects.None 'Put up a no-entry sign
End If
End Sub
This worked for file dragging, but then I ran into only a minor snag in my DragDrop event. But this
ended up only helping me to better understand the special handling that Drag and Drop still affords
files. As an experiment, I had placed the following line into my DragDrop event:
Page –74–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
MsgBox(e.Data.GetData(DataFormats.FileDrop).ToString) 'I used ToString because the returned data is of type Object
The result, instead of a filename, was “System.String[]”. This did not do very well as a
filename, but it was informing me that the result was actually a string array (C# and C++
use square brackets instead of VB’s parentheses to indicate arrays). So, as a further
experiment, I changed my test code to this:
Dim DataFiles() As String = e.Data.GetData(DataFormats.FileDrop, True) 'grab file list
Dim Nms As String = vbNullString 'init list accumulator
For idx As Integer = 0 To UBound(DataFiles) 'loop through file list
Nms &= CStr(idx) & ": " & DataFiles(idx) & vbCrLf & vbCrLf 'append each file
Next
MsgBox(Nms) 'display result
NOTE: If you have set Option Strict On, as I do, then you must also cast the data returned by GetData(), because it
returns a generic Object type. We know that this type is actually a String Array, so we can use the DirectCast()
command to cast it to string: Dim DataFiles() As String = DirectCast(e.Data.GetData(DataFormats.FileDrop,
True), String()).
Of course, you will also have to remember the DragEnter() event, which will allow you to specify
what kinds of files your target control will allow, which is now handled during dragging, rather than
generically by the enablement of a control parameter, which had invited the undesired possibility of
invalidly formatted data being dropped under VB6:
Private Sub lstFiles_DragEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs) Handles lstFiles.DragEnter
If e.Data.GetDataPresent(DataFormats.FileDrop, True) Then 'Note that you can use "FileName" or even "FileDrop" in
e.Effect = DragDropEffects.Copy 'place of DataFormat.FileDrop. Parameter TRUE allows other compatible
Else 'formats to be converted to the specified target format.
e.Effect = DragDropEffects.None 'Put up a no-entry sign
End If
End Sub
NOTE: Just a reminder, I use the optional parameter “True” to get the GetData() and GetDataPresent() methods
to allow compatible format that might not be of that type to be converted to that type.
Overall, I am very impressed with VB.NET’s implementation of Drag and Drop. It is easy to use
and easy to upgrade, and offers me much tighter control over what can or cannot be dropped on a
control that accepts dropped items.
Page –75–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
NOTE: If you do not find these entries in the Choose Toolbox Items dialog box, then you may not or no longer have the VB6
redistributables on your system, so you will have to minimally install the free Runtime Distribution Pack for Service Pack 6
for Visual Basic 6.0, available from Microsoft (http://www.microsoft.com/downloads/details.aspx?FamilyId=7B9BA261-
7A9C-43E7-9117-F673077FFB3C&displaylang=en). You are allowed to do this even if you no longer own VB6.
However, if you do not want to use VB6 form controls and use as little resources and code as possible,
you can easily add VB.NET native methods to perform these tasks for you. The code to do that in
covered in an article named “Send (SMTP) and Retrieve (POP3) Email with Ease under VB.NET” in
the companion volume to this book, “Enhancing Visual Basic .NET Far Beyond the Scope of Visual
Basic 6.0” (www.slideshare.net/davidrossgoben). Download it for free to learn how to do it.
Page –76–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Closing Remarks
This concludes the assistance guide, “Navigating Your Way through Visual Basic 6.0 to Visual Basic .NET
Application Upgrades”. Anything not covered by this document can usually be resolved with similar ease. Always
remember that each warning inserted by the Upgrade Wizard also includes a link to the MSDN help, which offers
you further assistance on the problem it either finds or, most often, simply cautions you to the per chance of a
problem. Using these tools and this guide together, you should find upgrading to be much less of a chore.
See the free companion manual, “Enhancing Visual Basic .NET Far Beyond the Scope of Visual Basic 6.0”
(www.slideshare.net/davidrossgoben) for more detailed information of the differences between VB6 and VB.NET.
It is also recommended that you download the free 719-page VB6 to VB.NET upgrade guide e-book, “Upgrading
Visual Basic 6.0 to Visual Basic .NET and Visual Basic 2005” from Microsoft Developer Network (MSDN) at
http://msdn.microsoft.com/en-us/library/aa480541.aspx. Pay particular attention to Chapter 7 through 9. You can
also download the “Visual Basic 6.0 Upgrade Assessment Tool” from here, along with before and after sample
files. This guide was developed jointly by the Microsoft patterns & practices team and ArtinSoft,
(www.artinsoft.com) a company with vast experience in Visual Basic upgrades and the developer of the Visual
Basic Upgrade Wizard and the Visual Basic Upgrade Wizard Companion. This guide provides valuable
information for organizations who are considering upgrading their VB6-based applications and components to
VB.NET. It provides proven practices to reach functional equivalence with a minimal amount of effort and cost,
as well as guidance for common advancements after the application is running on the .NET framework.
Another free 547-page e-book from Microsoft is “Upgrading Microsoft Visual Basic 6.0 to Microsoft Visual
Basic .NET” at http://msdn.microsoft.com/en-us/vbrun/ms788236.aspx. It is the complete technical guide to
upgrading Visual Basic 6 applications to Visual Basic .NET, covering all upgrade topics from APIs to ZOrders. It
shows how to fix upgrade issues with forms, language, data access, and COM+ Services, and how to upgrade
applications with XML Web services, ADO.NET, and .NET remoting. It also provides big-picture architectural
advice, a reference of function and object model changes, and hundreds of before-and-after code samples.
Still another free 274-page e-book from Microsoft is “Introduction to Microsoft Visual Basic 2005 for
Developers” (http://msdn.microsoft.com/en-us/vbasic/ms788235). Even though it is designed around VB2005, it
is worth its weight in gold. VB2005 is a giant step forward from earlier editions of VB.NET, and is in my view
almost VB2008, which is the starting platform you need to get really serious in VB development. If you currently
work with Visual Basic 6, these authors fully understand the adoption and code migration issues you'll encounter.
They'll step you through a quick primer on .NET Framework programming, offering guidance for a productive
transition. If you already work with .NET, you'll jump directly into what's new, learning how to extend your
existing skills. From the innovations in rapid application development, debugging, and deployment, to new data
access, desktop, and Web programming capabilities, you get the insights and code walkthroughs you need to be
productive right away.
Though some of the information provided in the above free Microsoft books are clearly dated, sometimes offering
solutions using older, more convoluted command paths than those offered by VB2008 or VB2010 (often thanks to
the new “My” namespace), those older solutions are still valid (I have tried to address those particular solutions
in this document and its companion).
To see how P/Invokes to the system are defined and used in .NET, visit PInvoke.net (www.pinvoke.net). This site is
a massive knowledgebase of .NET P/Invoke information, providing examples for VB.NET and C#. This is an
interactive, user-supported site. If you have a P/Invoke that is not yet documented, or is not presented for your
development language, such as VB.NET, here is your chance to contribute your knowledge to the hive.
Another place to get great upgrade information is VB Migration Partners. Although products and services are
sold here, it also features a wealth of freely available information (www.vbmigration.com).
Page –77–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
Einstein calculated E = MC2 / T, which would mean that at the speed of light, an object would attain infinite
volume and infinite mass. Correct me if I am wrong, but even an ordinary mortal like me sees this as ridiculous,
because this would mean that we would attain several quintillion times the volume and mass that exists in the
entire universe (and even that may an infinite understatement). All you have to do is simply ponder it for a
moment.
However, reverse the order, where E = T / MC2, and all calculations yield the same results, save one, and that is
that instead of attaining infinite volume and infinite mass at the speed of light, one instead moves into an alternate
universe. What most people do not realize is that the very atoms of our body, at this very moment, are already
vibrating at just under the speed of light. Just a tiny push and we would become multidimensional beings. I think
this is why Quantum Physics, which is still nowhere near being a perfect model for planck-scale physics, will,
even so, discover the Far World, or what the ancients called the Underworld or the Hidden World, or most
everyone today refers to as the Afterlife or Heaven. Quantum Physics has already demonstrated that nothing in the
universe has ever existed without Consciousness. So, where was Consciousness when the Universe was created
out of literally nothing during a causal Singularity Event? Evidence shows that it existed. And that has been the
crux of great debates throughout history.
There is an easy experiment that can prove that Einstein’s Theory of Special Relativity is inverted. Weigh a very
heavy object, and then drop it onto a solid base, such as a concrete floor. Now weight the object again. It will
weigh less… for about 20 minutes; at which time its full weight will finally return. Where did the missing mass go
to during those 20 minutes? If Einstein’s calculation had been correct, the object would have actually weighed
more after being dropped, not less.
Page –78–
Navigating Your Way Through Visual Basic 6.0 to Visual Basic .NET Application Upgrades – David Ross Goben
The following Visual Basic documents are publicly available at: http://www.slideshare.net/DavidRossGoben, and at Google Docs at:
http://docs.google.com/leaf?id=0B_Dj_dKazINlN2JlY2EwMmEtNGUyMy00NzQzLTliN2QtMDhlZTc5NDUzY2E5&sort=name&layout=list&num=50.
Page –79–