Google Analytics

Tuesday, July 15, 2014

Configuring GhostDoc Pro, part 1 - File, Class, and Properties Headers

I started this post back in the beginning of 2013. I apparently tossed my exported rules(*), so am redoing them and thought I would revisit my own, hopefully useful, advice.

I swear, why is it so hard to find documentation on doing some of the simplest programming things? Why is it that these companies can create such a relatively useful product, and yet it is impossible to find any real sort of documentation on how to use them? I have multiple blog entries brewing on my frustrations using Infragistics Reporting in their NetAdvantage suite. Today though, I'm going to rail against SubMain's GhostDoc Pro.

GhostDoc, a VisualStudio extension for automatically generating XML documentation and code comments, is a total time saver when it is properly configured. Because of the government certification our products generally go through, our code has to be meticulously commented in very specific ways. At one point my rules were properly configured and I had spent about half a day configuring the templates as well as I could, so that all I had to do was go to Tools > GhostDoc > Document File and my code file would be properly documented including headers, properties and fields. Unfortunately when I chose to upgrade my current version of GhostDoc it apparently took my desire to "Upgrade" my current rules file and merely used that as a "suggestion", which it then ignored, and I ended up with the default GhostDoc Rules templates. *So my first suggestion to you is that as soon as you have the templates the way you want them, export them to avoid having go through the hassle of re-creating them.

