CommenTater: The Cure for the Common Potato?


One absolutely cool part of C# is the XML documentation comments. They are XML tags that appear in comments describing properties or methods in a file. In fact, the IDE helps you out by automatically inserting the appropriate documentation comments for a program construct when you type in ///above that construct. There are three very important reasons you should always fill out C# documentation comments. The first is that doing so enforces a consistent commenting standard across teams and, well, the C# programming universe. The second is that the IDE IntelliSense automatically picks up any specified <summary> and <param> tags, which is a great help to others using your code because it gives them much more valuable information about the item. If the code is in the project, you don't have to do anything to get the benefits of using documentation comments. If you're providing a binary-only solution, the documentation comments can be gathered into an XML file at compile time, so you can still provide the cool summary tips to users. All you need to do is have the resultant XML file in the same directory as the binary, and Visual Studio .NET automatically picks up the comments to show in the IntelliSense tips.

The last reason you should fill out the documentation comments is that the resulting XML file can simply have a good dose of XSLT applied to it to build a complete documentation system for your product. Please note that I'm not talking about the Build Comment Web Pages option on the Tools menu. There is plenty of important information not picked up by that command, such as <exceptions>, so it's not that useful. Far better tools can generate the documentation, as I'll discuss in a moment.

I encourage you to read about all the XML documentation comment tags in the Visual Studio .NET documentation so that you can do the best job possible documenting your code. To create the XML document file, fill out the XML Documentation File field on the project Property Pages dialog box, Configuration Properties folder, Build page, as shown in Figure 9-3. Make sure you fill out the field individually for all configurations so that the document file is built every time.

click to expand
Figure 9-3: Setting the /doc command.line option to produce an XML documentation comment file

To produce full output from the XML documentation comment files, I included an XSL transform file and cascading style sheet in the DocCommentsXSL directory available with this book's sample files. However, a far better tool to use is the NDoc tool available from http://ndoc.sourceforge.net. NDoc sucks up the XML documentation comments and creates an HTML Help file that looks just like the MSDN .NET Framework class library documentation. NDoc even provides links to general methods such as GetHashCode, so you can jump right into the MSDN documentation! NDoc is an excellent way to document your team's code, and I highly recommend its use. In fact, with Visual Studio .NET 2003's new ability to do post build processing, you can easily make NDoc part of your build.

Since documentation comments are so important, I wanted some way of automatically adding them to my C# code. About the same time I was thinking about this, I found that the Task List window automatically shows any comments in your code that start with key phrases such as "TODO" when you right-click in the Task List window and select either All or Comment from the Show Tasks option on the shortcut menu. My idea was to have either a macro or an add-in that would add any missing documentation comments and use "TODO" in the comments so that I could easily go through and ensure all the documentation comments were properly filled out. The result was CommenTater. The following shows a method that's been processed by CommenTater.

/// <summary> /// TODO - Add Test function summary comment /// </summary> /// <remarks> /// TODO - Add Test function remarks comment /// </remarks> /// <param name="x"> /// TODO - Add x parameter comment /// </param> /// <param name="y"> /// TODO - Add y parameter comment /// </param> /// <returns> /// TODO - Add return comment /// </returns> public static int Test ( Int32 x , Int32 y ) { return ( x ) ; }

Visual Studio .NET makes iterating the code elements in a source file trivial, so I was pretty excited because I thought that all I'd have to do was wind through the code elements, grab any lines above the method or property, and if the documentation comment wasn't present, poke in the documentation comment for that method or property. That's when I discovered that the code elements all come with a property named DocComment that returns the actual documentation comment for the item. I immediately tipped my hat to the developers for thinking ahead and really making the code elements useful. Now all I needed to do was set the DocComment property to the value I wanted, and life was good.

At this point, you might want to open up the CommenTater.VB file in the CommenTater directory. The source code for the macro is too large to include in the book pages, but consider looking at the file as I discuss the ideas behind its implementation. My main idea was to create two functions, AddNoCommentTasksForSolution and CurrentSourceFileAddNoCommentTasks. You can tell from the names what level the functions were to work on. For the most part, the basic algorithm looks relatively similar to the examples in Listing 9-1 in that I just recurse through all the code elements and manipulate the DocComment property for each code element type.

