Moving up the technology stack: VB6 migration reality check

This article was originally published in VSJ, which is now part of Developer Fusion.
When you program, you will inevitably have to write two types of code. Some of the code you write will resolve certain technological issues, while the other will deal directly with the problem domain you are trying to address. The code you write to resolve technological issues has no direct business values for you or your client, so you will generally want to minimize the quantity of such code in your application. For this purpose, you can use some third party products, like components or frameworks, write your own reusable components or frameworks, or as the simplest alternative of all when it is available, use the functionality provided by the platform itself, in this case .NET framework.

You can also use the same mechanisms to try to minimize the quantity of code from business domain, but when dealing with domain code much more customization is likely to occur. For example, take a look at this code:

If client.VIP Then
	order.Discount = ClientDiscounts.VIP
End If
It is clear that these lines of code embed a relevant and important business rule. If you were talking to your client about it, you could most probably explain without difficulty what the code is doing: “If the client is VIP, then apply VIP discount to his order” is pretty much obvious even for a non-programmer reading the code.

On the other hand consider the following few statements:

Dim connection New SqlConnection( _
	"Data Source=SHOPSERVER;" + _
	"Initial Catalog=ONLINESHOP)"
Dim command = New SqlCommand
strSql = "Update Client Set Email = " _
	+ "@Email Where ClientId = @Id"
command.Parameters.AddWithValue( _
	"@Email", TxtEmail.Text)
command.Parameters.AddWithValue( _
	"@Id", TxtId.Text)
connection.Open()
command.Connection = connection
command.CommandText = strSql
command.ExecuteNonQuery()
connection.Close()
This code is updating client data in the database, persisting new client email inside Client table from ONLINESHOP database. If you tried to explain this code to your client using the same words, you would probably get some questioning glances. Or, if your client is of more inquisitive sort, he just might rightfully ask you: “But why do you have to persist data?” A perfectly valid question if you think about it. The reason why you persist data is because computer RAM memory is volatile, so the application would lose data each time the computer is shut down. By persisting data to database, you are making sure that data is written to a disc drive. This device is capable of surviving power outage so no data is lost. It is a technological problem that we are resolving with this code. Your client is concerned not to lose data, but how exactly this problem is solved is rarely of much interest to him.

This leads us to the following conclusion: choosing a platform that solves more typical technological problems will make you more productive and will simplify the development and maintenance process. With a more complete platform, both you and your client win.

If you programmed Web Services using the Microsoft SOAP toolkit in VB6 and then programmed web services in .NET, you will know exactly what I am driving at. With the SOAP toolkit there is much more “plumbing” involved than with VB.NET.

A similar shift is now happening with Microsoft .NET framework 3.5 and Visual Studio 2008. Before I explain the impact that new technologies such as LINQ have on VB6 code migration, let’s see how the VB6 migration to .NET is performed in a pre-.NET 3.5 environment.

Upgrading your VB6 code to pre-3.5 .NET employing refactoring

In a previous VSJ article, Refactoring – the elixir of youth for legacy VB code I provided a number of recipes for transforming VB6 code to Visual Basic .NET and pointed out that VB6 to VB.NET transformation has two essential stages: migration and upgrade. During migration, only minimal changes to code are performed in order to make the code execute in exactly the same manner in the VB.NET environment. This stage is to some extent (far from completely) performed with the help of VB Migration Wizard tool that comes bundled with the Visual Studio IDE. In practice, this stage unfortunately often turns out to be the only stage in transformation process. During upgrade, the next stage in the transformation process, freshly migrated code is restructured so that it can take advantage of all the benefits of VB.NET as a strongly statically typed and fully object-oriented language, compared to one generation older object-based VB6. Since migrated VB6 code can have a strong resemblance to poorly structured VB.NET code, I have demonstrated how refactoring can be used to inject new life into this code by improving its design and restructuring it along object oriented design principles and harvesting features available in VB.NET only.

A new style of application development

Some of the new features in VB 9 are XML literals, local variable type inference, extension methods, lambda expressions and of course LINQ. With .NET framework 3.5 you get a number of implementations that are making use of these new language features. For example, LINQ to XML is a new XML programming interface that will let you query XML document using LINQ syntax; LINQ to SQL will let you access relational database in the same way.

New language features and frameworks emerging in the wake of .NET 3.5 provide an excellent mix for new application design paradigm. Let’s take a look at these acronyms and see how they can help you design applications in efficient and robust manner.

POCO (Plain Old CLR Object) is used to refer to classes that are not forced to inherit other classes or implement interfaces just to make use of environment in which they reside. Not so classes you will use to program your COM+ components. To run your component inside the COM+ environment, your class will have to inherit the System.EnterpriseServices.ServicedComponent. For example, take a look at this mock COM+ component class:

Imports System.EnterpriseServices

Public Class MyCOMPlusComponent
	Inherits ServicedComponent
	'...
End Class
The problem with such a class is that it is seriously limiting our flexibility:
  • Your class can now run only in serviced environment
  • Your class cannot inherit any other class; thus reuse potential is severely reduced
POCO means you do not have to inherit any other class in order to have your class serviced by a lightweight container environment. This results in greater freedom when designing your domain classes. Neither need they be tarnished with persistence code. This means your classes will be much more reusable with a clean cut separation of concerns.

Object Relational Mapping (ORM) does persistence for you. In theory, you map your class to table(s) in the database and you let the ORM do its magic. You do not write SQL or stored procedures. All this work is performed by the ORM. Not only have you greatly reduced the amount of code you have to write and maintain, but you generally get advanced features like caching, lazy loading and transactions out of the box. ORM will generally let you switch from one database vendor to another with minimum hassle and even generate a database schema for you. When you write your own SQL, it is notoriously difficult to keep it database neutral.

Dependency Injection (DI) can help minimize dependencies in your application – adhere to the “Program to Interface not Implementation” principle and this will facilitate unit testing your application. When using DI, you program a set of services and then assemble your application with the help of the DI framework. Information on concrete implementation for dependencies is kept in configuration files or in your source code in the form of attributes. Switching from one implementation to another can be as easy as changing the configuration file. Many DI frameworks go further than that and offer additional services by way of a lightweight container concept. So you can get many of the services that heavyweights like MTS provide but without making any sacrifices in your design.

These concepts fit well together in creating a new paradigm for robust and flexible application design that promotes rapid development and rich domain models. If you are just starting to work on a new application, especially if you are using latest version of Visual Studio and .NET framework, you have probably considered at least some of approaches I have mentioned. If not, you will be well advised to do so.

But how does this all fit with legacy application upgrade? After all, a few years back we used to write our own persistence code, MTS was promoted as a panacea and there was no inheritance in VB6. In order to explore this issue, I will examine one such legacy application and will try to upgrade it to .NET under this new design formula.

Engine-Collection-Class design pattern

One example of a more elaborate design in VB6 is the Engine-Collection-Class (ECC) pattern. A sample application illustrating this pattern is available for download from MSDN. To download it, type “DesiPat.exe” in the MSDN search box, since URL is pretty much unrepeatable. This pattern provides a structured and uniform approach to application construction in VB6. Its aim is to provide guidance and best practices and to streamline development of enterprise applications in VB6. It also provides an object-oriented approach to application design in VB6, despite VB6 (or COM for that matter) limitations in this department.

The basic premise of the pattern can be described with following statement: Each domain class from your conceptual model is implemented as Class, Engine and Collection classes in your implementation model. These three classes are closely linked so I will refer to them as the “ECC triad”. You can appreciate relationships between them in Figure 1.

Figure 1
Figure 1: Engine-Collection-Class static structure diagram depicting the relationship between three classes in the pattern

As you can see, Engine depends upon both the Class and the Collection, while Collection can hold many instances of Class. Let’s take a closer look at the role each of these classes is fulfilling in this model.

  • Engine serves as a sort of faade, an entry point that all clients will use in order to get hold of Collection instances. Both Collection and Class are marked as “PublicNotCreatable” so Engine is the only class clients can instantiate. It also implements a search and retrieval methods that will search Class instances in the database and use search result data to fill the Collection that is then returned to the client. In addition, it is capable of creating a new empty Collection that can be used to create new instances of Class and to persist them to the database later on.
  • Collection is a type-safe wrapper over the original VB6 Collection class. For example, Item property of Collection will return a specific Class instance, not just any object. Collection also implements methods that will let you update or delete Class instances and then persist these changes to database. The method used to commit changes to the database is called “Update”.
  • Class is used to embed business or domain rules. So one Class can have another Class or Collection of another Class as a property, when there is some kind of association between Classes. Class also has a number of persistence-related properties like: Dirty, IsNew and DeleteFlag that signal correct operations to Collection when committing changes to the database. A Class is generally mapped to one or more tables in the database, either directly or by calling appropriate stored procedures or views.
Now that you understand the role of classes in this pattern, let’s see how you can upgrade an application developed using it to .NET 3.5.

Upgrading an Engine-Collection-Class application to VB 2008

Since many ECC sample applications are concerned with database persistence, I will make use of LINQ to SQL technology to implement persistence in the application. I will also make my domain classes adhere to the POCO principle as much as possible. Finally, I will try to make use of VB.NET language features like structured error handling, inheritance and generics to name a few. With this in mind, let’s take a look at an EEC sample.

Each application constructed along ECC design pattern will consist of number of ECC triads. For illustration purposes, I will upgrade only one such ECC triad to VB 2008. Since uniformity is a strong characteristic of the pattern, a similar approach to upgrade can be applied to any such triad. If you have downloaded the example from MSDN site, you can take a look at IOBPUserInfo project used to maintain user information. The project is a component that consists of three classes:

  • IOBPUserInfoEng – represents Engine in ECC
  • ColUserInfo – represents Collection in ECC
  • CUserInfo – represents Class in ECC
This project uses number of modules and components that provide some utility functions related to error handling, security management and database operations. For example, there is a Data Access Engine component that encapsulates execution of stored procedures on the application database. These are less relevant for the purpose of upgrade since .NET framework provides many similar and out-of-the-box alternatives.

Upgrading Class

Since business-related code is contained inside Class, let’s start by upgrading CUserInfo. This code has greatest value for the client and therefore it is important to preserve it without changing any of the business rules from the original application. First take a look at CUserInfo code. I will omit a lot of less relevant code here, but if you are interested in taking a look at complete application, and in giving it a test ride, download the sample application from the MSDN site. So let’s take a look at the code:
'Set to true when class changes
Private mblnDirty As Boolean
'Set to true when new class first initialized.
Private mblnIsNew As Boolean
Private mblnDeleteFlag As Variant
Private mlngUserNumber As Variant
Private mvarUserID As Variant
Private mvarPassword As Variant
Private mvarName As Variant
Private mvarAddress As Variant
Private mblnDisabledFlag As Variant
Private mvarRecordTimestamp As Variant
Private mcolAccount As Object
'etc

Public Property Let EmailAddress(ByVal vData As Variant)
'...
	mvarEmailAddress = LetUIValue(vData, vbVariant)
	Me.Dirty = True
'...
End Property

Public Property Get Account() As Object
	On Error GoTo ErrorHandler
	Dim engAccount As Object
	Dim ErrorNum As Long
	If mcolAccount Is Nothing Then
		If Not SafeCreateObject(engAccount, _
			OBP_ACCOUNT_ENG, ErrorNum) Then
				Err.Raise ERR_ACCOUNTSAFECREATEFAILED, _
				"IOBPUserInfoEng.Account Property", _
				"Unable to create "& OBP_ACCOUNT_ENG & _
				". Return Code was: " & ErrorNum
			GoTo CleanExit
		End If
'Only get the non-deleted records.
		Set mcolAccount = engAccount.Search( _
		SecurityToken:=SecurityToken, UserNumber:=Me.UserNumber)
	End If
	Set Account = mcolAccount
	CleanExit:
'See error handling code
'in Property Get Dirty() routine
End Property

Public Property Get Dirty() As Variant
	On Error GoTo ErrorHandler
	Dirty = mblnDirty
CleanExit:
	Exit Property
	ErrorHandler:
	Dim lErrLine As Long
	Dim lErrNumber As Long
	Dim strErrSource As String
	Dim strErrDescription As String
'Preserve Error Information
	lErrNumber = Err.Number
	lErrLine = Erl
	strErrSource = Err.Source
	strErrDescription = Err.Description
	Call HandleException(SecurityToken, _
		lErrNumber, strErrDescription, lErrLine, strErrSource)
	Exit Property
End Property

Friend Property Let Dirty(ByVal vData As Variant)
	On Error GoTo ErrorHandler
	If VarType(vData) <> vbBoolean And IsEmpty(vData) Then
'raise error
		Err.Raise ERR_USERINFOINVALIDDIRTY, _
			"CUserInfo.Dirty LET", _
			"Invalid Data Type Expected Boolean."
		GoTo CleanExit
	End If
'Set Value
	mblnDirty = vData
	CleanExit:
'See error handling code in Property Get Dirty() routine
End Property
Basically, CUserInfo class has a number of properties. These are used to record important personal data like user name, password, name, address, phone, city, postal code, email, time of last login etc. There is also Account property that will let you obtain all Account objects that belong to a certain user. We could say that these properties make up a core of business-related logic expressed in a UserInfo triad and consequently this is the code that has the biggest value from business point of view.

Besides these business logic related properties, CUserInfo has some properties relevant for persistence logic. A number of Boolean flag properties are used to indicate the state of User object in relation to persistence. IsNew indicates that instance is new and that should be saved to database, Dirty indicates the value of some property has been changed so that update of the instance is necessary. DeleteFlag indicates that an instance should be eliminated from the database.

As you can see, the code is quite verbose and written in “defensive” style, with a lot of emphasis on error handling code. You will also appreciate that all public members expose VB6 Variant or Object type in the signature. This works against static type checking but is necessary in VB6 in order for this component to be used by scripting clients. One such client is ASP page and since ASP technology uses weakly typed VBScript or JScript, UserInfo has to expose Variant type in member signatures.

When upgrading CUserInfo, I will preserve business-related properties, but I will eliminate persistence-related properties. Using LINQ to SQL, all persistence-related control is implicit and performed by the framework itself. That way, our entity classes can preserve “persistence ignorance”. In order to make CUserInfo managed by LINQ to SQL, we need to provide some metadata to the framework. For example, we need to indicate which table in the database the class is mapped to. This can be done by means of some configuration files, or by applying attributes to CUserInfo class are we’ll choose the latter:

<Table(Name:="UserInfo")> _
	Public Class UserInfo
We also need to provide information on property to column mappings.

Take a look at the listing below to see CUserInfo code. By the way, the class has been renamed as UserInfo as “C” prefix is not recommended by new .NET framework naming guidelines.

Option Explicit On
Option Strict On
Imports System.Data.Linq.Mapping

<Table(Name:="UserInfo")> Public Class UserInfo
	Private userNumberValue As Long
	Private userIdValue As String
	Private nameValue As String
	Private violationCountValue As Long
	Private emailAddressValue As String
	Private userTypeIdValue As Long
'Private accountsValue As List(Of Account) - (not yet upgraded)
'other private variables...

<Column(Name:="UserNumber", DbType:="int", CanBeNull:=False, IsPrimaryKey:=True, _
	IsDbGenerated:=False)> Public Property UserNumber() As Long
		Get
			Return userNumberValue
		End Get
		Set(ByVal value As Long)
			userNumberValue = value
		End Set
	End Property

	<Column(Name:="Name", DbType:="NVarChar(50) NOT NULL", CanBeNull:=False)> _
	Public Property Name() As String
		Get
			Return nameValue
		End Get
		Set(ByVal value As String)
			nameValue = value
		End Set
	End Property
	'Other properties...
End Class

Eliminating Collection

Collection is basically a type safe wrapper over the VB6 Collection container. Methods like Clear or property Count delegate execution to internal collection:
Private mCol As Collection
' Used to store the Classes
	Public Property Get Count() As Variant
		On Error GoTo ErrorHandler
		If mCol Is Nothing Then
			Count = 0
		Else
			Count = mCol.Count
		End If
		'...
	End Property
ColUserInfo Collection class also has a number of persistence-related methods. Take a look at the listing below for Delete method. This method is calling spDUserInfo stored procedure, a simple SQL DELETE statement, again programmed in verbose and defensive manner so it is more than 30 lines long. The rest of stored procedures also implements simple CRUD operations.
Public Function Delete(Optional ByVal Index _
		As Variant) As Variant
	On Error GoTo ErrorHandler
	Dim ErrorNum As Long
	Dim oDALEng As IOBPDA.IOBPConnection
	Dim oCUserInfo As CUserInfo
	Dim vParameters() As Variant
	Dim LowerLimit As Long
	Dim UpperLimit As Long
	Dim inx As Long
	Dim SPName As String
	Delete = False
	If Me.Count = 0 Then
'Nothing to Update
		Delete = True
		GoTo CleanExit
	End If
'If Index is supplied then Delete only the supplied record
	If Not IsMissing(Index) Then
		If Index < 1 Or Index > Me.Count Then
			Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _
			"ColUserInfo.Delete PROC", "Index out of range"
			GoTo CleanExit
		Else
			LowerLimit = Index
			UpperLimit = Index
		End If
	Else
		LowerLimit = 1
		UpperLimit = Me.Count
	End If
	If Not SafeCreateObject(oDALEng, _
		OBP_DA_CONNECTION, ErrorNum) Then
			Err.Raise ERR_USERINFOSAFECREATEFAILED, _
				"ColUserInfo.Delete PROC", "Unable to create " _
				& OBP_DA_CONNECTION & _
				". Return Code was: " & ErrorNum
		GoTo CleanExit
	End If
	For inx = UpperLimit To LowerLimit Step -1
		Set oCUserInfo = Me.Item(inx)
		If Not oCUserInfo.IsNew Then
'Delete from DB If Not New
			ReDim vParameters(PARMUBOUND, 1)
			With oCUserInfo
				.ClassStorage = True
'Fill Parameter Array
				vParameters(PARMNAME, 0) = _
				PARMNAMESP_USERINFOUSERNUMBER
				vParameters(PARMTYPE, 0) = _
				PARMTYPESP_USERINFOUSERNUMBER
				vParameters(PARMLENGTH, 0) = 4
				vParameters(PARMDIR, 0) = adInput
				vParameters(PARMVALUE, 0) = .UserNumber
				vParameters(PARMNAME, 1) = _
				PARMNAMESP_USERINFORECORDTIMESTAMP
				vParameters(PARMTYPE, 1) = _
				PARMTYPESP_USERINFORECORDTIMESTAMP
				vParameters(PARMLENGTH, 1) = 8
				vParameters(PARMDIR, 1) = adInput
				vParameters(PARMVALUE, 1) = .RecordTimestamp
				.ClassStorage = False
			End With
			If Not oDALEng.Execute(SecurityToken, SP_D_USERINFO, _
					vParameters) Then
				Err.Raise ERR_USERINFODALDELETEFAILED, _
				"ColUserInfo.Delete PROC", _
				"Delete Failed. SPName was: " & SP_D_USERINFO
				GoTo CleanExit
			End If
		End If
		Set oCUserInfo = Nothing
'Remove from Collection
		mCol.Remove (inx)
	Next
	Delete = True
	CleanExit:
		Erase vParameters()
		Set oDALEng = Nothing
		Set oCUserInfo = Nothing
		Exit Function
	ErrorHandler:
		'...
End Function
When using LINQ to SQL, you will generally let the framework generate the SQL “under the bonnet”. Also, in VB.NET since the 2005 version you can use generic type-safe containers provided by the .NET framework.

What does this tell us? You have no need for Collection class or simple CRUD stored procedures in VB 2008! So, neither the Collection class nor related stored procedures will be upgraded. You have just saved yourself from upgrading or maintaining a huge quantity of code.

Upgrading Engine

IOBPUserInfoEng contains code that queries the database and populates collection with UserInfo instances. For this purpose it uses parameterized stored procedures that contain SQL query code. Similar to persistence related code in Collection class, this code can be replaced by functionality provided by LINQ to SQL System.Data.Linq.DataContext and System.Data.Linq.Table classes in the .NET framework. We can turn easily turn or Engine class into the thin wrapper over DataContext and Table classes. Take a look at the listing below for upgraded Engine code which uses the more straightforward name “UserEngine” for Engine class.
Option Explicit On
Option Strict On
Imports System.Data.Linq
Public Class UserInfoEngine
'TODO: read connection string from configuration
Private Const ConnectionString As String = "Data Source=TESLATEAM;" & _
	"Initial Catalog=OnlineBill; Integrated Security=True"

	Private contextValue As DataContext
	Private userInfoTable As Table(Of UserInfo)

	Public Sub New()
		contextValue = New DataContext(ConnectionString)
			userInfoTable = contextValue.GetTable( Of UserInfo)()
	End Sub

	Public ReadOnly Property Table() As Table(Of UserInfo)
		Get
			Return userInfoTable
		End Get
	End Property

	Public Sub SubmitChanges()
		contextValue.SubmitChanges()
	End Sub
End Class

Model, unit test and implement

In a typical VB6 application, huge quantities of existing code have been made functionally obsolete by new technologies available in Visual Studio 2008. Your legacy code is likely to suffer from number of problems:
  • Code proliferation and duplication because of limited object-oriented capabilities in VB6
  • Obsolete coding and naming conventions coupled with verbosity of VB6 syntax
  • Poor structure, long methods and use of structured programming constructs like global functions in VB6 Module
  • Legacy error handling
  • Business logic spread between VB and SQL code
Depending on the state of application, it is possible that much of your legacy code is not actually used in production any more, meaning it is dead code that is still part of your code base but in fact never gets executed. It is also possible that old applications are ridden with bugs as a result of years of maintenance and development of new features.

All these deficiencies of legacy code coupled with benefits of VB 2008 imply that integral upgrade processes are not practical any more. Instead, you should only reuse those parts of code that still have direct value from a business point of view. This means that as a part of the upgrade process you should go back to the drawing board. The process can be performed in following steps:

  1. Start by modeling your problem domain in object-oriented terms
  2. Write unit tests for each functionality your system should implement.
  3. Implement functionality under test by upgrading specific sections of code
By driving your upgrade process by unit tests, you will be able to weed out all the dead code your application might have accumulated during years of maintenance.

Conclusion

The latest versions of Visual Basic and the .NET framework are changing the VB programming landscape. New features bring new levels of productivity and expressiveness. In this new landscape, integral upgrade of legacy VB6 applications is rapidly losing its purpose. Those still upgrading legacy VB6 applications mechanically are at risk of producing a lot of boilerplate code that should not even be written any more. Such an approach will hinder productivity, make maintenance cumbersome and will stand in the way of the implementation of new features. Instead of an indiscriminate upgrade of your VB6 code, go back to the problem domain, rethink your design and let unit tests guide you while you upgrade only those pieces of code that are relevant from the business point of view.


Danijel Arsenovski is the author of Professional Refactoring in Visual Basic, published by Wrox. Currently, he works as Product and Solutions Architect at Excelsys S.A, designing Internet banking solutions for numerous clients in the region. He started experimenting with refactoring while overhauling a huge banking system, and he hasn’t lost interest in refactoring since. You can find his blog at blog.vbrefactoring.com.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” - Martin Fowler