This topic has not yet been rated - Rate this topic
Advanced Basics
Building an Attribute Documenter and Viewer
Ken Spencer
Code download available at:
AdvancedBasics0210.exe
(141 KB)
Q I'm trying to decide which Microsoft® .NET-compliant language to use to build my applications. I know one of the advantages of C# is the ability to comment code and turn those comments into XML docs. If I use Visual Basic® .NET, can I still do this?
Q I'm trying to decide which Microsoft® .NET-compliant language to use to build my applications. I know one of the advantages of C# is the ability to comment code and turn those comments into XML docs. If I use Visual Basic® .NET, can I still do this?
A It's been obvious to me that with a little work, a cool documentation facility could be added to Visual Basic .NET. Recently, I found the time to dig into it. As luck would have it, I just picked up a copy of Matt Tagliaferri's book, Visual Basic Codemaster's Library (Sybex, 2002), for another project and the idea gelled as a result.
A It's been obvious to me that with a little work, a cool documentation facility could be added to Visual Basic .NET. Recently, I found the time to dig into it. As luck would have it, I just picked up a copy of Matt Tagliaferri's book, Visual Basic Codemaster's Library (Sybex, 2002), for another project and the idea gelled as a result.
On page 88 of his book, Matt shows a little application that allows you to document your code using custom attributes. I thought about his example a bit and then began to extend it, trying to see how it could be used as a general-purpose tool.
My testing has culminated into two projects. One implements the attribute Matt provided plus a couple of other attributes, and the second provides a UI for viewing the attributes. The result is a nice little viewer built using Windows® Forms that you can use against any assembly to find its classes and display their custom attributes if the classes provide them. Let's take a look.
The Attribute Documenter
The first project is implemented as a DLL named CodeDocumentorAttribute.dll and provides several documentation attributes and a set of routines for manipulating them.
The simplest attribute (GeneralDocumentation) is shown in Figure 1. The attribute is simply implemented as a class that has properties you want the attribute to implement. In this case, there is only one property: Description. To use the attribute, you must do two things. First, in the project in which you use the attribute you must place a reference to the assembly that contains the attribute. Second, you must attach the attribute to an item in your code and enter its properties. For instance, the following attribute can be used like this:
This attaches an instance of the attribute to the DoSomething procedure and adds a comment describing the procedure. This particular attribute is defined so it can be used with all attribute targets (such as class, methods, properties, and so on). The usage is determined by applying one of the AttributeTargets.All enumerations to the definition, as shown in the first line of Figure 1. You cannot just drop an attribute into your code. Instead, it must be attached to a member of an assembly, as shown here. Supported members are listed in Figure 2. As you can see from this figure, you can apply attributes to many different member types.
<GeneralDocumentation("This function does something")> _ Sub DoSomething() End Sub
| Member Name | Description |
|---|---|
| All | Attribute can be applied to any application member such as a class, method, or property. |
| Assembly | Attribute can be applied to an assembly. |
| Class | Attribute can be applied to a class definition. |
| Constructor | Attribute can be applied to a constructor of classes definition. |
| Delegate | Attribute can be applied to a delegate. |
| Enum | Attribute can be applied to an enumeration. |
| Event | Attribute can be applied to an event. |
| Field | Attribute can be applied to a field. |
| Interface | Attribute can be applied to an interface. |
| Method | Attribute can be applied to a method. |
| Module | Attribute can be applied to a module. Note that a module refers to a portable executable file (.dll or .exe) and not to a Visual Basic standard module. |
| Parameter | Attribute can be applied to a parameter. |
| Property | Attribute can be applied to a property. |
| ReturnValue | Attribute can be applied to a return value. |
| Struct | Attribute can be applied to a structure—that is, a value type. |
<AttributeUsage(AttributeTargets.All)> _ Public Class GeneralDocumentation Inherits System.Attribute Private localDescription As String Sub New(ByVal Description As String) MyBase.New() localDescription = Description End Sub Property Description() As String Get Return localDescription End Get Set(ByVal Value As String) localDescription = Value End Set End Property End Class
The other attributes in the CodeDocumentorAttribute assembly are implemented in the same way, just using different properties and targets.
This assembly also contains a class named AttributeRoutines. This class contains several methods and functions that I pulled from Matt's code and modified, plus a bit of additional code that I added. The public methods of this class are listed in Figure 3.
| Method | Description |
|---|---|
| FindPropertiesMissingMe | Returns all methods and properties that are not documented using the CodeDescriptor attribute |
| RetrieveAllCustomAttributes | Returns all custom attributes |
| RetrieveDocAttributes | Returns all CodeDescriptor attributes |
| RetrieveExtraDocAttribute | Returns the document name for classes that have an extra documentation file |
Let's take a look at the simplest method: RetrieveExtraDocAttribute. The method takes one parameter, which is the class it will pull the attributes from:
The next two lines define variables for the routine. The first variable is of type Attribute and will be used to access the attributes:
Next comes a For Each loop that walks through all custom attributes in the class; when it finds the DocFileAttribute, it returns its value. This works because there can only be one of these attributes in a class:
Public Shared Function RetrieveExtraDocAttribute(ByVal t As Type) _ As String
Dim oAT As Attribute Dim oDocFileAttribute As DocFileAttribute
For Each oAT In t.GetCustomAttributes(False) If TypeOf oAT Is DocFileAttribute Then oDocFileAttribute = CType(oAT, DocFileAttribute) Return oDocFileAttribute.ResourceName Else Return "" End If Next
If the DocFileAttribute is found, then oDocFileAttribute is set to reference it and the name of the resource is returned by a call to ResourceName.
That's all the code you need to walk through the attributes in a class. There are other methods that Reflection provides, such as GetConstructors and GetMethods, which can be used to extract attributes for those types of items.
The Attribute Viewer
Now, let's see how to put the viewer together. Figure 4 shows the main interface for this app. To run it, click the File Open button on the toolbar (the folder button) and select an assembly to pull comments from. Following that, click the second button. This will cause the documentation attributes to be pulled out and the values to be displayed, as you can see in Figure 4.
The top box shows the documentation, while the bottom box shows any methods or properties that do not have documentation accompanying them. That allows you to actually see what is documented and what is not. This was one of the really good ideas that Matt provided.
Figure 5 shows the code that generates this display. This is where my code takes a departure from Matt's. Instead of placing the attributes class in the assembly being documented, the attributes are in a separate assembly so they can be used from other applications without the need to add the attribute's source to each project. To accomplish this, the LoadFrom method of the Assembly class is called to load the assembly:
Then the GetExportedTypes method is called to extract an array of all the types (classes) in the assembly:
Then the For Each loop processes each assembly and documents it using the functions in the AttributeRoutines class. This loads the two textboxes with comments and missing comments information from all classes in the assembly. At this point, you have a very generic application that you can use to document your apps.
oAssembly = [Assembly].LoadFrom(sFileName)
arrayOfTypes = oAssembly.GetExportedTypes()
Sub GetAttributes() Dim t As Type Dim oDesc As CodeDocumentorAttribute.AttributeRoutines Dim othis As Object Dim oAssembly As [Assembly] Dim arrayOfTypes() As Type If sFileName = "" Then Exit Sub End If oAssembly = [Assembly].LoadFrom(sFileName) arrayOfTypes = oAssembly.GetExportedTypes() txtOutput.Text = "" For Each t In arrayOfTypes If t Is Nothing Then MsgBox("Type was not found — exiting") Exit Sub End If txtOutput.Text &= oDesc.RetrieveDocAttributes(t) txtOutput.Text &= vbCrLf & vbCrLf txtMissing.Text &= oDesc.FindPropertiesMissingMe(t) Next oAssembly = Nothing End Sub
I thought this was pretty cool and considered stopping here, but I realized that some of the C# folks would still talk about how great their commenting features were. To address this, I thought I could extend this project by allowing a developer to link a custom text file to a class. Then the developer could comment the class more fully and not be limited by any particular attributes.
Along with the ability to use attributes in your code, another really cool feature is the ability to add files as resources to a project. For instance, you can add a .txt file such as doc.txt to a project, then make it an embedded resource. You do this by selecting Embedded Resource as the value of the Build Action property in the project. Let's apply this to the application.
I added a doc.txt file to the test assembly's project. Then I changed the header for the test class by adding a DocFileAttribute attribute like this:
It's important to note that resources are case sensitive, so make sure that you name the file with the same name as the attribute.
<CodeDescriptor("kls", "6/30/2002", "Book Detail Data"), _ DocFileAttribute("doc.txt")> _ Public Class BookClassCompiled
Now that the doc file is in the project and the attribute points to it, how do you use it? The code in Figure 6 is used by the main form to load a combobox with all the classes it finds in the target assembly that contain a DocFileAttribute. This allows the user to obtain a list of all classes that are more fully documented.
Sub GetExtraDocuments() Dim t As Type Dim oDesc As CodeDocumentorAttribute.AttributeRoutines Dim othis As Object Dim oAssembly As [Assembly] Dim arrayOfTypes() As Type Dim sDocName As String Dim dt As New DataTable() Dim dr As DataRow If sFileName = "" Then Exit Sub End If oAssembly = [Assembly].LoadFrom(sFileName) arrayOfTypes = oAssembly.GetExportedTypes() txtOutput.Text = "" bLoading = True dt.Columns.Add("ClassName") dt.Columns.Add("DocName") For Each t In arrayOfTypes If t Is Nothing Then MsgBox("Type was not found — exiting") Exit Sub End If sDocName = oDesc.RetrieveExtraDocAttribute(t) If sDocName <> "" Then dr = dt.NewRow() dr(0) = t.Name dr(1) = sDocName dt.Rows.Add(dr) End If Next cboDocList.BeginUpdate() cboDocList.DataSource = dt cboDocList.DisplayMember = "ClassName" cboDocList.ValueMember = "DocName" cboDocList.EndUpdate() bLoading = False End Sub
The code in Figure 7 finishes the process by retrieving the doc file from the assembly and displaying it in another form which is shown as a dialog. The code uses LoadFrom to load the assembly, then the name of the assembly is combined with the doc file for the combobox to build the full path to the documentation file:
Next, the resource is extracted into a stream using the GetManifestResourceStream method:
Then the results of the text file are shown in frmDocIt. As you can see, it's pretty simple to extract a resource file and use or manipulate its values.
oAssembly = [Assembly].LoadFrom(sFileName) sResourceName = oAssembly.GetName().Name _ & "." & cboDocList.SelectedValue().ToString
Dim strm As Stream = _ oAssembly.GetManifestResourceStream(sResourceName)
If Not bLoading Then Dim frm As New frmDocIt() Dim oAssembly As [Assembly] Dim sResourceName As String oAssembly = [Assembly].LoadFrom(sFileName) sResourceName = oAssembly.GetName().Name _ & "." & cboDocList.SelectedValue().ToString Dim strm As Stream = _ oAssembly.GetManifestResourceStream(sResourceName) If Not strm Is Nothing Then 'Read the contents of the embedded file Dim reader As StreamReader = New StreamReader(strm) frm.txtOutput.Text = reader.ReadToEnd() frm.ShowDialog() End If oAssembly = Nothing End If
Conclusion
The features created in this project represent only the tip of the iceberg; you can extend these attributes and the application in many different ways. These attributes, and the application that is used to read them, can also be used with C# assemblies.
In addition, because the attributes and resources are stored in the assembly, the assemblies are also self-documenting at design time and after they're deployed. You don't need a compiler, and you don't need Visual Studio® .NET to pull out the attributes.
In an upcoming column I will show you how to add more features to this code that make it even more useful.
Send questions and comments for Ken to Basics@microsoft.com.
Ken Spencerworks for 32X Tech (http://www.32X.com). 32X provides training, software development, and consulting services on Microsoft technologies.