ASP Web API Help pages - Link to class from XML <see> tag

 ̄綄美尐妖づ 提交于 2019-11-29 17:27:27
Apex ND

my solution is the following:

  1. you've got your custom documentation (from the generated xml file) working
  2. 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;
  3. 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);    
}
  1. 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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!