|
|
Generate Large Blocks of Code With MacrosGenerating Error Free Code With Macros | | Would you like to be able to generate a Class with hundreds of lines of Bug-Free code without buying a third-party tool? This article will show you how to do it with a Macro. When someone mentions Extensibility, Automation, or Add-Ins, some developers have never bothered to investigate the power associated with these terms. When I showed a developer a Macro recently, he said, "I used to write Macros", and looked as if he had moved past that phase of his .NET experience. But, actually, Macros are a great tool for generating large blocks of bug-free code. In this article I am going to show you a macro that will generate an entire complete class to process an input row from a fixed-length field data file. I often use this type of object in processing fixed field input files.
Remember the Type Statement in VB6? It has now been replaced by the Structure statement in VB.NET. But in VB6, we often used the Type Statement to define record layouts. In the Type, as shown below, we also used fixed length strings to define the length of input and output fields.
Figure 1 - VB6 Type Definition.
Type InputRecord
RecordId As String * 2
LastName As String * 15
FirstName As String * 25
MiddleName As String * 15
Address1 As String * 35
Address2 As String * 35
City As String * 25
State As String * 25
Zip As String * 10
HomePhone As String * 12
WorkPhone As String * 12
EmailAddress As String * 50
LicenseNumber As String * 15
DLState As string * 2
End Type
|
If you allow the .NET Migration tool to migrate an application from VB6, and it contains Type structures as shown above, you will probably not get anything nearly as useful as the Type shown above was in VB6. I am going to use a macro to convert the old VB6 Type definition into something very useful in .NET. This will work in VB.NET or C# if you convert the macro to generate C# code.
First, I will show you the Class created from the Macro and describe its usage. Then I will show you the Macro itself and how to use it. Figure 2 shows the Class created by the Macro. The macro does not actually create a class and add it to the project, but that would only require a few more lines of code in the macro. Instead, the macro simply adds a Text file to the IDE and creates the code for the class in that window. I manually added a class to the project and copied the code from the Text Window to overlay the new Class.
Figure 2 - Input Class Created by a Macro
Public Class InputRecord
Private _RecordId As String = String.Empty
Private _LastName As String = String.Empty
Private _FirstName As String = String.Empty
Private _MiddleName As String = String.Empty
Private _Address1 As String = String.Empty
Private _Address2 As String = String.Empty
Private _City As String = String.Empty
Private _State As String = String.Empty
Private _Zip As String = String.Empty
Private _HomePhone As String = String.Empty
Private _WorkPhone As String = String.Empty
Private _EmailAddress As String = String.Empty
Private _LicenseNumber As String = String.Empty
Public Property RecordId() As String
Get
Return _RecordId
End Get
Set(ByVal Value As String)
_RecordId = Value
End Set
End Property
Public Property LastName() As String
Get
Return _LastName
End Get
Set(ByVal Value As String)
_LastName = Value
End Set
End Property
Public Property FirstName() As String
Get
Return _FirstName
End Get
Set(ByVal Value As String)
_FirstName = Value
End Set
End Property
Public Property MiddleName() As String
Get
Return _MiddleName
End Get
Set(ByVal Value As String)
_MiddleName = Value
End Set
End Property
Public Property Address1() As String
Get
Return _Address1
End Get
Set(ByVal Value As String)
_Address1 = Value
End Set
End Property
Public Property Address2() As String
Get
Return _Address2
End Get
Set(ByVal Value As String)
_Address2 = Value
End Set
End Property
Public Property City() As String
Get
Return _City
End Get
Set(ByVal Value As String)
_City = Value
End Set
End Property
Public Property State() As String
Get
Return _State
End Get
Set(ByVal Value As String)
_State = Value
End Set
End Property
Public Property Zip() As String
Get
Return _Zip
End Get
Set(ByVal Value As String)
_Zip = Value
End Set
End Property
Public Property HomePhone() As String
Get
Return _HomePhone
End Get
Set(ByVal Value As String)
_HomePhone = Value
End Set
End Property
Public Property WorkPhone() As String
Get
Return _WorkPhone
End Get
Set(ByVal Value As String)
_WorkPhone = Value
End Set
End Property
Public Property EmailAddress() As String
Get
Return _EmailAddress
End Get
Set(ByVal Value As String)
_EmailAddress = Value
End Set
End Property
Public Property LicenseNumber() As String
Get
Return _LicenseNumber
End Get
Set(ByVal Value As String)
_LicenseNumber = Value
End Set
End Property
Private Sub Parse(ByVal line As String)
RecordId = GetField(line, 1, 2, 2)
LastName = GetField(line, 3, 17, 15)
FirstName = GetField(line, 18, 42, 25)
MiddleName = GetField(line, 43, 57, 15)
Address1 = GetField(line, 58, 92, 35)
Address2 = GetField(line, 93, 127, 35)
City = GetField(line, 128, 152, 25)
State = GetField(line, 153, 177, 25)
Zip = GetField(line, 178, 187, 10)
HomePhone = GetField(line, 188, 199, 12)
WorkPhone = GetField(line, 200, 211, 12)
EmailAddress = GetField(line, 212, 261, 50)
LicenseNumber = GetField(line, 262, 276, 15)
End Sub
Public Sub New(ByVal line As String)
Parse(line)
End Sub
Public Function GetField(ByVal dataLine As String, _
ByVal stChar As Integer, ByVal endChar As Integer, _
ByVal len As Integer) As String
If dataLine.Length < 205 Then dataLine += Space(205 - dataLine.Length)
If stChar >= 1 AndAlso _
endChar <= 205 AndAlso _
endChar >= stChar AndAlso _
dataLine.Length >= endChar AndAlso _
(endChar - stChar + 1) = Len _
Then
Return dataLine.Substring(stChar - 1, endChar - stChar + 1).Trim
Else
Throw New Exception("ServiceRequests: GetField() bad input parameters")
End If
End Function
End Class
|
The class is made up of two methods, one private variable and associated Public Property for each variable in the original VB6 Type definition. The Class acts like an Object Factory when you instantiate an instance of the InputRecord Class. In other words it returns an instance of itself. Each of the Properties can then be referenced to retrieve the values of the input line of the input file. You can see that the constructor of the class parses the input line and fills the private variables. Typically, you would place the instantiation of the InputRecord objects in a For loop reading through the input file. In the case of this simple Type definition shown in Figure 1 above, the Macro has created 153 lines of bug-free code. List 1 below shows how to create an input line object and process the data.
Figure 3 - Using the New Class.
Dim inputRec As InputRecord
Using sr As New System.IO.StreamReader(fileName)
Dim line As String = sr.ReadLine()
inputRec = New InputRecord(line)
With inputRec
Dim firstName As String = inputRec.FirstName
Dim lastName As String = inputRec.LastName
'... use similiar code to retrieve the remainder of the fields
' process the input
End With
End Using
|
Figure 4 shows the code for the Macro itself. To use the Macro, put it into a module in the Macro IDE, select the field definitions inside the Type definition, shown in Figure 1, and double-click the Macro name in the Macro Explorer.
Figure 4 - Macro for Creating Input Line Objects.
Public Sub CreateInputObjectMethod()
Dim ts As TextSelection = DTE.ActiveDocument.Selection
Dim s As String = ts.Text
Dim mc As MatchCollection = Regex.Matches(s, _
"^\s*(?<field>\w+)\s+As\s+String\s+\*\s+(?<nbr>\d+)", RegexOptions.Multiline)
Dim cnt As Integer = 0
Dim stPtr As Integer = 1
Dim sb As New Text.StringBuilder(5000)
Dim sbpriv As New Text.StringBuilder(5000)
Dim sbprop As New Text.StringBuilder(5000)
Dim sbClass As New Text.StringBuilder(5000)
Const gf1 As String = _
"Public Function GetField(ByVal dataLine As String, " & _
"ByVal stChar As Integer, ByVal endChar As Integer, " & _
"ByVal len As Integer) As String"
Const gf2 As String = _
" If dataLine.Length < 205 Then dataLine += Space(205 - dataLine.Length)"
Const gf3 As String = " If stChar >= 1 AndAlso _"
Const gf4 As String = " endChar <= 205 AndAlso _"
Const gf5 As String = " endChar >= stChar AndAlso _"
Const gf6 As String = " dataLine.Length >= endChar AndAlso _"
Const gf7 As String = " (endChar - stChar + 1) = Len _"
Const gf8 As String = " Then"
Const gf9 As String = _
" Return dataLine.Substring(stChar - 1, endChar - stChar + 1).Trim"
Const gf10 As String = " Else"
Const gf11 As String = _
" Throw New Exception(""ServiceRequests: GetField() bad input parameters"")"
Const gf12 As String = "End If"
Const gf13 As String = "End Function"
Dim name As String = InputBox("Enter name for new Input Object", "Enter Object Name", "")
If name.Length > 0 Then
sbClass.Append("Public Class " & name & vbCrLf)
sb.Append(" Private Sub Parse(Byval line As String)" & vbCrLf)
For Each m As Match In mc
Dim nbr As Integer = CType(m.Groups("nbr").Value, Integer)
cnt += nbr
' create the private var for each match
sbpriv.Append(" Private _" & m.Groups("field").Value & _
" As String = String.Empty" & vbCrLf)
' create the matching property
sbprop.Append(" Public Property " & _
m.Groups("field").Value & "() As String" & vbCrLf)
sbprop.Append(" Get" & vbCrLf)
sbprop.Append(" Return _" & m.Groups("field").Value & vbCrLf)
sbprop.Append(" End Get" & vbCrLf)
sbprop.Append(" Set(ByVal Value As String)" & vbCrLf)
sbprop.Append(" _" & m.Groups("field").Value & " = Value" & vbCrLf)
sbprop.Append(" End Set" & vbCrLf)
sbprop.Append(" End Property" & vbCrLf)
sb.Append(m.Groups("field").Value & " = GetField(line, " & _
stPtr.ToString & ", " & cnt & ", " & _
m.Groups("nbr").Value & ")" & vbCrLf)
stPtr += nbr
Next
sb.Append(" End Sub" & vbCrLf)
sb.Append(" Public Sub New(Byval line As String)" & vbCrLf)
sb.Append(" Parse(line)" & vbCrLf)
sb.Append(" End Sub" & vbCrLf)
sb.Append(gf1 & vbCrLf)
sb.Append(gf2 & vbCrLf)
sb.Append(gf3 & vbCrLf)
sb.Append(gf4 & vbCrLf)
sb.Append(gf5 & vbCrLf)
sb.Append(gf6 & vbCrLf)
sb.Append(gf7 & vbCrLf)
sb.Append(gf8 & vbCrLf)
sb.Append(gf9 & vbCrLf)
sb.Append(gf10 & vbCrLf)
sb.Append(gf11 & vbCrLf)
sb.Append(gf12 & vbCrLf)
sb.Append(gf13 & vbCrLf)
Debug.WriteLine(sb.ToString)
DTE.ItemOperations.NewFile("General\Text File")
sbClass.Append(sbpriv.ToString() & vbCrLf)
sbClass.Append(sbprop.ToString() & vbCrLf)
sbClass.Append(sb.ToString() & vbCrLf)
sbClass.Append("End Class" & vbCrLf)
DTE.ActiveDocument.Object("TextDocument"). _
Selection.Insert(sbClass.ToString)
End If
End Sub
|
The Macro is not really complex. You can see that I use a RegularExpression to parse the fields of the Type definition. I hope this will ignite a new interest in Macros and Extensibility in general. When I originally wrote this Macro, I was converting a VB6 application with 12 Type Definitions. As soon as I completed the Macro, which took less than an hour, I used it to generate 12 Classes with over 1500 lines of bug-free code that would never have to be debugged at all.
| Ask a Question, or give your feedback on my articles or products by going to the KnowDotNet Forum or by clicking on My Blog. |  |
|
|