I'm working on Developing a Web-API project, and i'm very impressed with the auto generated documentation by Microsoft's HelpPages.
i enabled custom documentation using the official site creating Help Pages
the documentation is generated successfully BUT none of the references to Classes from the <See cref="">
Tag seems to be added to the description, the HelpPages Simply ignores them (that's for a reason).
i really wanted to have this feature in my project, i searched a lot (got close sometimes) but none gave a convincing answer.
That's why i decided to post my solution to this tweak and hopefully benefit other programmers and spare them some time and effort.
(my answer is in the replies below)
my solution is the following:
- you've got your custom documentation (from the generated xml file) working
- enable HTML and XML tags within the documentation, they normally get filtered out, thanks this Post you can preserve them.
simply go to: ProjectName > Areas > HelpPage > XmlDocumentationProvider.cs
on line 123 in method: GetTagValue(XPathNavigator parentNode, string tagName)
change the code return node.Value.Trim();
to return node.InnerXml;
- create the following partial view:
ProjectName\Areas\HelpPage\Views\Help**_XML_SeeTagsRenderer.cshtml**
this is my code:
@using System.Web.Http;
@using MyProject.Areas.HelpPage.Controllers;
@using MyProject.Areas.HelpPage;
@using MyProject.Areas.HelpPage.ModelDescriptions
@using System.Text.RegularExpressions
@model string
@{
int @index = 0;
string @xml = Model;
if (@xml == null)
@xml = "";
Regex @seeRegex = new Regex("<( *)see( +)cref=\"([^\"]):([^\"]+)\"( *)/>");//Regex("<see cref=\"T:([^\"]+)\" />");
Match @xmlSee = @seeRegex.Match(@xml);
string @typeAsText = "";
Type @tp;
ModelDescriptionGenerator modelDescriptionGenerator = (new HelpController()).Configuration.GetModelDescriptionGenerator();
}
@if (xml !="" && xmlSee != null && xmlSee.Length > 0)
{
while (xmlSee != null && xmlSee.Length > 0)
{
@MvcHtmlString.Create(@xml.Substring(@index, @xmlSee.Index - @index))
int startingIndex = xmlSee.Value.IndexOf(':')+1;
int endIndex = xmlSee.Value.IndexOf('"', startingIndex);
typeAsText = xmlSee.Value.Substring(startingIndex, endIndex - startingIndex); //.Replace("<see cref=\"T:", "").Replace("\" />", "");
System.Reflection.Assembly ThisAssembly = typeof(ThisProject.Controllers.HomeController).Assembly;
tp = ThisAssembly.GetType(@typeAsText);
if (tp == null)//try another referenced project
{
System.Reflection.Assembly externalAssembly = typeof(MyExternalReferncedProject.AnyClassInIt).Assembly;
tp = externalAssembly.GetType(@typeAsText);
}
if (tp == null)//also another referenced project- as needed
{
System.Reflection.Assembly anotherExtAssembly = typeof(MyExternalReferncedProject2.AnyClassInIt).Assembly;
tp = anotherExtAssembly .GetType(@typeAsText);
}
if(tp == null)//case of nested class
{
System.Reflection.Assembly thisAssembly = typeof(ThisProject.Controllers.HomeController).Assembly;
//the below code is done to support detecting nested classes.
var processedTypeString = typeAsText;
var lastIndexofPoint = typeAsText.LastIndexOf('.');
while (lastIndexofPoint > 0 && tp == null)
{
processedTypeString = processedTypeString.Insert(lastIndexofPoint, "+").Remove(lastIndexofPoint + 1, 1);
tp = SPLocatorBLLAssembly.GetType(processedTypeString);//nested class are recognized as: namespace.outerClass+nestedClass
lastIndexofPoint = processedTypeString.LastIndexOf('.');
}
}
if (@tp != null)
{
ModelDescription md = modelDescriptionGenerator.GetOrCreateModelDescription(tp);
@Html.DisplayFor(m => md.ModelType, "ModelDescriptionLink", new { modelDescription = md })
}
else
{
@MvcHtmlString.Create(@typeAsText)
}
index = xmlSee.Index + xmlSee.Length;
xmlSee = xmlSee.NextMatch();
}
@MvcHtmlString.Create(@xml.Substring(@index, @xml.Length - @index))
}
else
{
@MvcHtmlString.Create(@xml);
}
Finally Go to: ProjectName\Areas\HelpPage\Views\Help\DisplayTemplates**Parameters.cshtml**
at line '20' we have the code corresponding to the Description in the documentation.
REPLACE this:
<td class="parameter-documentation">
<p>
@parameter.Documentation
</p>
</td>
With THIS:
<td class="parameter-documentation">
<p>
@Html.Partial("_XML_SeeTagsRenderer", (@parameter.Documentation == null? "" : @parameter.Documentation.ToString()))
</p>
</td>
& Voila you must have it working now.
Notes:
- i tried putting HTML list inside the docs and it rendered it fine
- i tried multiple class references (multiple
<see cref="MyClass">
and i worked fine
- you can't refer to a class that is declared within a class
- when you refer to a class that is outside the current project please add the assembly .getType of a class within that project (check my code above)
- any un-found class found inside a
<see cref>
will have it's full name printed in the description (for example if you reference a property or namespace, the code won't identify it as a type/class but it will be printed)
I have implemented class which process xml documentation block and changes documentation tags to html tags.
/// <summary>
/// Reprensets xml help block converter interface.
/// </summary>
public class HelpBlockRenderer : IHelpBlockRenderer
{
/// <summary>
/// Stores regex to parse <c>See</c> tag.
/// </summary>
private static readonly Regex regexSeeTag = new Regex("<( *)see( +)cref=\"(?<prefix>[^\"]):(?<member>[^\"]+)\"( *)/>",
RegexOptions.IgnoreCase);
/// <summary>
/// Stores the pair tag coversion dictionary.
/// </summary>
private static readonly Dictionary<string, string> pairedTagConvertsion = new Dictionary<string, string>()
{
{ "para", "p" },
{ "c", "b" }
};
/// <summary>
/// Stores configuration.
/// </summary>
private HttpConfiguration config;
/// <summary>
/// Initializes a new instance of the <see cref="HelpBlockRenderer"/> class.
/// </summary>
/// <param name="config">The configuration.</param>
public HelpBlockRenderer(HttpConfiguration config)
{
this.config = config;
}
/// <summary>
/// Initializes a new instance of the <see cref="HelpBlockRenderer"/> class.
/// </summary>
public HelpBlockRenderer()
: this(GlobalConfiguration.Configuration)
{
}
/// <summary>
/// Renders specified xml help block to valid html content.
/// </summary>
/// <param name="helpBlock">The help block.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The html content.</returns>
public HtmlString RenderHelpBlock(string helpBlock, UrlHelper urlHelper)
{
if (string.IsNullOrEmpty(helpBlock))
{
return new HtmlString(string.Empty);
}
string result = helpBlock;
result = this.RenderSeeTag(result, urlHelper);
result = this.RenderPairedTags(result);
return new HtmlString(result);
}
/// <summary>
/// Process <c>See</c> tag.
/// </summary>
/// <param name="helpBlock">Hte original help block string.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The html content.</returns>
private string RenderSeeTag(string helpBlock, UrlHelper urlHelper)
{
string result = helpBlock;
Match match = null;
while ((match = HelpBlockRenderer.regexSeeTag.Match(result)).Success)
{
var originalValues = match.Value;
var prefix = match.Groups["prefix"].Value;
var anchorText = string.Empty;
var link = string.Empty;
switch (prefix)
{
case "T":
{
// if See tag has reference to type, then get value from member regex group.
var modelType = match.Groups["member"].Value;
anchorText = modelType.Substring(modelType.LastIndexOf(".") + 1);
link = urlHelper.Action("ResourceModel", "Help",
new
{
modelName = anchorText,
area = "ApiHelpPage"
});
break;
}
case "M":
{
// Check that specified type member is API member.
var apiDescriptor = this.GetApiDescriptor(match.Groups["member"].Value);
if (apiDescriptor != null)
{
anchorText = apiDescriptor.ActionDescriptor.ActionName;
link = urlHelper.Action("Api", "Help",
new
{
apiId = ApiDescriptionExtensions.GetFriendlyId(apiDescriptor),
area = "ApiHelpPage"
});
}
else
{
// Web API Help can generate help only for whole API model,
// So, in case if See tag contains link to model member, replace link with link to model class.
var modelType = match.Groups["member"].Value.Substring(0, match.Groups["member"].Value.LastIndexOf("."));
anchorText = modelType.Substring(modelType.LastIndexOf(".") + 1);
link = urlHelper.Action("ResourceModel", "Help",
new
{
modelName = anchorText,
area = "ApiHelpPage"
});
}
break;
}
default:
{
anchorText = match.Groups["member"].Value;
// By default link will be rendered with empty anrchor.
link = "#";
break;
}
}
// Build the anchor.
var anchor = string.Format("<a href=\"{0}\">{1}</a>", link, anchorText);
result = result.Replace(originalValues, anchor);
}
return result;
}
/// <summary>
/// Converts original help paired tags to html tags.
/// </summary>
/// <param name="helpBlock">The help block.</param>
/// <returns>The html content.</returns>
private string RenderPairedTags(string helpBlock)
{
var result = helpBlock;
foreach (var key in HelpBlockRenderer.pairedTagConvertsion.Keys)
{
Regex beginTagRegex = new Regex(string.Format("<{0}>", key), RegexOptions.IgnoreCase);
Regex endTagRegex = new Regex(string.Format("</{0}>", key), RegexOptions.IgnoreCase);
result = beginTagRegex.Replace(result, string.Format("<{0}>", HelpBlockRenderer.pairedTagConvertsion[key]));
result = endTagRegex.Replace(result, string.Format("</{0}>", HelpBlockRenderer.pairedTagConvertsion[key]));
}
return result;
}
/// <summary>
/// Gets the api descriptor by specified member name.
/// </summary>
/// <param name="member">The member fullname.</param>
/// <returns>The api descriptor.</returns>
private ApiDescription GetApiDescriptor(string member)
{
Regex controllerActionRegex = new Regex("[a-zA-Z0-9\\.]+\\.(?<controller>[a-zA-Z0-9]+)Controller\\.(?<action>[a-zA-Z0-9]+)\\(.*\\)");
var match = controllerActionRegex.Match(member);
if (match.Success)
{
var controller = match.Groups["controller"].Value;
var action = match.Groups["action"].Value;
var descriptions = this.config.Services.GetApiExplorer().ApiDescriptions;
return descriptions.FirstOrDefault(x => x.ActionDescriptor.ActionName.Equals(action) &&
x.ActionDescriptor.ControllerDescriptor.ControllerName == controller);
}
return null;
}
}
To use it, you will need to change XmlDocumentationProvider
class:
private static string GetTagValue(XPathNavigator parentNode, string tagName)
{
if (parentNode != null)
{
XPathNavigator node = parentNode.SelectSingleNode(tagName);
if (node != null)
{
return node.InnerXml;
}
}
return null;
}
And then I wrote extension class to use this class directly from view:
/// <summary>
/// Represents html help content extension class.
/// Contains methods to convert Xml help blocks to html string.
/// </summary>
public static class HtmlHelpContentExtensions
{
/// <summary>
/// Converts help block in xml format to html string with proper tags, links and etc.
/// </summary>
/// <param name="helpBlock">The help block content.</param>
/// <param name="urlHelper">The url helper for link building.</param>
/// <returns>The resulting html string.</returns>
public static HtmlString ToHelpContent(this string helpBlock, UrlHelper urlHelper)
{
// Initialize your rendrer here or take it from IoC
return renderer.RenderHelpBlock(helpBlock, urlHelper);
}
}
And finally, for example in Parameters.cshtml:
<td class="parameter-documentation">
<p>@parameter.Documentation.ToHelpContent(Url)</p>
@if(!string.IsNullOrEmpty(parameter.Remarks))
{
<p>@parameter.Remarks.ToHelpContent(Url)</p>
}
</td>