Using Microsoft ASP.NET Web API 2 Help Page – Part 2

This post is a continuation of my previous post Documenting the WebApi using Microsoft ASP.NET Web API 2 Help Page

Including multiple XML files

The first change that I wanted to make was that the HelpPage is only reading from one XML file. Each project is producing its own XML file, and by default the XML file is saved in the bin folder. The UI project is getting its xml file saved to the app_data folder, and I wanted the core project to get its XML file saved to the same location. From the Core project properties, from the Build enable XML Documentation file. This value cannot use macros, so I saved the file to xml\core.xml. I initially tried to use macros to save the XML file, but it just created a folder to match what macros where being used. Second step was to modify the Build Events Post Build command with the following command to copy the XML file to be App_Data folder.

copy $(ProjectDir)xml\core.xml $(SolutionDir)\MotorDB.UI\App_Data

I did this because I have always hated seeing ..\..\ inside a solution file, and this two step process just seems cleaner to me. This is personal preference only. Feel free to do whatever you feel comfortable with.

Now that all project XML files are being saved to the App_Data folder, we need to tell the HelpPage registration to use multiple files.

Created a new class, MergeXml, which takes the path of two XML files, and produces a single file

public class MergeXml
{
	public XPathNavigator MergeDocument(string rootXmlDocument, string xmlDocumentToMerge)
	{
		var xml1 = XDocument.Load(rootXmlDocument);
		var xml2 = XDocument.Load(xmlDocumentToMerge)
                    .Descendants("members");

		foreach (var element in xml2.Descendants("member"))
		{
			xml1.Root.Element("members").
                            LastNode.AddAfterSelf(element);
		}
		return xml1.CreateNavigator();
	}
}

This class can only be used to merge two XML files that are produced by Visual Studio, it cannot be used to merge the contents of any two XML files.

After I wrote this class, I realized that I should have passed in a path, instead of location of two files. But as I am only wanting to merge two files at present I have left the class as it. If a third project is ever introduced, then I will look into rewriting this to accept a path only parameter.

I then created an overloaded constructor within the XmlDocumentationProvider class to accept two parameters for location of the two XML files

public XmlDocumentationProvider(string rootDocument,
                                string secondaryDocument)
{
	var mergeXML = new MergeXml();
	if (string.IsNullOrEmpty(rootDocument) ||
            string.IsNullOrEmpty(secondaryDocument))
	{
		throw new ArgumentNullException(string.Format(
                        "Invalid file locations {0}, {1}",
			rootDocument, secondaryDocument));
	}
	_documentNavigator = mergeXML.MergeDocument(rootDocument, secondaryDocument);
}

Then finally modified the HelpPageConfig register method to call the newly created overloaded constructor

config.SetDocumentationProvider(new XmlDocumentationProvider(
    HttpContext.Current.Server.MapPath("~/App_Data/UI.xml"),
    HttpContext.Current.Server.MapPath("~/App_Data/Core.xml")));

I will explain later why I wanted to import multiple XML files.

Help Page UI

I have never been very happy with the default layout that is provided with the WebApi HelpPage. In the previous post I changed the _ViewStart page to refer to the common _Layout.cshtml page. I changed it back as wanted to do some changes that where going to be specific to the documentation layout and I did not want these changes to affect the entire site.

My modified _Layout.cshtml page for the Documentation is now.

@using System.Web.Optimization
<!DOCTYPE html>
<html>
    <head>
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width" />
         <title>@ViewBag.Title</title>
        	<link href="~/Content/motordb.css" rel="stylesheet" />
        @Styles.Render("~/Content/css")
        @Styles.Render("~/Content/themes/base/css");
        @Scripts.Render("~/bundles/modernizr")
    </head>
    <body>
        @RenderBody()
        @Scripts.Render("~/bundles/jquery")
        @Scripts.Render("~/bundles/jqueryui")
        @RenderSection("scripts", required: false)
    </body>
</html>

In the initial page, the rendering of the scripts was being done at the top of the page. This is now againsts the convention for scripts, so I moved this to the end.
As the solution is going to make use of jquery, needed to bring in references to jquery and jqueryui.
I created a separate stylesheet for these pages, I decided to just link directly to the stylesheet, instead of creating a new style bundle.

Run the application, go to the documentation, and everything is being displayed.

In the project as it currently stands, there is not that many api calls, but I have seen other api documentation that, well there are no words to describe it. The closest I can come that is still polite is bad. And its down to the number of routes that have been configured.

For example, you have the following routes defined

routes.MapHttpRoute("SearchAPI",
	"api/search/{controller}/{id}", new { id = RouteParameter.Optional });
routes.MapHttpRoute("RestAPIUserPreferences",
	"api/User/Preferences/{controller}/{id}", new { id = RouteParameter.Optional });
routes.MapHttpRoute("RestAPI",
	"api/{controller}/{id}", new { id = RouteParameter.Optional });

Using the PolicyController, if it did not have the RoutePrefixing defined, then the HelpPage will produce a call using each of the routes defined. The first time this occurred to me, I thought that I had identified a bug, but after running Fiddler, and calling every route that the documentation produced, the correct results where being returned. Another side benefit of why I really like the new route prefix for ApiController.

The modified layout page is referring to motordb.css. I created this stylesheet, with the following style only

.controller_api {
    display: none;
}

To hide the table that contains all the api calls, you need to modify the partial view ApiGroup.cshtml

Added the new style to the table style. In addition, I also gave each table a unique id, which corresponds to the name of the Controller


