The newest version of Visual Basic now has support for full object-oriented programming, provides access to the .NET Framework and use power and flexibility of the Common Language Runtime.
Never have there been more reason for VB developers to consider making the move to Visual Basic .NET. Yet, amidst the excitement surrounding the .NET platform, some major productivity features have been lost in the shuffle.
The move from Visual Basic 6.0 to Visual Basic .NET is undeniably substantial. Visual Basic .NET offers more power and flexibility while maintaining its signature appeal and emphasis on productivity. The goal of this article is to demystify some of the new object-oriented programming (OOP) features of VB.NET as well as unearth some powerful productivity enhancements you may have previously overlooked.
The excitement surrounding the .NET platform has eclipsed some of the productivity features designed for the Visual Basic .NET programmer.
Visual Basic .NET is the result of a significant rebuild of Visual Basic for the .NET platform. The .NET platform consists of three major components: the Common Language Runtime (CLR), the .NET Framework, and integrated support for Web services. The CLR provides support for easy multithreading, memory management through garbage collection and built-in security. The .NET framework provides a vast array of built-in class libraries, basically eliminates the need to call methods from the Win32 API. Finally, Visual Basic .NET offers Web services by implementing components from the SOAP toolkit, providing XML serialization and adhering to the Web Service Description Language (WSDL) standard. This release is intended to bring the platform to the Visual Basic developer, or any developer, who wants a fast, RAD product.
The goal of Visual Basic will always be productivity. While the fundamental tools from the .NET platform give obvious productivity gains, what have the language and environment gained from this release? The excitement surrounding the .NET platform has eclipsed some of the productivity features designed for the Visual Basic .NET programmer.
Objects: What's the fuss?
Many considered the release of Visual Basic 4.0 to be the passage of VB into the universe of OOP. In that release, Microsoft added support for classes, modules and interface implementation. Yet without true class inheritance, it seems more appropriate to say that Visual Basic 4.0 supported some features of OOP but was not truly object oriented. Not so with VB.NET?there is no question that VB.NET is an object-oriented, first class language.
So what is the difference; what is true class inheritance? The net gain, pardon the pun, is simply code reuse.
The Visual Basic 6.0 interface-based development model requires each class to perform the full functionality the interface prescribes. Implementing an interface is basically like signing a contract; the object is expected to fulfill the requirements the interface lays out. But implementation of interfaces is only that?a contract. If several classes adhere to the same contract, the code to implement them has to be written or, more often, copy-and-pasted into the functions to fulfill those expectations.
With Visual Basic .NET and class inheritance, a class is defined once. Derived classes can be defined which inherit from it to directly reuse the implementation. The implementation contained in the base class is implicitly copied into child classes.
The Canonical Example
Class Employee
...
Sub New()
m_BaseSalary = 20000
End Sub
Sub UpdateAddress(ByVal addr As String)
' code to update address
...
End Sub
Property BaseSalary
' code to get/set base salary
...
End Property
End Class
It makes sense to define more specific types of employees so that these types can define functionality specific to their job. Both the Developer and the Marketer classes inherit from Employee. By inheriting from Employee, they gain all the functionality provided via the Employee class. The Developer and Marketer classes are both expected to be able to UpdateAddress and return their BaseSalary, yet the methods do not need to be rewritten to gain that functionality. All the Developer and Marketer classes have to do is inherit from Employee.
Class Developer
Inherits Employee
End Class
Class Marketer
Inherits Employee
End Class
Module Driver
Sub Main()
Dim d As New Developer()
Console.WriteLine("The default base " & _
"salary is: " & d.BaseSalary().ToString)
End Sub
End Module
The output of the above program would be: The default base salary is: 20000, which is the default set in the Employee's New method. Notice the definition of the Developer class is nearly empty?all of the functionality is implicitly copy-and-pasted from the Employee class declaration. Eventually the Developer and Marketer classes would be expanded to define functionality specific to their role and resources within the company.
There is no question that Visual Basic .NET is an object-oriented, first class language.
While this example may not be rocket science in terms of object oriented functionality, it does illustrate an important point: object inheritance enables you to write common code and reuse it directly without having to copy and paste. This not only provides things like encapsulation, a concept at the crux of OO, but it also saves time through implicit code reuse.
A Simple API
Visual Basic .NET allows function overloading, which gives developers the ability to create different versions of a Sub or Function that have the same name but take different argument types as parameters. Overloading a method enables you to keep your interface consistent. Methods are named based on their functionality rather than by the type of data passed in. Using the same name help you to remember what a procedure does, as opposed to having to come up with new names or a convoluted naming convention.
To overload a method in Visual Basic .NET, simply specify multiple methods with the same name and mark them with the Overloads keyword. These methods must be differentiated by signature, and their parameter types must be different. Note that having a different return type does not constitute a different signature.
At Microsoft, as at any company, it is not uncommon to incur job-related expenses for which you are reimbursed. As such, it makes sense for the Employee class to define a method that logs an expense to the Human Resources database. While most expenses, like buying office supplies, could be logged to the employee's account, some expenses, like taking a recruit out for dinner, might need to be logged to a specific recruiting account. The Employee class defines two methods to accept both of these situations. The first takes the cost of the expense and charges it to the Employee's default account. The second takes the cost and a specific account number:
Class Employee
...
Sub LogExpense(cost As Double)
' generic expense log to employee acct
...
End Sub
Sub LogExpense(cost As Double, acct As Integer)
' log to specified expense acct
...
End Sub
End Class
In this case, to write cleaner code, it is probably useful to get the account number for the employee within the first function and use the second, a specific LogExpense function, to do all of the database access.
Class Employee
' more code here
Sub LogExpense(cost As Double)
' generic expense log to employee acct
Me.LogExpense(cost, Me.Account)
End Sub
Sub LogExpense(cost As Double, acct As Integer)
' log to specified expense acct
...
End Sub
End Class
Allowing the name of the method to refer to the functionality gives the Employee class a much simpler design. The simplicity directly translates to productivity when creating a class library or an object model to be shared amongst members of your team. The method functionality becomes transparent by virtue of the names.
IntelliSense
Source code is more than just a serial stream of characters. It has structure, hierarchies, dependencies and references as well as control flow, logic and meaning. It contains a wealth of information that, given the ability to navigate and utilize it, can contribute enormously to programmer productivity.
The VB.NET compiler runs continuously in the background on a low priority thread, in effect performing a build every time you make an edit.
VB.NET automatically gathers rich structural information from your source code and uses it to power IntelliSense. IntelliSense encompasses a number of convenient features, such as Quick Info, close-parentheses highlighting, Parameter Info and statement completion. The statement completion feature has productivity benefits familiar to everyone. VB has had this feature for some time now. The Quick Info, Parameter Info and close-parentheses features, however, are new to VB.NET and they make a big difference. Take, for example, the Quick Info feature. There's no longer a need to perform a text search through your whole project or whole solution to find a class member's type information. By hovering your mouse over an identifier, the IntelliSense engine looks up the definition of that identifier and displays its accessibility and type. This is especially useful for constants because the constant value is displayed as well (see Figure 1).
Method information is equally important and perhaps more difficult to find with a text search due to of the possibility of multiple overloads. It's also important to know each overload's parameter names and types, as this can aid in understanding an unfamiliar component. For convenience, VB.NET's IntelliSense engine displays a scrollable method-tip with complete overloads and parameter information for every method, such as displayed in Figure 2. The time savings can be substantial. The cursor never needs to leave your current edit point because the essential information sits right at your fingertips. IntelliSense performs the navigation and searching for you.
Lastly, it can be a real nuisance to match close-parentheses inside complicated logical expressions or deeply nested function calls. No one wants their time wasted dealing with these kinds of mundane issues. VB.NET does the counting for you by highlighting the matching open-parentheses, saving you time and aggravation. It's a nice feature that has finally made a debut in the VB development environment.
Background Compilation
The process that gathers the data for use by the IntelliSense engine is called background compilation. That is, the VB.NET compiler runs continuously in the background on a low priority thread, in effect performing a build every time you make an edit. Not only does background compilation power IntelliSense, but it enables the “Pretty Lister” to automatically format your code (described below), populate the dropdowns (the navigation aides at the top of an editor window) and provide real-time compile errors. The last feature, unique to Visual Basic .NET, is perhaps the most significant advance from VB6. Whenever the compiler detects a change in source code, it re-runs the compilation and instantly presents any errors detected as a blue squiggle lines underneath the offending text (the compiler also adds the errors into the Task List window). This direct and immediate feedback helps to not only reduce the time you spend detecting and fixing errors, but by association helps you avoid future errors.
For those of us in large teams who struggle with coding-style conventions and documents, the automatic code-formatting feature of VB.NET is a dream come true.
Immediate error reporting also works across projects, which can be a major productivity boost. Imagine the following scenario: you are developing a random number generating component called Rand with a class member GetNext, which returns the next random number. In the same solution you have another VB project that consumes this component called Engine. If you edit the Rand class, such as changing the accessibility of GetNext from Public to Private, you can observe the effects of this edit right away (which is an error at every use of GetNext because it has become inaccessible). Figure 3 shows this behavior.
Also notice in Figure 3 that a few other simple mistakes have been made in the Engine component (which was clearly written by someone else). The variable accum has been misspelled acum and the variable index, which is type Decimal, is being implicitly used as an Integer. The syntax of the statement is correct, but because the background compiler performs an entire compilation, it makes all the necessary type checks to detect the errors. These kinds of simple errors are undetectable in conventional tools, such as VB6, without manually starting a new build.
What does all of this mean? It means that for the first time the compiler is truly interactive. Building the solution is no longer a separate step of the development process. Just write the code and fix the errors you (accidentally) create when you create them. Then press F5 and your program will run. It's so simple and will save you a lot of time.
The Pretty-Lister
As mentioned above, the background compiler supplies rich contextual information to several components of the editor, one of which is the Auto-Formatter/Pretty-Lister. The Pretty-Lister is responsible for making sure identifiers have consistent case (VB's case-insensitivity is a huge productivity boost; there, it's said) and that appropriate white space gets inserted between operators and keywords.
The Pretty-Lister's job also includes consistent code formatting. For those of us in large teams who struggle with coding-style conventions and documents, the automatic code-formatting feature of VB.NET is a dream come true. Based on the current context, the editor indents bodies of code to the appropriate nesting level. Programmers no longer have to break concentration and spend precious brain cycles worrying about the TAB key. Auto-formatting also makes it really easy to spot errors with End constructs such as “End Sub” and “End If” because it becomes completely obvious when there are too many or too few.
VB.NET provides another way to hook up events?declarative event handling?that makes event handling part of the class definition.
The importance of consistent formatting cannot be overestimated. Consistent formatting helps increase code maintainability; this is why code-style documents exist. Any help the code editor gives to the programmer in this regard directly impacts productivity because it lets the programmer concentrate on more important things, like architecture and algorithms.
Declarative Event Hookup using WithEvents
The .NET Event model is built upon delegates, which are basically type-safe function pointers. Raising an event is simply invoking one of these delegates. The difficulty is that the event must be “hooked-up,” that is, the object sourcing the event must be made aware at runtime of the delegate it needs to invoke when an event gets raised. The AddHandler and RemoveHandler statements, used for dynamically hooking-up events at runtime, take care of all these necessary details:
Class EventSource
Event Click()
...
End Class
Class Driver
Shared Sub Main()
Dim x As New EventSource()
AddHandler x.Click, AddressOf OnClick
...
End Sub
Sub OnClick()
Console.WriteLine("Click Event raised")
End Sub
End Class
This is the extent of event handling support in other languages. You must manually hook up and unhook events with statements located somewhere in a method or constructor. This keeps you from quickly determining event handling behavior due to the fact that the events in which a class handles can vary at runtime. This can produce subtle and hard-to-detect bugs.
VB.NET provides another, faster, way to hook up events with a technique called declarative event handling. Declarative event handling makes event handling part of the class definition. In many ways, this is more convenient because event handling can be built into your component architecture and results in more manageable code.
Declaratively handling events is a two-step process in VB.NET. First, the object that is going to raise events must be stored in a field with the WithEvents modifier. This indicates the field's availability for declarative event hookup. Then, methods can state they handle a particular event raised by the object by specifying a Handles clause in the method declaration. For example:
Class Driver
WithEvents x As New EventSource()
Sub OnClick() Handles x.Click
Console.WriteLine("Click Event raised")
End Sub
End Class
In this example, OnClick visibly handles the Click event raised by the object stored in field x. This makes it easy for you to quickly analyze the Driver class and recognize its event handling characteristics. Furthermore, multiple events can be handled with the same handler by specifying additional events in the Handles clause:
Sub OnClickAndKey() Handles x.Click, x.KeyPress
Console.WriteLine("events raised")
End Sub
See how much easier this is than the AddHandler and AddressOf approach? This is an example of the gestalt of VB.NET?an easy approach to handling the 80% case, with the capability of handling the 20% case as well.
Type-less Programming
When you are writing code and your variable types don't matter, you are practicing type-less programming. This is true of Variants in VB6, in languages like Visual FoxPro, and of the type system in functional programming languages such as LISP.
Type-less programming has a huge affect of making all versions of Visual Basic a rapid prototyping tool.
Type-less programming, often called “late binding,” is all about flexibility. It's perfect for building quick prototypes or function stubs when the final details, such as return types and parameter types, haven't been decided. When designing VB.NET, one of the goals was to continue to support type-less programming in similar fashion to VB6. Type-less programming has a huge affect of making all versions of Visual Basic a rapid prototyping tool.
The switch that controls this type-less programming behavior is Option Strict. With Option Strict off (the default), an Object can be used as a general type. With Option Strict on, the developer must explicitly specify the type of each expression. This may involve adding CTypes, or conversion expressions, in key places or changing the types of variables. Aside from simple conversions, type-less programming also entails late binding.
When compiling a piece of source code, the compiler must decide what each identifier represents. This process is called binding because it “binds” the string of characters to an actual method or member of a class. Here's an example:
Class Class1
Sub Print()
End Sub
End Class
Module Driver
Sub Main()
Dim cls As Class1 = New Class1()
Dim obj As Object = New Class1()
...
cls.Print 'call determined at compile time
obj.Print 'call determined at run time
End Sub
End Module
When analyzing the text “cls.Print,” the compiler first determines what type “cls” refers to, which is Class1. Then it takes the string “Print” and looks in Class1 for a member called Print. If Class1 contains such a member, then “cls.Print” has been successfully bound, and the intermediate language (IL) for this expression can be emitted. If not, the compiler generates an error that Print is not a member of Class1.
Most of this binding work occurs at compile time; the compiler has determined exactly what members to call by the time it creates an .EXE or .DLL. Sometimes, however, this information is impossible to determine until the program actually runs, so the compiler defers binding until run time. The process of binding deferral is called late binding. You're most likely doing late binding anytime you use “.” on an Object variable. Late binding can also be thought of as type-less programming because type constraints are considered only at runtime.
When analyzing the text “obj.Print” from the code above snippet, the compiler determines that obj is an Object variable. Depending on how the program runs, obj can hold an instance of any type.
Because of this, the compiler has to treat obj.Print as a late bound call and wait until run time to determine what it does. The mechanism that determines what to call at run time is called the VB Late Binder. If you were to analyze the IL that the compiler generates for the obj.Print call, you would see that the compiler is, metaphorically speaking, sending a message to the VB Late Binder which says, “You need to perform a call to Print and here is all the information I have.” The VB Late Binder, contained within Microsoft.VisualBasic.dll and written entirely in Visual Basic, looks at the type contained in obj and tries to find a member called Print. If it succeeds, it makes the call. If it fails, the VB runtime throws a MissingMember exception.
You can also dynamically load .NET assemblies and easily use their containing types without having to deal with the complexities of reflection and method invocation. This code snippet dynamically loads an assembly called WidgetLibrary and instantiates an object of type Widget:
Imports System.Reflection
Module Driver
Sub Main
Dim lib = [Assembly].Load("WidgetLibrary")
Dim obj = lib.CreateInstance("Widget")
obj.PerformSomeTask()
End Sub
End Module
The beauty here is that you can use Widget just as you would any other type, except the compiler knows nothing about it when it builds the .EXE. All the method binding is performed at run time on an as-needed basis.
Keep in mind that this flexibility comes at a sacrifice of some performance. But when you need to get a project off the ground as quickly as possible, late binding can be just the right catalyst.
Exposing .NET Classes to COM with the ComClass Attribute
Exposing a managed class to COM by hand is a confusing process that requires a good deal of COM programming knowledge. To ease this process, you can use a tool called TLBEXP, which comes with the .NET Framework SDK, to expose your classes to COM. However, TLBEXP auto-generates class interfaces that include all public members in your class hierarchy, all the way to Object. This makes the interface rather susceptible to versioning issues, especially when any of the base classes change. TLBEXP also never generates event interfaces. As an easy alternative, the ComClass attribute instructs the VB.NET compiler to handle most of the complexity involved in exposing your classes to COM.
Listing 1 demonstrates how to use the ComClass attribute. Simply attach it to your public class and specify any optional GUIDs you would like used. The rest is taken care of for you. Listing 2 reveals what actually happens when you apply a ComClass attribute to your class. The details are beyond the scope of this article. However, what happens is that a class interface and event interface gets created and each of the corresponding members are decorated with the necessary attributes to specify DispIDs, default interfaces and GUIDs. TLBEXP then uses this information to build the correct COM description of your component.
The ComClass attribute instructs the VB.NET compiler to handle most of the complexity involved in exposing your classes to COM.
Because the compiler performs all of this work for you, there is less complexity in your code and less opportunity for error. Just as with VB6, exposing your classes to COM is not a difficult process. Instead, you have more time to spend on the important things, like architecture and algorithms.
That Warm and Fuzzy Feeling
Visual Basic .NET is undeniably a departure from previous versions. From running on top of a new platform, to gaining full object-oriented status, the Visual Basic of the next millennium is somewhat different from days of yore. However, the focal point for all versions of Visual Basic, past, present and future is and will always be making you a better, more productive programmer.
Listing 1: Before ComClass attribute expansion
Imports Microsoft.VisualBasic
<ComClass("371A7EC2-A0F0-491A-9364-AFD6DCDFCA5D", _
"B6D9002C-59D9-4FF9-ABF0-F85EFCB12876", _
"AA49B4FB-36F6-4B59-9DE6-CA75E3677EB8")> _
Public Class Class1
Function Initialize() As Integer
End Function
Event Click()
End Class
Listing 2: After ComClass attribute expansion
Imports System.Runtime.InteropServices
<Guid("371A7EC2-A0F0-491A-9364-AFD6DCDFCA5D"), _
ComSourceInterfaces(GetType(Class1.__Class1)), _
ClassInterface(ClassInterfaceType.None)> _
Public Class Class1 : Implements _Class1
<Guid("B6D9002C-59D9-4FF9-ABF0-F85EFCB12876")> _
Interface _Class1
<DispId(1)> Function Initialize() As Integer
End Interface
<Guid("AA49B4FB-36F6-4B59-9DE6-CA75E3677EB8"), _
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)> _
Interface __Class1
<DispId(1)> Sub Click()
End Interface
Function Initialize() As Integer Implements _Class1.Foo
End Function
Event Click()
End Class