The first issue I ran into was what I considered a small weakness in the code element object model. The DocComment property is not a common property on the CodeElement class, which can be substituted as a base for any general code element. So I had to jump through the hoops necessary to convert the generic CodeElement object into the actual type based on the Kind property. That's why the RecurseCodeElements function has the big Select…Case statement in it.

The second problem I ran into was completely self-inflicted. For some reason, it never dawned on me that the DocComment property for a code construct needed to be treated as a fully formed XML fragment. I'd build up the documentation comment string I wanted, and when I'd try to assign it to the DocComment property, an ArgumentException would be thrown. I was quite confused because it looked like the DocComment property was both readable and writable, but it was acting like it was only readable. For some bizarre reason, I had a brain cramp and didn't realize that the reason for the exception was that I wasn't properly bracketing my documentation comment XML with the necessary <doc></doc> element. Instead I figured I was running into a bug and therefore looked for alternative means for getting the documentation comment text in.

Since the individual code elements included a StartPoint property, I just needed to create the appropriate EditPoint and start jamming in the text. A little experimentation quickly proved that worked just fine, so I implemented a set of routines to squirt in the necessary text. Since there are plenty of times when manually inserting the text is required, I left the routines in the bottom of the CommenTater.VB file so that you can see the work necessary to add text and keep it aligned, but I commented them out.

For the first version of the macro, I was pretty happy with the way everything worked and used CommenTater quite a bit in my own development. I thought I might have to port CommenTater over to a full add-in because a macro might be too slow, but the macro was always plenty fast for me. The first version of CommenTater added only missing documentation comments. That was fine, but I quickly realized that I really wanted CommenTater to get some smarts and compare existing function comments with what was currently in the code. Many times I had changed a function prototype by adding or removing parameters but didn't remember to update the documentation comment to reflect the change. By adding this comparison functionality, CommenTater would be even more useful.

When I first started looking at what it was going to take to update existing comments, I was a little disheartened. Since at this point I thought the DocComment property was read-only, I realized I was going to have to do quite a bit of text manipulation to properly update comments—and that wasn't looking appealing. However, as I looked at CommenTater in the macro debugger, the big, fat, juicy clue slapped me hard in the forehead, and it dawned on me that I simply needed to put in the <doc></doc> elements around any documentation comments to write to the DocComment property. Once I got over my own stupidity, writing the ProcessFunctionComment function was much easier. It's shown in Listing 9-2.

Listing 9-2: ProcessFunctionComment from CommenTater.VB