Secondly, bookmark this blog post as reference to refer back to later. The extent of SubMain's online documentation for creating your own templates seems to be to simply rely on the community of GhostDoc users creating blog posts in their community pages (http://community.submain.com/) and on StackOverflow. If you go to the tutorials on SubMain's site you will notice there are no tutorials for GhostDoc. (I take it back. Since I first began writing this blog article way long ago, a simple tutorial has been added. It is a very quick overview of all of GhostDoc Pro's features.) The help file that is downloaded for GhostDoc Pro contains an Introduction to T4 Templates and a description of each of the different templates that are used.

Editing T4 Templates
To edit the GhostDoc Pro T4 Rules templates, in Visual Studio go to Tools > GhostDoc Pro > Options > Rules and select the template you want to update. The template will open into an editor that does provide some Intellisense and color-coding.

Adding Global Properties
Global properties, accessible through the GetGlobalProperty of the Context object, can be added in the GhostDoc Pro options window under Global Properties. Here you can enter the key-value pairs for commonly used properties throughout your templates.

File Header

By necessity, our standard file header looks like this:

// -----------------------------------------------------------------------------
// <copyright company="Dominion Voting Systems" file="ElectionDataService.cs">
//     Copyright (c) 2013 Dominion Voting Systems, Inc. All Rights Reserved.
//     Any distribution of source code by others is prohibited.
// </copyright>
// <summary>
//    This file contains the Election Data Service Class
// </summary>
// <revision date="1/11/2013" revisor="ben.rice">
//     File created.
// </revision>
// -----------------------------------------------------------------------------

In order to create this I am using the following template.

<#@ template language="C#" #>
// -----------------------------------------------------------------------------
// ">
//     Copyright (c) <#= Context.DestinationFileCreationDateTime.ToString("yyyy") #> <#= Context.GetGlobalProperty("CompanyName") #>. All Rights Reserved.
//     Any distribution of source code by others is prohibited.
//
//









//     This file contains the <#= System.IO.Path.GetFileNameWithoutExtension(Context.DestinationFile) #> class.
//
// " date="<#= Context.DestinationFileCreationDateTime.ToString("MM/dd/yyyy") #>">
//     File created.
//
// -----------------------------------------------------------------------------

You can see that the Context object is perhaps the most useful in creating these templates. It's use is based on the template in which it is used. In this case, for example, the Context.Exceptions would be empty since the context of its use here is the File and Exceptions enumerates only any exceptions thrown by a method or property. You can also see the use of Context to retrieve our Global Properties. Of course, this can be done in whatever template you are editing. Also note that I can use the standard .Net objects and methods to get the file name without the file extension, for example. 

I need to modify the rule yet to add the "File modified." revision note. For now I throw that on manually.

Class Header
Our standard class header:

///









///     Interaction logic for navigating views within the Ballot Viewer.
///
///
///     Class created.
///

For this I am basically using the default class template for GhostDoc Pro.

<#@ template language="C#" #>
<#  CodeElement codeElement = Context.CurrentCodeElement; #>
///









///<# GenerateSummaryText(); #>
///
<#  if(codeElement.HasTypeParameters) 
{
for(int i = 0; i < codeElement.TypeParameters.Length; i++) 

TypeParameter typeParameter = codeElement.TypeParameters[i]; 
#>
/// <# GenerateTypeParamText(typeParameter, i); #>
<# }

#>
<#= Context.GetNonGeneratedTags() #>
<# GenerateRemarksText(); #>

<#+
private void GenerateSummaryText()
{
if(Context.HasExistingTagText("summary"))
{
this.WriteLine(Context.GetExistingTagText("summary"));
}
else if(Context.CurrentCodeElement.IsSealed)
{
this.WriteLine("TODO: Add summary for Class " + Context.CurrentCodeElement.Name + ". This class cannot be inherited." + Context.ExecMacro("$(End)"));
}
else
{
this.WriteLine("TODO: Add summary for Class " + Context.CurrentCodeElement.Name + "." + Context.ExecMacro("$(End)"));
}
}
    
private void GenerateTypeParamText(TypeParameter typeParameter, int index)
{
if(Context.HasExistingTagText("typeparam", index)) 

this.Write(Context.GetExistingTagText("typeparam", index));

else 
{
string typeParameterName = typeParameter.Name;
if(typeParameterName != null)
{
if(typeParameterName.Length == 1)
{
this.Write("");
}
else
{
this.Write("The type of " + Context.ExecMacro(typeParameterName, "$(TheAndAll)") + ".");
}            
}          

}

private void GenerateRemarksText()
{
if(Context.HasExistingTagText("remarks"))
{ #>
/// <#= Context.GetExistingTagText("remarks") #>
<#+    }
else if (!string.IsNullOrEmpty(Context.GetGlobalProperty("DefaultBlankRemarksText")))

            // Should you require a default comment, set it in
// Options -> Global Properties -> DefaultBlankRemarksText
#>
/// <#= Context.GetGlobalProperty("DefaultBlankRemarksText") #>
<#+    }
}
#>

Notice that within the template I can call C# methods coded below. In this case we are generating type parameter elements for each type parameter if there are any. If a summary element or remarks element already exists in the header, that text is preserved, else some default text is inserted. Mine has a TODO reminder in the summary text to highlight that I need to change that.

I'm going to skip ahead now to the Properties rule

Our property header will look something like this:
///









///     Gets or sets the name of the election.
///
/// The name of the election.
///
///     Member created.
///
///
///     Changed to allow public setter.
///

I added to the default property generated in order to set the revision element.

<#@ template language="C#" #>
///









///<# GenerateSummaryText(); #>
///
<# GenerateValueText(); #>
<# if(Context.HasExceptions) 
{
foreach (System.Collections.Generic.KeyValuePair> pair in Context.ExceptionDictionary)
{
string exceptionText = Context.GetExistingExceptionTagText(pair.Key); #>
/// <#= ((exceptionText != null && exceptionText.Length > 0) ? exceptionText : Context.GetExceptionTagText(pair.Value)) #>
<# }
}  #>
<#= Context.GetNonGeneratedExceptionTags() #>
<#= Context.GetNonGeneratedTags() #>
<# GenerateRevisionTag(); #>
<# GenerateRemarksText(); #>

<#+
private void GenerateSummaryText()
{
CodeElement codeElement = Context.CurrentCodeElement;
   
if(Context.HasExistingTagText("summary"))
{
this.WriteLine(Context.GetExistingTagText("summary"));
}
else if(Context.HasInheritedTagText("summary"))
{
this.WriteLine(Context.GetInheritedTagText("summary"));
}
else if(IsBooleanProperty()) 
{
if(IsStateCheckBooleanProperty())
{
// state check boolean property
this.WriteLine(Context.ExecMacro("$(PropertyAccessText) a value indicating whether this instance $(PropertyName.Words.All)."));
}
else if(IsSingleWordBooleanProperty())
{
// single word boolean property
this.WriteLine(Context.ExecMacro("$(PropertyAccessText) a value indicating whether this $(DeclaringTypeName.ShortNameAsSee) is $(PropertyName.Words.First)."));
}
else
{
// boolean property
this.WriteLine(Context.ExecMacro("$(PropertyAccessText) a value indicating whether $(End)[$(PropertyName.Words.All)]."));
}
}
else if(Context.ContainsOfTheReordering(codeElement.Name)) 
{
this.WriteLine(Context.ExecMacro(codeElement.Name, "$(OfTheReordering)", Context.ExecMacro("$(PropertyAccessText)")));
}
else
{
this.WriteLine(Context.ExecMacro("$(PropertyAccessText) $(PropertyName.Words.TheAndAll)."));
}
}
        
private void GenerateValueText()
{
CodeElement codeElement = Context.CurrentCodeElement;
        
if(Context.HasExistingTagText("value"))

#>
/// <#= Context.GetExistingTagText("value") #>
<#+
}
else if(Context.HasInheritedTagText("value"))

#>
/// <#= Context.GetInheritedTagText("value") #>
<#+
}
else /*if(Context.CurrentCodeElement.HasSet) - commented for StypeCop compliance (SA1609) */
{
if(IsBooleanProperty()) 
{
.....
}
}

.... 
    
private bool IsBooleanProperty()
{
CodeElement codeElement = Context.CurrentCodeElement;  
return (bool)(codeElement.ReturnType != null && 
codeElement.ReturnType.FullName != null &&
codeElement.ReturnType.FullName.Length > 0 && 
(string.Compare( codeElement.ReturnType.FullName, "System.Boolean", true) == 0 ||
string.Compare( codeElement.ReturnType.FullName, "System.Nullable", true) == 0));
}  

private void GenerateRemarksText()
{
if(Context.HasInheritedTagText("remarks"))
{ #>
/// <#= Context.GetInheritedTagText("remarks") #>
<#+    }
else if(Context.HasExistingTagText("remarks"))
{ #>
/// <#= Context.GetExistingTagText("remarks") #>
<#+    }
else if (!string.IsNullOrEmpty(Context.GetGlobalProperty("DefaultBlankRemarksText")))

            // Should you require a default comment, set it in
// Options -> Global Properties -> DefaultBlankRemarksText
#>
/// <#= Context.GetGlobalProperty("DefaultBlankRemarksText") #>
<#+    }
}

private void GenerateRevisionTag()

if( Context.HasExistingTagText("summary")) {#>
/// <revision revisor="<#= Context.GetGlobalProperty("UserName") #>" date="<#= DateTime.Now.ToString("MM/dd/yyyy") #>">
///     TODO: Add Revision
/// </revision>
///
<#+
} else { #>
/// " date="<#= DateTime.Now.ToString("MM/dd/yyyy") #>">
///     Member created.
///
<#+ }
}
#>

Please note that in the interest of space, I omitted code that determines how to word the value element of a boolean property. In the custom "GenerateRevisionTag" method I did a check to see if the summary has already been written. If it hadn't been (prior to this GD call), I assume the property has just been created and add "Member created." to the revision text. Otherwise, I assume that the property already existed, but is being revised and add the reminder to myself to update what the revision was. Also note that the GenerateRevisionTag call is made after Context.GetNonGeneratedTags is written. This makes later revisions appear below previously written ones.

No comments: