Outputting Code - Creating a Class
(Page 11 of 19 )
Creating a class using brute-force code generation has similarities with the XSLT approach to code generation. In both cases, you can use regions to organize both your output file and your template, and you can supply a different method to output each region. You start with an entry-level method that’s the rough equivalent to the XSLT entry-level template.
Creating the Entry-Level Method The entry-level method is specified in the Process attribute of the directive in the harness script file. The harness calls this method and expects it to generate a stream containing the output code. The entry-level template has to be a shared (C# static) method. In this case, the entry-level method is GetStream.
The GetStream method first creates a MemoryStream for output, then an IndentedTextWriter, and some other variables:
Public Shared Function GetStream( _
ByVal Name As String, _
ByVal fileName As String, _
ByVal genDateTime As String, _
ByVal nodeSelect As Xml.XmlNode) _
As IO.Stream
Dim stream As New IO.MemoryStream
Dim inwriter As New CodeDom.Compiler.IndentedTextWriter( _
New IO.StreamWriter(stream))
Dim nodeList As Xml.XmlNodeList
Dim nodeColumn As Xml.XmlNode
GetAttributeOrEmpty is a generic utility method that returns the attribute specified or an empty string if the attribute isn’t present in the XML DOM element. You’ll need a namespace manager when you reference a namespace (via a prefix) in the input XML. The namespace would generally be referenced with a prefix in arguments passed to the SelectSingleNode or SelectNode methods of the XML DOM. A utility method in the Tools class creates the namespace manager:
Dim singularName As String = Utility.Tools.GetAttributeOrEmpty(nodeSelect, _ "SingularName")
Dim nsmgr As Xml.XmlNamespaceManager = _ Utility.Tools.BuildNameSpaceManager( _ nodeSelect.OwnerDocument, "dbs", False)
CAUTION: Namespaces can be hard to handle, especially in .NET, which is unfriendly to the default namespace (as discussed in Appendix A).
The GetStream method then calls the FileOpen method contained in a separate support file. Placing this method in a central location allows reuse by other brute-force templates. Outputting an empty string using the WriteLine method produces a blank line:
Support.FileOpen(inwriter, "KADGen,System", fileName, genDateTime)
inwriter.WriteLine("")
Another supporting method, WriteLineAndIndent, outputs a line of text and then indents the writer. Changeable information is inserted either as a variable or as a direct lookup of information in the metadata XML. The following outputs the class declaration using a variable (singularName is declared previously):
Support.WriteLineAndIndent(inwriter, "Public Class " & singularName & _ "Collection")
inwriter.WriteLine("Inherits CollectionBase")
Similar to the XSLT code generation, it’s a lot easier to debug if you call a method for each region. The collection class contains two regions so the high-level method calls two other methods to output the core of the class:
CollectionConstructors(inwriter, nsmgr, nodeSelect)
PublicAndFriend(inwriter, nsmgr, nodeSelect) Support.WriteLineAndOutdent(inwriter, "End Class")
The WriteLineAndOutdent method is similar to the WriteLineAndIndent method, and it decreases the indent by one and then outputs the specified text.
Outputting the row class is much like outputting the collection class. The next code uses a For Each construct to loop through the columns and calls a method for each column:
inwriter.WriteLine("")
inwriter.WriteLine("")
Support.WriteLineAndIndent(inwriter, "Public Class " & singularName) inwriter.WriteLine("Inherits RowBase")
ClassLevelDeclarations(inwriter, nsmgr, nodeSelect)
Constructors(inwriter, nsmgr, nodeSelect)
BaseClassImplementation(inwriter, nsmgr, nodeSelect) Support.MakeRegion(inwriter, "Field access properties")
nodeList = nodeSelect.SelectNodes("dbs:TableColumns/*", nsmgr)
For Each nodeColumn In nodeList
ColumnMethods(inwriter, nsmgr, nodeColumn)
Next
Support.EndRegion(inwriter)
Support.WriteLineAndIndent(inwriter, "End Class")
Flushing the stream after you’ve finished output ensures that the stream has emptied any buffers and all information is safely in the stream before the writer goes out of scope. Not all writers need to be flushed, but it’s a great habit to be in because it’s essential with many of them:
inwriter.Flush()
Return stream
End Function
Looking at a Sample Region I’ll skip over the Constructors region method because it doesn’t present anything new and instead will walk through one of the more complex region methods. Just like the XSLT templates, the PublicAndFriend region outputs the primary key only if it exists. Passing the namespace manager parameter means you don’t have to re-create it if methods need it for any XML processing:
Private Shared Sub PublicAndFriend( _
ByVal inWriter As CodeDom.Compiler.IndentedTextWriter, _
ByVal nsmgr As Xml.XmlNamespaceManager, _
ByVal node As Xml.XmlNode)
Support.MakeRegion(inWriter, _
"Public and Friend Properties, Methods and Events")
The SelectSingleNode statement retrieves the primary key element of the TableConstraints element that’s stored in the nodeTemp variable. If this node is found, the value of its Name attribute is assigned to the primaryKeyName variable:
Dim nodeTemp As Xml.XmlNode = node.SelectSingleNode( _
"dbs:TableConstraints/dbs:PrimaryKey/dbs:PKField", nsmgr)
Dim primaryKeyName As String = ""
Dim primaryKey As Xml.XmlNode
If Not nodeTemp Is Nothing Then
primaryKeyName = Utility.Tools.GetAttributeOrEmpty(nodeTemp, "Name")
End If
This XPath expression in the next SelectSingleNode uses the primaryKeyName to retrieve the TableColumn node with a Name attribute matching the primaryKeyName:
primaryKey = node.SelectSingleNode( _
"dbs:TableColumns/dbs:TableColumn[@Name='" & primaryKeyName & _
"']", nsmgr)
Support.WriteLineAndIndent(inWriter, "Public Overloads Sub Fill( _")
inWriter.Indent += 4
Once you’ve got the primaryKey node, you can use it in a simple conditional to output a parameter for the primary key value only if there’s a primary key. You can use the Write method to output a partial line without a CRLF:
If Not primaryKey Is Nothing Then
inWriter.Write("ByVal " & primaryKeyName & " As ")
inWriter.WriteLine(Utility.Tools.GetAttributeOrEmpty(primaryKey, _
"NETType") & ", _")
End If
The remainder of the function reuses the same techniques you’ve already seen:
inWriter.WriteLine("ByVal UserID As Int32)")
inWriter.Indent -= 4
inWriter.WriteLine("ByVal UserID As Int32)")
inWriter.Write(Utility.Tools.GetAttributeOrEmpty(node, "SingularName") & _
"DataAccessor.Fill(Me")
If Not primaryKey Is Nothing Then
inWriter.Write(", " & primaryKeyName)
End If
inWriter.WriteLine(", UserID)")
Support.WriteLineAndOutdent(inWriter, "End Sub")
Support.EndRegion(inWriter)
End Sub
This chapter is from Code Generation in Microsoft .NET by Kathleen Dollard (Apress, 2004, ISBN: 1590591372). Check it out at your favorite bookstore today.
Buy this book now. |
Next: Creating the Support Template >>
More .NET Articles
More By Apress Publishing