start example
 ' Does all the work to take an existing function comment and ensure ' that everything in it is kosher. This might reorder your  ' comments, so you might want to change it. Private Sub ProcessFunctionComment(ByVal Func As CodeFunction)         Debug.Assert("" <> Func.DocComment, """"" <> Func.DocComment")         ' Holds the original doc comment.     Dim XmlDocOrig As New XmlDocument()     ' I LOVE THIS!  By setting PreserveWhitespace to true, the      ' XmlDocument class will keep most of the formatting...     XmlDocOrig.PreserveWhitespace = True     XmlDocOrig.LoadXml(Func.DocComment)         Dim RawXML As New StringBuilder()         ' Get the summary node.     Dim Node As XmlNode     Dim Nodes As XmlNodeList = XmlDocOrig.GetElementsByTagName("summary")     If (0 = Nodes.Count) Then         RawXML.Append(SimpleSummaryComment(Func.Name, "function"))     Else         RawXML.AppendFormat("<summary>{0}", vbCrLf)         For Each Node In Nodes             RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)         Next         RawXML.AppendFormat("</summary>{0}", vbCrLf)     End If         ' Get the remarks node.     Nodes = XmlDocOrig.GetElementsByTagName("remarks")     If (Nodes.Count > 0) Then         RawXML.AppendFormat("<remarks>{0}", vbCrLf)         For Each Node In Nodes             RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)         Next         RawXML.AppendFormat("</remarks>{0}", vbCrLf)     ElseIf (True = m_FuncShowsRemarks) Then         RawXML.AppendFormat("<remarks>{0}TODO - Add {1} function " + _                                 "remarks comment{0}</remarks>", _                             vbCrLf, Func.Name)     End If         ' Get any parameters described in the doc comments.     Nodes = XmlDocOrig.GetElementsByTagName("param")         ' Does the function have parameters?     If (0 <> Func.Parameters.Count) Then             ' Slap any existing doc comment params into a hash table with         ' the parameter name as the key.         Dim ExistHash As New Hashtable()         For Each Node In Nodes             Dim ParamName As String             Dim ParamText As String             ParamName = Node.Attributes("name").InnerXml             ParamText = Node.InnerText             ExistHash.Add(ParamName, ParamText)         Next             ' Loop through the parameters.         Dim Elem As CodeElement         For Each Elem In Func.Parameters             ' Is this one in the hash of previous filled in params?             If (True = ExistHash.ContainsKey(Elem.Name)) Then                 RawXML.AppendFormat("<param name=""{0}"">{1}{2}{1}" + _                                                  "</param>{1}", _                                       Elem.Name, _                                       vbCrLf, _                                       ExistHash(Elem.Name))                 ' Get rid of this key.                 ExistHash.Remove(Elem.Name)             Else                 ' A new parameter was added.                 RawXML.AppendFormat("<param name=""{0}"">{1}TODO - Add " + _                                     "{0} parameter comment{1}</param>{1}", _                                      Elem.Name, vbCrLf)             End If         Next             ' If there is anything left in the hash table, a param          ' was either removed or renamed. I'll add the remaining          ' with TODOs so the user can do the manual deletion.         If (ExistHash.Count > 0) Then             Dim KeyStr As String             For Each KeyStr In ExistHash.Keys                 Dim Desc = ExistHash(KeyStr)                 RawXML.AppendFormat("<param name=""{0}"">{1}{2}{1}{3}" + _                                                         "{1}</param>{1}", _                                       KeyStr, _                                       vbCrLf, _                                       Desc, _                                       "TODO - Remove param tag")             Next         End If     End If         ' Take care of returns if necessary.     If ("" <> Func.Type.AsFullName) Then         Nodes = XmlDocOrig.GetElementsByTagName("returns")         ' Do any returns nodes.         If (0 = Nodes.Count) Then             RawXML.AppendFormat("<returns>{0}TODO - Add return comment" + _                                                       "{0}</returns>{0}", _                                 vbCrLf)         Else             RawXML.AppendFormat("<returns>{0}", vbCrLf)                 For Each Node In Nodes                 RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)             Next                 RawXML.AppendFormat("</returns>{0}", vbCrLf)         End If     End If         ' Do any example nodes.     Nodes = XmlDocOrig.GetElementsByTagName("example")     If (Nodes.Count > 0) Then         RawXML.AppendFormat("<example>{0}", vbCrLf)         For Each Node In Nodes             RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)         Next         RawXML.AppendFormat("</example>{0}", vbCrLf)     End If         ' Do any permission nodes.     Nodes = XmlDocOrig.GetElementsByTagName("permission")     If (Nodes.Count > 0) Then         For Each Node In Nodes             RawXML.AppendFormat("<permission cref=""{0}"">{1}", _                                 Node.Attributes("cref").InnerText, _                                 vbCrLf)             RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)             RawXML.AppendFormat("</permission>{0}", vbCrLf)         Next     End If         ' Finally exceptions.     Nodes = XmlDocOrig.GetElementsByTagName("exception")         If (Nodes.Count > 0) Then         For Each Node In Nodes             RawXML.AppendFormat("<exception cref=""{0}"">{1}", _                                 Node.Attributes("cref").InnerText, _                                 vbCrLf)             RawXML.AppendFormat("{0}{1}", Node.InnerXml, vbCrLf)             RawXML.AppendFormat("</exception>{0}", vbCrLf)         Next     End If     Func.DocComment = FinishOffDocComment(RawXML.ToString()) End Sub
