How to Generate Large Blocks of Code With Macros in .Net
utopian-io·@evariste·
0.000 HBDHow to Generate Large Blocks of Code With Macros in .Net
#### What Will I Learn? 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 tutorial will show you how to do it with a Macro. #### Requirements .Net Framework #### Difficulty Intermediate ## Generating 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 tutorial 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 VB? It has now been replaced by the Structure statement in .NET. But in VB, 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. #### 1 - VB 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 VB, and it contains Type structures as shown above, you will probably not get anything nearly as useful as the Type shown above was in VB. I am going to use a macro to convert the old VB Type definition into something very useful in .NET. This will work in .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. #### 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 VB 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. #### 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. #### 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 VB 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. <br /><hr/><em>Posted on <a href="https://utopian.io/utopian-io/@evariste/how-to-generate-large-blocks-of-code-with-macros-in-net">Utopian.io - Rewarding Open Source Contributors</a></em><hr/>