<table class="help-page-table controller_api"
    id="@Model.Key.ControllerName">

Running the api documentation, the table that list the api calls per controller is now not being displayed. Next step was to add a button for each controller that will show/hide the api calls

<button
        data-text-swap="Hide @Model.Key.ControllerName API Calls"
        data-text-original="Hide @Model.Key.ControllerName API Calls">
    Show @Model.Key.ControllerName API calls
</button>

The button is making use of HTML5 data attributes as this will be used to change the text of the button. The button needs to be placed above the table element. The reason why will become clearer in a few minutes

On the main index.cshtml file, we need to include some javascript that will control the display of the api documentation, and the text of the button

<script type="text/javascript">
$(function() {
	$(":button").button(
		{
			icons: { primary: "ui-icon-info" }
		});
	$(":button").on("click", ShowHideApiCalls);
});
function ShowHideApiCalls() {
    var buttonObject = $(this);
    var apiTable = buttonObject.next(".controller_api");
    if (buttonObject.text() == buttonObject.data("text-swap")) {
        buttonObject.button( "option", "label", buttonObject.data("text-original") );
        apiTable.hide();
    } else {
        buttonObject.data("text-original", buttonObject.text());
        buttonObject.button( "option", "label", buttonObject.data("text-swap") );
        apiTable.show();
    }
}
</script>

What is first being performed in the above is to use jquery to find all button elements on the form and turn them into jQueryUI buttons, with a default info icon.

The second jQuery statement is to bind the click of each button to method ShowHideApiCalls. Within the method get the button object that called the method, and using this button, find the next table that has the class name “controller_api”. It is because of this jQuery command to find the next element is why the button needed to be placed above the table.

The rest of the method is just checking to see what is the current text of the button and if it matches the default text. The text is pretty easy to follow. I know the anti if campaign folks are chocking on their corn flakes with that if statement, but in this instance, its use is practical because there is only ever two states. On/Off. I am not going to go overboard creating strategy / state pattern to solve this problem.

I also decided to keep this javascript inline in the page instead of creating it within a separate js file, or even to create a typescript file.

Some Gotchas with the HelpPage.

If you are using the C# XML comments, you have like me, added in a returns node.
On the Get method of the PolicyController, I have this comment


<returns>Returns a list of <see cref="Policy" /> objects</returns>

Unfortunately, with the HelpPage, out of the box, what is displayed is “Returns a list of objects” The node element for cref has been ignored.
In Part 3 of HelpPage customizations, Step 2 Modifying the XmlDocumentationProvider of Adding additional information to the HelpPageApiModel, has this line within method GetResponseDocumentation

return returnsNode.Value.Trim();

And it is this line that loses the cref link.

If you open up the XML file that is produced, what is included is the following


<returns>Returns a list of <see cref="T:MotorDB.Core.Models.Policy" /> objects</returns>

As first step workaround I produced this extension method to strip out the value for cref, and include this value in the xml

public static class StringExtensions
{
	public static string IncludeSeeNodeInReturn(this string returnDescription)
	{
		//InnerXml Returns a list of  objects
		var startOfseeNode = returnDescription.IndexOf("<see");

		if (startOfseeNode < 0) 			return returnDescription; 		var endOfseeNode = returnDescription.IndexOf("/>", startOfseeNode);

		var crefData = returnDescription.Substring(startOfseeNode + 11,
		    returnDescription.Substring(startOfseeNode + 11).IndexOf("/>") - 2).Split('.');
		var crefObject = crefData.Last();
		var result = String.Format("{0}{1}{2}",
		    returnDescription.Substring(0, startOfseeNode), crefObject, returnDescription.Substring(endOfseeNode + 2));
		return result;
	}
}

It is not pretty but it gets the desired result.

With this method, my return statement from GetResponseDocumentation is:
return returnsNode.InnerXml.IncludeSeeNodeInReturn();

To include the default Response object, need to add a line to the Register method of HelpPageConfig class

config.SetActualRequestType(typeof(Core.Models.Policy), “Policy”, “Get”);

I have been informed that this line has been superseeded with attributes in version 5, but I have not looked into this properly, and this will be updated soon.

If you run the app now, and view the documentation, after expanding the policy api to view all the method calls, drilling down to the Get method, everything is displaying.

Still not 100% happy with what is being produced, but right now, I am going to leave that for another post. While the samples are being displayed, I do want to display a table with the properties for each object being returned. This is why I wanted to read multiple XML files so that I could get reference to the models.

The customizations from Microsoft are from the following three pages

http://aspnet.uservoice.com/forums/147201-asp-net-web-api/suggestions/3745813-support-cref-attribute-in-asp-net-web-api-help-pag

http://blogs.msdn.com/b/yaohuang1/archive/2012/10/13/asp-net-web-api-help-page-part-2-providing-custom-samples-on-the-help-page.aspx

http://blogs.msdn.com/b/yaohuang1/archive/2012/12/10/asp-net-web-api-help-page-part-3-advanced-help-page-customizations.aspx

As always the latest code can be found on GitHub

Advertisements

My musing about anything and everything

Tagged with: , ,
Posted in Help Page, MotorDB, Other, WebAPI
2 comments on “Using Microsoft ASP.NET Web API 2 Help Page – Part 2
  1. […] my previous post, Using Microsoft ASP.NET Web API 2 Help Page – Part 2 I mentioned that in MVC5, the HelpPage API, you no longer to specify the response type as part of […]

  2. Chris says:

    Great article! Exactly what I was needing! Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 12 other followers

%d bloggers like this: