Contents
x.doc is short for "External Documentation." It is a Visual Studio (VS) 2005 add-in that provides a means to manage and visualize source-code comment documentation interactively in the IDE. Its main feature is that it provides an easy way to 'externalize' the documentation comments using the documentation XML <include>
tag. It also has many other features which are documented in the Features section below. This type of add-in is not new and has been developed by others - see the Resources section below. x.doc, however, is completely developed in C#, uses no third-party libraries and is built specifically for VS2005/.NET 2.0. Currently x.doc only supports documentation management for C# code files, but sufficient place-holders have been coded to make this add-in work for the other languages. Although challenging, it should be a feasible exercise.
Good source-code documentation can be very useful and is considered imperative in many commercial development environments. VS provides a very efficient method for placing documentation directly in the code using special comment delimiter tags and embedding the text in special XML tags. An overview of how this works can be found at [1] (listed in the Resources section below). VS2005 now provides even smarter code-completion for the inputting of documentation, as well as Intellisense prompting of XML tag keywords. This makes it very easy for documentation to be directly 'coded' into the source-code files using XML. However, large blocks of documentation delimited with XML tags are cumbersome and difficult to read directly in the code, and can even make the readability of the code itself difficult. The answer is to replace the entire block of documentation with a single <include>
tag that contains a reference to the block of documentation saved in an external file. Of course, once the documentation has been located in a separate documentation file, it offers the opportunity to further manage and utilize the documentation and the source-code as separate entities.
If using <include>
tags is not to your taste, the add-in also provides navigation and outlining functionality specifically for traversing blocks of documentation.
The add-in provides comprehensive functionality and parts of the code maybe considered complex. The objective of this article and its accompanying source-code is to provide a fairly complete example of how to write such an add-in and to demonstrate the way the add-in should interact with the VS IDE. It is not guaranteed to be code-complete, nor is the code guaranteed to be bug-free. Although care was taken in the design and coding of the add-in and industry-accepted standards were used in the development, the code should be regarded as demonstrative only, and not necessarily of the quality considered to be "industrial strength."
- How to: Use the XML Documentation Features (C# Programming Guide) - MSDN reference for using the C# documentation features in your source-code.
- XML Comments Let You Build Documentation Directly From Your Visual Studio .NET Source Files - J. Andrew Schafer. Comprehensive information and techniques for rendering the documentation XML to HTML using XSL/T and CSS styles.
- NDoc Code Documentation Generator for .NET - NDoc generates class library documentation from .NET assemblies and the XML documentation files generated by the C# compiler. NDoc uses pluggable documenters to generate documentation in several different formats, including the MSDN-style HTML Help format (.chm), the VS .NET Help format (HTML Help 2), and MSDN-online style web pages.
- Visual Studio 2005 Automation Samples - These Microsoft code samples show you how to build VSMacros projects, add-ins, and wizards.
- Retrieving a CodeElement reliably in VS.NET 2005 - Using the
DTE.ActiveDocument.ProjectItem.FileCodeModel
object to retrieve the CodeElement
in Visual Studio 2005. - SlashDocs: Manage C# XML Documentation in External Files - A Visual Studio Add-In to help to write and maintain C# XML Comment Documentation in external files included using the C# Documentation Comment
<include
tag. Freeware. Installer application for VS2003/.NET 1.1. Source-code not available. - CR_Documentor - The Documentor Plug-In for DXCore - An elegant documentation visualizer with very similar functionality to this add-in. Requires the Developer Express, Inc. DXCore plugin for Visual Studio. Freeware. Installer application for VS2003/.NET 1.1, also works in VS2005/.NET 2.0. Source-code not available.
The figure below shows the x.doc windows within the VS IDE:
Figure 1: The Visual Studio IDE with the x.doc Visualizer and x.doc Output windows shown with a C# source-code window
x.doc provides two windows:
- The Visualizer window, which is the window that provides the rendered XML documentation text, using a defined set of XSL templates and a CSS. This window is interactively updated as the caret is moved in a code-editor window. If the documentation is not well-formed XML or an error occurs in the rendering, an error is displayed in this window. If the caret is not positioned in a documentation block, the message "No documentation" is displayed. This window is implemented to work as a standard tool window in the IDE and can be floated, docked, etc. like any other tool window.
- The Output window is similar to the IDE build window in that it displays the error with the appropriate source-code file path, line and character position offsets. Double-clicking the error line in this window will move the caret to the referenced editor window and the offset in the code at which the error has occurred. It shares the standard output window; its content is displayed by selecting the x.doc menu-item in the 'Show output from:' selector.
x.doc provides a menu tool-strip above the Visualizer window. The available commands are described below, as they are ordered on the tool-strip from left to right:
- Prior Documentation Item: This button command moves the caret to the prior documentation block in the code editor window and selects the block as shown in Figure 1 above.
- Next Documentation Item: This command moves the caret to the next documentation block.
- Collapse All Documentation: This command traverses the entire code document and outlines the documentation blocks. Each documentation block is shown only by three ellipses ("...") and is an effective way to quickly hide all the documentation.
- Import Documentation Item: This button command imports the documentation from an external file and embeds it as a documentation block in the source-code. This will only happen if the caret is positioned over a single-line documentation block containing a well-formed
<include>
tag that points to a valid external documentation file with a name attribute that can be resolved to a documentation item in the external file. See The code section for constraints on the use of this tag. - Export Documentation Item: This command reverses the action of the "Import Documentation Item" above. The documentation block is exported to the external documentation file and a suitably formed
<include>
tag is substituted in its place. See The code section for constraints on the use of this tag. Note that badly-formed XML is exported as-is, so it is best to check the Output window for errors before exporting the documentation block. - Import All Documentation: This command works the same as the "Import Documentation Item" above, except that it traverses the entire source-code file and imports all documentation.
- Export All Documentation: This command exports all documentation to the external documentation file, reversing the action executed by "Import All Documentation" above.
- Documentation Display Style Selector: This selection list will accommodate any number of XSL/CSS templates which are imported from external .xsl and .css files when the add-in is first loaded. The XSL/CSS files x.doc.xsl and x.doc.css are included with the x.doc add-in installer, providing a pseudo-MSDN documentation look-and-feel. Obviously, these files can be modified or more style files added. These can then be individually selected, causing the documentation to be immediately re-rendered using the selected style. It is also possible to extend the VS IDE documentation tags and provide customized documentation functionality. NDoc [3] already does this to provide the various standards of documentation-styles, including their MSDN style, which is exactly like the Microsoft H1 and H2 Help.
x.doc is completely NDoc compatible, in that the XML files that are generated by VS when you compile with the /doc:file option can be used as-is with NDoc. This applies whether the documentation is read directly from the source-code or from an external file.
The installer package is found in the file xdocsetup.zip. It contains the file setup.exe and setup.msi. To install the add-in, run the setup.exe application and follow the install instructions. The add-in files are installed in the [PersonalFolder]\Visual Studio 2005\Addins folder on your computer. This normally equates to something like C:\Documents and Settings\John\My Documents\Visual Studio 2005\Addins if your computer logon name is "John," and you are running Windows XP. the following files are installed in the add-in folder:
- x.doc.Addin: VS 2005 extensibility add-in XML file
- x.doc.dll: Add-in assembly
- x.doc.css: Add-in CSS file for the x.doc render style
- x.doc.xsl: Add-in XSL file for the x.doc style transformations
Once installation has completed, VS can be started. The x.doc Visualizer window should appear as a floating window. If the x.doc window is not visible, the VS Add-in Manager window can be used to activate the x.doc add-in. This is done from the Tools/Add-in Manager... menu item of VS. In the Add-in Manager dialog, tick the "Available Add-ins" and "Startup" columns for the x.doc add-in. To ensure that these settings are permanent, open the x.doc.Addin file in a text-editor, and check that the following text is present:
..
<Extensibility ..>
<Addin>
<LoadBehavior>1</LoadBehavior>
<CommandPreload>1</CommandPreload>
</Addin>
</Extensibility>
..
The above settings indicate that the add-in is available and that it is loaded when VS is started. The zip file xdocsource.zip contains the source-code for the add-in and comprises a C# project -- x.doc.csproj, which is the build project for the x.doc add-in -- and Setup.vdproj, which is the x.doc installer project. In order to debug the add-in, read the text-file readme.txt in the x.doc project and follow the listed instructions. The VS solution file x.doc.sln contains these projects and provides the complete build environment for x.doc.
This section highlights important implementation details of the x.doc add-in. The more obvious and simpler code sections are not discussed and should be easy to follow in the source-code files.
Add-in components
The x.doc add-in window is implemented as a VS tool window that can be docked and floated exactly as the built-in tool windows of the IDE. It is defined in a class x.doc.ToolWindowControl
, which is derived from System.Windows.Forms.UserControl
. The window was implemented using very similar code to the Toolwindow sample in [4]. The window is instanced in the add-in response method Connection.OnConnect
, which is called by the VS extensibility interface when the add-in is connected to the VS IDE:
private Window _windowToolWindow;
..
..
object programmableObject = null;
string guidstr = "{FCBF911C-17BA-4260-88A5-9BD84C5C1188}";
EnvDTE80.Windows2 windows2
= (EnvDTE80.Windows2)_applicationDTE2Object.Windows;
System.Reflection.Assembly asm
= System.Reflection.Assembly.GetExecutingAssembly();
_windowToolWindow = windows2.CreateToolWindow2(_addInInstance, asm.Location,
x.doc.ToolWindowControl.ToolWindowClassName,
x.doc.ToolWindowControl.ToolWindowName,
guidstr, ref programmableObject);
_windowToolWindow.SetTabPicture(Resource.x_doc_tool_bmp.GetHbitmap());
_windowToolWindow.Visible = true;
..
The x.doc ToolWindow consists of a System.Windows.Forms.ToolStrip
object that contains the command buttons and style-selector. The x.doc Visualizer is a standard System.Windows.Forms.WebBrowser
. The x.doc Output window is implemented by referencing the existing VS output window, manipulated through the standard EnvDTE.OutputWindow
interface:
OutputWindow outputWindow = (OutputWindow)_applicationDTE2Object.Windows.Item(
Constants.vsWindowKindOutput).Object;
_outputWindowPane = outputWindow.OutputWindowPanes.Add(
x.doc.ToolWindowControl.ToolWindowName);
Text editor caret tracking and DTE events
The most important function of the add-in is to track the caret (text cursor) in the VS active text-editor window as the user clicks or types in the window. This is done in order to interactively display the rendered documentation XML in the Visualizer window. The way this works is as follows:
- When the caret information has changed -- this is defined as a change in the line, and/or the character position in the line, and/or the active document has changed in the IDE -- a notification handler is called which determines if the active document has changed. If so, the currently active external documentation is saved to the external file, and the appropriate external documentation file is read into memory for the newly activated document.
- The text under the caret in the text-editor is analysed. If it is inside a documentation comment then the extents of the documentation, including the leading comment characters, are determined. Information about the documentation block is returned, including the block start and end points, as well as the textual content of the block. If the caret is not positioned on any documentation comments, the content text string is
null
and the 'NoDocTag' (No Documentation) is displayed in the Visualizer window. - The XML documentation is extracted from the comment-block and rendered using the active XSL/CSS templates. Then the resulting HTML is set in the Visualizer window. If the extracted XML string is not well-formed or if an error exception occurs during the rendering process, a suitable error message is displayed in the Visualizer and Output windows. In most cases, the error message displayed in the Output window provides navigation to the cursor position in the active document where the error occurred.
The code below shows the control method that provides the above functionality:
private void UpdateCaretResponse(bool updateUI, bool ignoreElapsedTime)
{
TextCaretTracker.CaretInformation caretInfo = _caretTracker.TrackCaret();
if (_lastCaretInfoUsed != caretInfo)
{
_caretChangeIndicator = CaretChangeState.Changed;
if (caretInfo.ActiveDocument != _lastCaretInfoUsed.ActiveDocument)
{
OnCaretDocumentChange(caretInfo);
}
_lastCaretInfoTicks = DateTime.Now.Ticks;
}
else
{
if (_caretChangeIndicator == CaretChangeState.Changed)
{
if ((DateTime.Now.Ticks - _lastCaretInfoTicks
>= PauseInterval * 10000) ||
(ignoreElapsedTime))
{
OnCaretChange(caretInfo);
_caretChangeIndicator = CaretChangeState.Unchanged;
if (updateUI)
{
UpdateWindowState(caretInfo, _lastDocumentation);
}
_lastCaretInfoTicks = DateTime.Now.Ticks;
}
}
}
_lastCaretInfoUsed.CopyFrom(caretInfo);
}
The UpdateCaretResponse
method is actually a polling loop as it is called from a System.Windows.Forms.Timer
object Tick
event handler set in the x.doc tool window control. This works very well because the use of a polling loop provides loose coupling between events generated by the DTE extensibility environment -- which mostly re-/-set state-flags in x.doc -- and the subsequent handling of those events by x.doc. Polling also allows two parameters to be used to control the interaction rate with the IDE. The first is the actual timer Interval
, which controls the rate at which caret changes or other DTE events are handled. The second is the pause interval, which is the interval of time that the user does nothing. Only if this elapsed time interval exceeds the preset value in x.doc are the actual documentation rendering and/or other time-consuming functions executed. Setting these time-intervals correctly minimizes the potentially detrimental impact that x.doc has on the typematic responsiveness of the text-editor windows. Currently, the value of 100 msec is used for the timer interval, while the pause interval is set at 500 msec. With these settings, x.doc is acceptably interactive and its impact on the text-editor windows is almost non-existent.
Besides using polling to ascertain the caret position, x.doc also handles some events generated by the VS extensibility interface. Again, code from [4] was used from the EventWatcher project in order to do this. The following code illustrates the setting of event handler delegates:
..
EnvDTE.Events events = _applicationDTE2Object.Events;
_windowsEvents = (EnvDTE.WindowEvents)events.get_WindowEvents(null);
_windowsEvents.WindowActivated +=
new _dispWindowEvents_WindowActivatedEventHandler(OnDTEWindowActivated);
_documentEvents = (EnvDTE.DocumentEvents)events.get_DocumentEvents(null);
_documentEvents.DocumentClosing +=
new _dispDocumentEvents_DocumentClosingEventHandler(OnDTEDocumentClosing);
_documentEvents.DocumentSaved +=
new _dispDocumentEvents_DocumentSavedEventHandler(OnDTEDocumentSaved);
_textDocumentKeyPressEvents =
((EnvDTE80.Events2)events).get_TextDocumentKeyPressEvents(null);
_textDocumentKeyPressEvents.AfterKeyPress +=
new _dispTextDocumentKeyPressEvents_AfterKeyPressEventHandler(
OnDTEAfterKeyPress);
..
The OnDTEWindowActivated
handler is used to ensure that if another text-editor window is activated in the IDE, an update of the Visualizer window is forced. This allows the displayed documentation to be updated correctly:
private void OnDTEWindowActivated(EnvDTE.Window gotFocus,
EnvDTE.Window lostFocus)
{
if (gotFocus.Caption != x.doc.ToolWindowControl.ToolWindowName)
{
ForceCaretUpdate();
}
}
The ForceCaretUpdate
method sets the state of the _lastCaretInfoUsed
member such that any subsequent caret information read from the text-editor is seen as a CaretChangeState.Changed
state. This means the next time the polling loop retrieves the caret information, it will force the appropriate update. This is a very useful technique to provide an asynchronous controlled response to events that come through the VS Extensibility interface.
A similar response method is used for the OnDTEDocumentClosing
handler, which is called when a document is closed in the IDE. This may be the active document in the x.doc add-in's context. The OnDTEAfterKeyPress
handles the condition where the DELETE key is pressed in the text-editor. This results in neither the caret line, caret character position nor active document changing. The only way to pick up this potential change in the documentation text is to handle the key-press directly. The OnDTEDocumentSaved
handler explicitly saves the external documentation file when the source-code document is saved.
The Visualizer window
Once the XML content of the active documentation is extracted, it is transformed into HTML:
public string RenderHtml(string xml)
{
if (xml == null) return null;
string html = null;
if (styleIndex != -1)
{
RenderStyler style = renderStyles[styleIndex];
const string uriPrefix = "file:///";
xml = SetXmlRootTag(xml, MemberTagName);
StringReader stringReader = new StringReader(xml);
StreamReader streamReader = null;
StringWriter stringWriter = null;
XmlTextReader xmlReader = null;
try
{
XPathDocument xpathDocument = new XPathDocument(stringReader);
XslCompiledTransform xslTransform = new XslCompiledTransform();
streamReader = new StreamReader(style.XslPath);
xmlReader = new XmlTextReader(streamReader);
xslTransform.Load(xmlReader);
XsltArgumentList args = new XsltArgumentList();
args.AddParam("csspath", string.Empty, uriPrefix + style.CssPath);
XPathNavigator xpathNavigator = xpathDocument.CreateNavigator();
stringWriter = new StringWriter();
xslTransform.Transform(xpathNavigator, args, stringWriter);
html = stringWriter.ToString();
}
catch (Exception e) {..}
finally {..}
}
else {..}
return html;
}
The path parameter is passed to the transform, where it is used to generate the link to the .css file as the following excerpt from the .xsl file shows:
..
<xsl:param name="csspath"/>
..
<xsl:template match="x.member">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<link rel="stylesheet" type="text/css" href="{$csspath}"/>
</head>
..
</html>
</xsl:template>
..
Since a new HTML page is created for each documentation member, the template should match the x.member
tag. A <head>
is generated that contains the link where the actual value of the csspath parameter is substituted for the {$csspath} token in the template. Once the HTML is created, it is set as the web-browser DocumentText
property:
this.wbDoc.DocumentText = html;
One of the problems with the .xsl is that it generates live links if link-generating documentation tags like <see cref=".../>
are used. Currently these links are not resolved correctly and clicking them produces the standard HTTP-404 Not Found response. A great work-around is as follows: set the HTML in the WebBrowser
control using only the DocumentText
property results in the WebBrowser.Url.AbsolutePath
property, renaming the value "blank" which represents the url "about:blank
." However, loading an HTML page by clicking on a link attempts to set the Url
property of the browser. The following code allows navigation to be cancelled if a link on the page is clicked, as opposed to the DocumentText
property being set:
..
this.wbDoc.Navigating +=
new System.Windows.Forms.
WebBrowserNavigatingEventHandler(this.wbDoc_Navigating);
..
..
private void wbDoc_Navigating(object sender, WebBrowserNavigatingEventArgs e)
{
if(e.Url.AbsolutePath != "blank") e.Cancel = true;
}
The Output window
By by double-clicking the error line, the VS IDE output window can be used to navigate from a reported error in the output window to the text-position in the text-editor document where the error was caused. This is used by the VS Build window. x.doc provides the same feature to report XML errors, XML-to-HTML transformation errors and other errors that are produced as a result of the x.doc add-in causing an exception. The format of such an error text line is as follows:
{full-filepath}(line,position): {message-text}
where full-path is the full file-path of the source-code file,
line is the source-code file line-number in the text-editor,
position is the character position offset in the line, and
message-text is the actual error message.
Example:
D:\Development\x.doc\Samples\DocumentationSample\
DocumentationSample.cs(13,31):
'value' is an unexpected token. The expected token is '>'.
Line 13, position 37.
The errors are reported with line and position numbers based on the offset from the start of the XML string extracted from the documentation block. The FormatDocumentationError
method converts these values to the correct line and column position, relative to the actual source-code file.
External documentation
A powerful feature of documentation comments is that the documentation can be saved in an external XML text file, and referenced using a single comment line from the source-code file. This feature is built into x.doc and the reference works as follows, shown in a normal comment block for C#:
/// <include file='xdoc\{code-file-name}.xdoc'
// path='x.doc/x.member[@name="{code-element-name}"]/*'/>
The <include>
tag requires two attributes:
- The
file
attribute: This attribute provides the path of the external document file. It can be an absolute or relative path. x.doc will generate a separate documentation file for every source-code file. It will have a file path of ".\xdoc\" to the source-code file path, the same file name as the source-code file, and an .xdoc file extension. In this way, the documentation file can be easily moved with the source-code file to another VS project, without loosing any documentation text. - The
path
attribute: This attribute provides the reference to the x.member
element in the external documentation XML that is identified with the name
attribute that has the same value as contained in {code-element-name}
.
An excerpt is shown from a typical external documentation file:
="1.0"="utf-8"
<x.doc>
..
..
<x.member name="x.doc.CSharp.Samples.DocumentationSample.DocumentationSample">
<summary>
Initializes a new instance of a
<c>DocumentationSample</c> type.
</summary>
<example>
The following is an example of initializing a
<c>DocumentationSample</c> type:
<code>
// Create the type.
DocumentationSample ds = new DocumentationSample();
if (null == ds)
return;
return ds.SomeMethod("someString");
</code>
</example>
</x.member>
..
..
</x.doc>
The required <include>
element in the referencing code will be:
<include file='xdoc\DocumentationSample.xdoc' path='x.doc/x.member[
@name="x.doc.CSharp.Samples.DocumentationSample.DocumentationSample"]/*'/>
From the element defined above, it can be seen that the file-name of the source-code file is DocumentationSample and that the external documentation file is DocumentationSample.xdoc.
The most interesting part of the automatic <include>
element generation that x.doc provides is in the generation of the name
attribute. The name has to be unique and should perhaps be interpreted by humans as well. The following scheme is used, which should work correctly even if all the documentation for all classes and namespaces in a VS project -- or even a VS solution -- are merged:
{code-namespace-name}.{
declared-type-name}[.{[method-name] |
[property-name] | [event-name] | [enum-value] | ...}]
This construct is the formal or full-name of the construct being commented. It can obtained from the ProjectItem.FileCodeModel
property of the Document
in the active text-editor window. [5] provides a detailed description of how to do this. x.doc uses this technique in a method called GetCodeElementName
to interactively determine the code element that is being documented. GetCodeElementName
provides a comprehensive set of validation rules to ensure that the <include>
element is correctly formed when it is substituted for the documentation XML. The method returns an EnvDTE.CodeElement
object and the name attribute is set to the CodeElement.FullName
. One caveat: if a code-file is edited that is not part of any of the loaded VS projects, the ProjectItem.FileCodeModel
property is null
. This means that for these files, the documentation can be imported but not exported, as it is impossible to generate the required name from the ProjectItem.FileCodeModel
property.
Another interesting problem was encountered when replacing the TextSelection
of the active source-code Document
. Using the TextSelection.Text
or TextSelection.DestructiveInsert
members, VS attempts to format the replaced text as if it is being typed in; i.e. XML closing tags are added immediately after the opening tags! The way around this is to copy the text to the clipboard and then paste it into the document:
..
Clipboard.SetData(DataFormats.Text, commentSection);
selection.Paste();
..
If time permitted, the following may have also worked:
- Undo support: Exporting and importing of documentation are, in essence, editor-based operations. As such, they can be undone using the VS Edit/Undo ... commands or the Ctrl+Z key. Currently, x.doc only supports undo actions on the source-code file, not the external documentation file. The result is that the two files can get out of synchronization and produce unexpected Documentation not found errors. There are a few ways around this: only the currently active document's external documentation is loaded into memory in a
Hashtable
. x.doc could be modified to load every loaded source-code document's documentation and switch between the various Hashtable
s as the active document is changed. Then, instead of saving the documentation every time an active document becomes inactive (when that text-editor window loses focus), it need only be saved when the source-code itself is saved. x.doc adds the documentation block to the external documentation Hashtable
when the documentation is exported. When it is imported, the entry in the external documentation Hashtable
is deleted. Currently, in the installer add-in, the deletion is not done. Only the documentation is substituted for the <include>
element in the source-code. - Add to project: As the external documentation file is integral to the source-code file if
<include>
s are used, it should be added as a source-code dependent file to the project when it is created. - Add-in settings: Some of the attributes provided are arguably properties that should be under user control. A useful feature would have x.doc support the modification of these properties in the VS Tools/Options... Options Dialog. [4] provides a ToolsOptionsPage sample add-in that does this.
- NDoc detail documentation: It should be possible to provide some of the sophistication of NDoc [3] interactively, without the need to reflect the declared types from the compiled assemblies. Using the
ProjectItem.FileCodeModel
to do this should be investigated. - Support for other .NET languages: An add-in such as this should at least provide the same support for VB and C++ as well as the currently supported C# language. Place-holders (read
TODO:
) have been placed in the code which should make this possible. This feature may form the basis for the next update of this add-in.
17 May 2007: Version 1.0.1
- Article edited and posted to the main CodeProject.com article base.
17 May 2006: Version 1.0.1
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.