end example

The beauty of the Microsoft .NET Framework class library certainly came into play. I could use the excellent XmlDocument class to do all the heavy lifting necessary to get the information out of the existing documentation comment string, allowing me to build up a new version. The ProcessFunctionComment function might reorder your documentation comments; I had to pick an order for putting the individual nodes in the file. Additionally, I format the comments as I like to see them, so CommenTater might change your careful formatting of your documentation comments, but it won't lose any information.

Once I had everything working with documentation comment updating, I thought it'd be a good idea to go ahead and implement the code necessary to handle an undo context. That way you could do a single Ctrl+Z and restore all changes to CommenTater in case of a bug. Unfortunately, the undo context causes a real problem. When I don't have an undo context open, all the changes I make to documentation comments works just fine. However, when an undo context is open before doing all changes, everything is completely messed up—it looks like the undo context interferes with the code elements. When CommenTater writes to a DocComment property, the code element start points are no longer updated, so the updates occur in the old positions, not the updated positions, thus corrupting the file. I found that if, instead of using the undo context to globally account for all changes, I used it each time I updated a method or property's documentation comment, it worked. Although not as good as a global undo of all changes, at least it's some form of undo. I hope Microsoft fixes the undo context problem so that you can use the undo context globally for large-scale changes.

One interesting problem I ran into when developing CommenTater was related to reserved characters in XML that were perfectly fine as function names. Although you can have an operator & function, the second you attempt to use & in an XML documentation comment, you get an exception indicating an invalid character. Of course, the same functionality applies to > and <, so all the following operators will cause problems: operator <, operator >, operator <<, and operator >>. To keep the XML parser happy, the BuildFunctionComment function in CommenTater ensures the appropriate symbols are substituted (such as &amp; for &).

CommenTater is a very useful macro, but one big enhancement would be an excellent addition and teach you a tremendous amount about the IDE object model. The <exception> documentation comment tag is there so that you can document which exceptions a function throws. What I'd like to see is an enhancement that searches the function for any throw statements and automatically adds a new entry for that particular exception type. Of course, you should also do the proper thing and mark any existing <exception> documentation comment tags as needing to be removed when the method no longer throws the exception.

start sidebar
Common Debugging Question: Are there any tricks to debugging macros and add-ins that are written in managed code?

One of the first things you'll notice about both macro and add-in development is that the Visual Studio .NET IDE eats exceptions like crazy. Although it makes sense for the IDE not to allow any exceptions to percolate up and crash the IDE, the IDE chomps so hard on any unhandled exceptions, you might not even realize you're causing exceptions. When I developed my first macros, I sat for 20 minutes wondering why I could never seem to hit a breakpoint I set.

What I end up doing to ensure there are no surprises is to open the Exceptions dialog box, select the Common Language Runtime Exceptions node in the Exceptions tree control, and in the When The Exception Is Thrown group box, ensure Break Into The Debugger is selected. Figure 9-4 shows the proper settings. Probably this will cause you to stop in the debugger a lot more, but at least you won't have any surprises when it comes to your code.

click to expand
Figure 9-4: Setting the Exceptions dialog box to stop on all exceptions

In Figure 9-4, I didn't set JScript Exception, which is generated by JScript .NET, to stop in the debugger because I don't use it for my add-in development. If you're brave enough to whip up an add-in in JScript .NET, make sure to set the JScript Execution node to stop on all exceptions.

One thing I've also noticed about debugging macros is that even when you set the Exceptions dialog box to stop on all exceptions, it might not. The trick to getting the macro debugger to start working correctly is to set a breakpoint somewhere in your code after you've set the Exceptions dialog box settings.

end sidebar




Debugging Applications for Microsoft. NET and Microsoft Windows
Debugging Applications for MicrosoftВ® .NET and Microsoft WindowsВ® (Pro-Developer)
ISBN: 0735615365
EAN: 2147483647
Year: 2003
Pages: 177
Authors: John Robbins

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net