Language Enhancements in Visual Basic 2005
Summary: This article explores the many new enhancements to Visual Basic 2005 and provides you with simple code snippets to illustrate how many of them work. Features covered include XML comments, generics, Global keyword, partial types, My, and more. (20 printed pages)
**Note **This article was originally written for Beta 1 of Visual Studio 2005. The latest version, Beta 2, has some minor changes that might alter the behavior of the features or code in this article.
Operator Overloading and Conversion Operators
Property Accessor Accessibility
Custom Event Accessors
Application Level Events
Explicit Array Bounds
The public Beta of Visual Studio 2005 brings you a stable preview of Visual Basic 2005 in which the new features have solidified to the point where few changes should occur before the RTM release. Some things are sure to change, of course, but you can expect the final product to look much like the Beta 1 preview. Even so, I recommend you don't install the Beta on a production machine, but rather run inside the safety of a Microsoft Virtual PC image.
Major changes to the Visual Basic language include My, XML comments, and generics. Several new language statements fill in logical gaps including Using, Continue, and TryCast, and the Global keyword. There are a number of structural improvements including changes to property accessor accessibility, custom event accessors, partial types, and application-level events. Operator overloading and conversion operators are new, along with the IsNot operator. Other useful changes include unsigned types, form default instances, compiler warnings, and explicit array bounds.
The first time you start Visual Studio 2005, you're prompted to select your preferred IDE settings. These settings control features such as IDE key mappings, window layouts, and code formatting in the code editor. You can change your settings at any time using the Tools | Import/Export Settings menu item as shown in Figure 1. You can import default settings for another programming language, or you can import custom settings files that all developers use in your company.
Figure 1. Import custom settings to standardize the development environment at your company
A major challenge faced by developers migrating to .NET from Visual Basic 6.0 and earlier versions is the sheer size of the .NET Framework and the number of classes in its namespace hierarchy. Commonly used application and environment objects are found throughout the Base Class Library (BCL) of the .NET Framework, and it takes time to learn their locations. The new My hierarchy in Visual Basic 2005 speeds up and simplifies application development by providing convenient, logically organized access to many commonly used framework classes.
My comes in two flavors:
- Items that provide well-organized shortcuts into the Framework classes.
- A set of dynamic objects created as you add forms, settings, resources, and Web services to your projects.
The My shortcuts are essentially a speed dial into many commonly used BCL objects. The top-level My shortcuts are My.Application, My.Computer, and My.User, each of which contains its own object hierarchy. For example, the My.User object exposes Audio, FileSystem, Keyboard, Mouse, Network, Registry, and more:
Dim sVar As String sVar = My.User.Identity.Name sVar = My.Computer.Info.OSFullName sVar = My.Application.CommandLineArgs.Count.ToString
The dynamic My objects are automatically populated by Visual Basic as you add forms, resources, settings, and Web services to your project. My.Resources and My.Settings are particularly interesting, as the Visual Basic compiler generates strongly typed variable references as you add resources and settings to your project.
Double-click the My Project item in Solution Explorer to bring up the application designer. Click on Settings in the tab list on the left side as demonstrated in Figure 2.
Figure 2. Application Designer showing the Settings tab. The Error List pane shows errors, warnings, and messages and is now separate from the Task List.
Add a setting name such as MaxInputValue of type Integer at User scope with a value of 100. You can immediately refer to this setting with a strongly typed object in your code with full IntelliSense as shown in Figure 3.
Figure 3. Example of the IntelliSense pop-up for a settings value you create in the Settings application designer
At the top right corner of the Settings designer is a link labeled View Code. Click on this link to view the file named MySettings_user.vb. This file is a partial class where you can add code to handle settings events to update the state of your application as your settings change (see the section on Partial Types for more information):
Partial Public Class MySettings '... End Class
Three MySettings events are exposed here:
The application Settings interface is pluggable, meaning that you can define how your settings are saved by implementing these event routines.
You get the same strong typing with Resources you add to your project. If you drop an image named Happy.bmp on the Images area of the Resources designer, you can refer to it in code as My.Resources.Happy. Happy is of type Bitmap, which means that you don't have to cast from an Object or Image type to use the bitmap in code. IntelliSense also works immediately when you type Happy in your code. For example:
PictureBox1.Image = My.Resources.HAPPY
This line of code assigns the bitmap object to a PictureBox's Image property. You can see the IntelliSense pop-up window in Figure 4, indicating that the object is of type Bitmap.
Figure 4. Dragging-and-dropping a bitmap image on the Resources tab in the application designer results in a strongly typed object that you can use immediately in your code.
As you add forms and Web services to your project, the My object is dynamically populated with strongly typed default instances of these objects that you can refer to in your code.
As you can see, My makes it more convenient to find application and environment related BCL classes, and gives you immediate and strongly typed access to settings, resources, and other objects you add to your projects. These are great productivity enhancements for Visual Basic developers of any experience level.
XML comments allow you to add structured documentation to your code, and have been the envy of Visual Basic developers because C# got them first. XML comments can describe a variety of code elements including classes, fields, methods, and enumerations.
Once you create an XML comment describing a code feature, you immediately get IntelliSense on parameters and the return type when you type the code feature in the editor. Assume you have a function prototype with the following signature:
Private Function GetCustomerData(ByVal CustomerCode As String) _ As DataSet
Place the code insertion point on the line above the function declaration and type three single-quote characters. Visual Basic generates an XML comment template matching the function declaration that you can tab through and fill out like a form. Assume you fill the comment template for your function like so:
''' <summary> ''' Returns a DataSet for a single customer. ''' </summary> ''' <param name="CustomerCode">Customer Code as String.</param> ''' <returns>DataSet for the supplied CustomerCode.</returns> ''' <remarks>Resides in the application's data layer.</remarks>
When you type this function name elsewhere in your code, you'll get full IntelliSense on the function summary, parameters, and return value as shown in Figure 5.
Figure 5. This tool tip shows the immediate IntelliSense feedback you get when you add XML Comments to your code elements.
This is a key feature in corporate and team environments. You can generate code documentation from the structured XML comments inserted in your source files. Also, an application architect or developer lead can prototype and comment features before implementation. As developers write code using the prototype objects, IntelliSense guides them in proper use of the features.
XML comments are treated as an integral part of your code by the compiler. The comment tag values are colored as comments, and if you collapse outlining on an XML comment, only the summary tag value displays in place of the whole XML comment template (see Figure 6).
Figure 6. XML comments are fully integrated with the compiler and code window. The XML comment on the FillDataGrid function is collapsed to its summary line.
If you change the name of the CustomerCode parameter in the function prototype to CustomerID, the compiler adds a green squiggly line under the
<param name="CustomerCode"> tag as a warning that the two names are mismatched (see the section on Compiler Warnings for more).
Visual Basic automatically generates an XML documentation file when you build the project. This file appears in the application output directory as AssemblyName.xml. Because this file is XML, you can easily transform it into other output formats as needed. XML comments make it much easier and more convenient to generate documentation for your code, both as IntelliSense while you're coding, and as code documentation once you build your application.
The BCL now provides the System.Collections.Generic namespace, which contains several classes defining generic collections. They're called generic because at declaration you specify a type placeholder for the contained objects instead of a specific type. You give the stored objects a specific type only when you create an instance of the collection.
Generics are a huge time saver. If you've ever created custom collections, you know how much code can be involved. Generics in Visual Basic let you create the equivalent of a custom collection in one line of code. You no longer need to create separate custom collections for each type of object you want to store. You simply provide the desired type as you instantiate the generic collection.
The easiest way to see how generics work is with an example. Assume you have a simple Customer class with two public properties (shown as public variables for brevity):
Public Class Customer Public Name As String Public CreditLimit As Decimal Public Sub New(ByVal CustomerName As String, _ ByVal CustCreditLimit As Decimal) Name = CustomerName CreditLimit = CustCreditLimit End Sub End Class
You can declare a list of Customers using the generic collection List class like so:
Dim Customers As New System.Collections.Generic.List(Of Customer)
With this single line of code, you've declared a strongly typed list that stores only Customer types and gives you full IntelliSense on the contained Customer objects. You could just as easily create a list of Product or Order objects, effectively creating a set of custom collections without writing all the custom collection code for each type. You can now write code like this:
Dim custA As New Customer("CustA", 1000) Customers.Add(custA) Dim custB As New Customer("CustB", 2000) Customers.Add(custB) For Each c As Customer In Customers MessageBox.Show("Customer: " & c.Name & _ ", limit: " & c.CreditLimit) Next 'compile error: Product cannot be converted to Customer Dim prodA As New Product("ProdA", CDec(29.95)) Customers.Add(prodA)
You also get a performance advantage using generic collections because the stored objects are strongly typed and not kept internally as type Object.
There are other collection generics available in the System.Collections.Generic namespace. For example, the Dictionary(of K,V) collection allows you to specify types for the keys and values. The LinkedList(of T) and Stack(of T) generic collections behave like regular linked lists and stacks, except you're allowed to specify what kinds of objects they'll contain.
You can create your own generic types using the new generic type declaration syntax. Consider a Comparer class that lets you compare different kinds of objects:
Public Class Comparer(Of itemType) '... End Class
You can define multiple type placeholders in a comma-separated list. You can also define constraints restricting which classes can be provided to the generic when it's instantiated:
Public Class Comparer(Of itemType As IComparable) '... End Class
This constraint ensures that Comparer(of T) instances can only be created with classes implementing the IComparable interface. Multiple constraints can also be applied to the class declaration.
As an example, consider an expanded Customers class that implements IComparable:
Public Class Customer Implements IComparable Public Name As String Public CreditLimit As Decimal Public Sub New(ByVal CustomerName As String, _ ByVal CustCreditLimit As Decimal) Name = CustomerName CreditLimit = CustCreditLimit End Sub Public Function CompareTo(ByVal obj As Object) As Integer _ Implements IComparable.CompareTo Dim c As Customer = CType(obj, Customer) If CreditLimit > c.CreditLimit Then Return 1 If CreditLimit < c.CreditLimit Then Return -1 Return 0 End Function End Class
A similar Product class is implemented, except that the CompareTo function compares the product prices instead of the customer's credit limits:
Public Class Product Implements IComparable Public Name As String Public Price As Decimal Public Sub New(...)... Public Function CompareTo(...)... End Class
Now the Comparer class provides the generic comparison operation:
Public Class Comparer(Of itemType As IComparable) Public Function GetLargest(ByVal Item1 As itemType, _ ByVal Item2 As itemType) As itemType Dim i As Integer = Item1.CompareTo(Item2) If i > 0 Then Return Item1 If i < 0 Then Return Item2 Return Nothing End Function End Class
You can now instantiate Comparers with objects of different types:
Dim pc As New Comparer(Of Product) Dim prod1 As New Product("LittleOne", 10) Dim prod2 As New Product("BigOne", 100) Dim lp As Product = pc.GetLargest(prod1, prod2) MessageBox.Show("The more expensive product is: " & lp.Name) Dim cc As New Comparer(Of Customer) Dim cust1 As New Customer("SmallCo", 1000) Dim cust2 As New Customer("LargeCo", 5000) Dim lc As Customer = cc.GetLargest(cust1, cust2) MessageBox.Show("The customer with a higher limit is: " & lc.Name)
Without generics, you'd have to define a comparison class for each type of object you want to compare (for example, ProductComparer and OrderComparer).
Generics can significantly reduce your coding efforts in many common scenarios. Consider using generics when you have multiple classes that vary only by the type of object on which they operate.
The Using statement is a shortcut way to acquire an object, execute code with it, and immediately release it. A number of framework objects such as graphics, file handles, communication ports, and SQL Connections require you to release objects you create to avoid memory leaks in your applications. Assume you want to draw a rectangle using a brush object:
Using g As Graphics = Me.CreateGraphics() Using br As System.Drawing.SolidBrush = _ New SolidBrush(System.Drawing.Color.Blue) g.FillRectangle(br, New Rectangle(30, 50, 230, 200)) End Using End Using
You want to dispose of the graphics and brush objects once you're done using them, and the Using statement makes doing this a snap. The Using statement is much cleaner than using Try / Catch and releasing the object in the Finally block as you have to in Visual Basic .NET.
The Continue statement skips to the next iteration of a loop, making the loop logic more concise and easier to read:
Dim j As Integer Dim prod As Integer For i As Integer = 0 To 100 j = 0 While j < i prod = i * j If prod > 5000 Then 'skips to the next For value Continue For End If j += 1 End While Next
The Continue statement is very clean and makes escaping the inner loop quite easy without resorting to a label and a goto statement. The Continue statement operates on For, While, and Do loops.
The Global keyword makes the root, or empty, namespace at the top of the namespace hierarchy accessible. In the past, you could not define a System.IO namespace within your company's namespace hierarchy:
Doing so would mess up all references to the framework's System namespace within your application. You can now use the Global keyword to disambiguate the namespaces:
Dim myFile As Global.System.IO.File
The Global keyword is especially useful in code generation scenarios where you want to be absolutely sure that generated namespace references point to what you intend. I expect that you'll see Visual Basic itself use the Global keyword in all generated code.
The IsNot operator provides a counterpart to the Is operator. IsNot lets you use a direct comparison, eliminating the Not operator:
If myObj IsNot Nothing Then
Similarly, you can eliminate the Not operator when checking if two object instances are different:
If MyObj1 IsNot MyObj2 Then
Although it's a simple change, IsNot fills a consistency gap in the Visual Basic language that helps make your code clearer.
In Visual Basic 2003, you can cast one type of object to another type in one of two ways:
Dim p As Product p = CType(obj, Product) p = DirectCast(obj, Product)
Here, you use CType to convert from one type to another, while DirectCast requires there be an inheritance or implementation relationship between the objects. The problem with these is that you'll get a run-time exception in either case if
obj cannot be converted or cast to type Product.
Enter the new TryCast statement. You use it the same way as CType or DirectCast, except that the result returned is
Nothing if the cast cannot be accomplished:
p = TryCast(obj, Product) If p IsNot Nothing Then 'use the p object... End If
Operator Overloading and Conversion Operators
One of the most powerful additions to Visual Basic 2005 is operator overloading. Operator overloading lets you define operators that act on any type you want, even to the point of creating your own base types.
The classic operator-overloading example is adding complex numbers. A simplified Complex class with the + operator overloaded might look like this:
Public Class Complex Public Real As Double Public Imag As Double Public Sub New(ByVal realPart As Double, ByVal imagPart As Double) Real = realPart Imag = imagPart End Sub Shared Operator +(ByVal lhs As Complex, ByVal rhs As Complex) _ As Complex Return New Complex(lhs.Real + rhs.Real, lhs.Imag + rhs.Imag) End Operator End Class
Use the + operator to add complex numbers in a very intuitive way:
Dim lhs As Complex = New Complex(2.0, 2.5) Dim rhs As Complex = New Complex(3.0, 3.5) Dim res As Complex = lhs + rhs 'res.real = 5.0, res.imag = 6.0
You can also overload conversion operators for your custom types. If you try to convert the value of
res as CType(res,String), you'll get a compiler error
Complex cannot be converted to
String." If you try looking at the value of
Res.ToString, you get a value of
Complex back as ToString shows the type name by default. You fix these issues by overloading the CType conversion operator and the ToString method, like so:
Public Shared Narrowing Operator CType(ByVal Value As Complex) _ As String Return Value.Real.ToString & "i" & Value.Imag.ToString End Operator Public Overrides Function ToString(ByVal Value As Complex) As String Return Value.Real.ToString & "i" & Value.Imag.ToString End Function
Now the returned values of CType(res,String) and
res.ToString are what you would expect: "5.0i6.0." Conversion operators must be declared with either Narrowing or Widening, indicating how the conversion is done.
Property Accessor Accessibility
An issue that has always bothered me about Visual Basic .NET properties is that the Get and Set accessors must have the same accessibility (Public, Friend, or Private). If you want to create a read-only public property (only the Get is public), there is no Set accessor you can use within your component to enforce validation or custom property handling.
The Get and Set accessors in Visual Basic 2005 can now have different accessibility settings, as long as Set is more restrictive than Get:
Private _myProp As String Public Property MyProp() As String Get Return _myProp End Get Friend Set(ByVal value As String) If value.Trim.Length > 0 Then _myProp = value.Trim Else value = "<no value>" End If End Set End Property
This is especially helpful in team development environments and for individual developers striving to get the highest amount of reuse out of their code.
Custom Event Accessors
Event accessors let you define a custom event and control what happens as clients add and remove handlers and raise your event. Suppose you have a custom class in which you raise an event RateChanged. You declare normal events in one of two ways:
Public Event RateChanged() 'or Public Event HoursChanged As EventHandler
Events declared in this way have an automatically managed backing store. In other words, the system handles how the event is managed and dispatched. Normally this is fine, but sometimes you need more control over how the event's listeners are notified.
You declare a custom event and its accessors using the new Custom keyword. When you hit the enter key on the event declaration, Visual Basic 2005 creates the code prototype for you in the same way Property accessors are generated:
Public Custom Event NameChanged As EventHandler AddHandler(ByVal value As EventHandler) 'hook handler to backing store End AddHandler RemoveHandler(ByVal value As EventHandler) 'remove handler from backing store End RemoveHandler RaiseEvent(ByVal sender As Object, ByVal e As EventArgs) 'invoke listeners End RaiseEvent End Event
When the client adds or removes a handler for your event, the AddHandler or RemoveHandler routine runs. When the event is raised, the RaiseEvent routine executes. In this way, you can take specific actions based on how you want to manage the backing store for the event. When you create custom events this way, you can think of the event as if it were a property.
An example where this is useful is when your object is serializable and you have an event that is handled by a non-serializable delegate object. If you try to serialize your object with a normal event, serialization will fail because the storage backing the event isn't serializable. Rocky Lhotka (another Visual Basic MVP) has an explanation of how this works and a detailed example in his blog entry at http://www.lhotka.net/WeBlog/CommentView.aspx?guid=776f44e8-aaec-4845-b649-e0d840e6de2c.
Partial types allow definition of a single type across multiple source files. The type definition has a normal declaration in one file:
Public Class TestPartial 'implementation... End Class
In other source files, you use the Partial keyword to tell the compiler that this code is a continuation of the original class definition:
Partial Public Class TestPartial 'implementation... End Class
Code in classes generated by designers in the IDE is marked with the Partial keyword, and is separated from user code. Add a form named TestForm to a project and look at the resulting source file. You may be surprised to see that it's empty except for the form's class declaration. Where's all the designer-generated code that used to go inside the Windows Form Designer Generated Code region?
The designer-generated code is now placed in file TestForm.designer.vb, which you can see by clicking the Show All Files button in the Solution Explorer toolbar. You shouldn't change the code in this file, but you can copy and paste what you need into the TestForm.vb file if you want to preserve something when the designer file is later regenerated.
Project teams can take advantage of partial classes by giving each developer a file containing their portion of the class. Multiple developers no longer need to share access to the same source files since you can now separate a class definition into multiple source files.
Application Level Events
Another new feature you'll want to take advantage of is a new set of Application-level events available in a partial class named MyApplication. Look for a file named MyEvents.vb under the My Project item in Solution Explorer. You can also find this file behind the View Code button on the Application tab in the application designer.
These new Application-level events are similar to the application events in the global.asax file in an ASP.NET application. Five events are exposed:
The first three events are fired as the application starts up and shuts down. NetworkAvailabilityChanged fires when the network state changes on the machine. Put code in the UnhandledException event in case an exception is thrown that you don't handle anywhere else.
Visual Basic 2005 has full support for unsigned types. The unsigned type names are:
The unsigned types work as regular types except the unsigned type can store only positive integers.
Unsigned types are most useful in Visual Basic for making Win32 API calls that take unsigned types as parameters. The following code illustrates a windows MessageBox API call:
Public Enum WindowsMessageResult As UInteger OK = 1 Cancel = 8 End Enum Private Const MB_OK As UInteger = 0 Private Const MB_ICONEXCLAMATION As UInteger = &H30 Private Const MB_OPTIONS As UInteger = MB_OK Or MB_ICONEXCLAMATION Private Declare Auto Function Win_MB Lib _ "user32.dll" Alias "MessageBox" _ (ByVal hWnd As Integer, ByVal lpText As String, _ ByVal lpCaption As String, ByVal uType As UInteger) _ As UInteger Public Function MessageThroughWindows(ByVal message As String, _ ByVal caption As String) As WindowsMessageResult Dim r As UInteger = Win_MB(0, message, caption, MB_OPTIONS) Return CType(r, WindowsMessageResult) End Function
Notice the last parameter and return type on the Win_MB declaration are both UInteger types.
Unsigned types can also help you conserve memory if you need to store values larger than an Integer. The Integer type uses four bytes of memory and can store positive and negative values up to 2,147,483,648. The UInteger type also uses four bytes and can store a value between zero and 4,294,967,295. Without an unsigned type, you'd need to use a Long taking eight bytes of memory to store values this large.
Another change to Visual Basic .NET that has tripped up many developers migrating from Visual Basic 6.0 is the lack of a default instance for forms. You need to create an instance of the form before using it:
Dim frm As New Form2 frm.Show()
Visual Basic 2005 supports form default instances, so you can use the familiar syntax:
It's best if you access this default form instance through the My.Forms collection:
Visual Basic 2005 supports compiler warnings, which give you a heads-up on issues that may cause problems at run time. A warning is shown as a green squiggly line under your code (errors are shown as blue squiggles).
Compiler warnings include recursive property access, overlapping catch blocks or case statements, creating a function without a return value, and others. My favorite warning is for a variable reference on an uninitialized object:
Dim cust As Customer If cust.Name.Length = 0 Then '... End If
Here the background compiler puts a green squiggly line under
cust in the
If statement. The warning text displayed reads "variable 'cust' is used before it has been assigned a value." A null reference exception could result at run time. I don't know how many times I've found this type of error in my code at run time, and now the compiler finds these errors at compile time.
Incidentally, as you type the code above in the editor, the compiler initially puts a green squiggly under
cust in the
Dim statement with the message "unused local variable 'cust'." At first this seems a bit annoying because you've just added the variable, but it helps you keep your code cleaner in the end.
Instead of showing all errors in the Task List in the IDE, there's a new Errors List window that separates messages into errors, warning, and messages (see Figure 2 and Figure 6). You have some control over whether the compiler flags warnings or errors on the Compile tab in the application designer, which has checkboxes for either disabling all warnings or for treating all warnings as errors.
Use the Option Strict On statement to generate compile-time errors for several scenarios. Option Strict will flag your code if you attempt an implicit narrowing conversion (one in which data could be lost):
Dim iValue As Integer Dim lValue As Long iValue = lValue 'narrowing conversion error lValue = iValue 'ok
You also get an error if you use late binding. An object is late bound when it is assigned to a variable of type Object:
Dim myObj As Object Call myObj.method1() 'late binding error
Use of Option Strict is now considered a best practice for programming with Visual Studio. You should turn on Option Strict in your code wherever possible. You can turn on Option Strict in the Tools | Options menu, under the Project settings by selecting the checkbox for Option Strict. You can also put the Option Strict On statement at the top of an individual class or module file.
Explicit Array Bounds
You can now declare arrays using explicit array bounds to make the declaration clearer:
Dim a(10) As Integer 'old way Dim b(0 To 10) As Integer 'new way
Array bounds in Visual Basic are still zero-based, so you'll get a compiler error if the array lower bounds value isn't 0.
The Visual Basic 2005 language gains several major features and many smaller enhancements that together provide significant increases in ease of use and developer productivity. The language has become more complete, and there is more parity with C# on major features like XML comments and operator overloading.
Coupled with a myriad of IDE enhancements, Visual Basic 2005 is poised to be the best version of Visual Basic ever released. Not only better from a language feature standpoint, but it's also a sheer joy to use. I'm disappointed only because I can't build production applications with the Visual Basic 2005 today. Give the Beta version a try, and I think you'll agree.
Stan Schultes is a Visual Basic MVP and a long-time contributing editor to Visual Studio Magazine. Check out his site at http://www.vbnetexpert.com/ for more information on him and a complete list of his publications.