Not long ago I started working on changing the look and feel of the default WebService documentation that is generated by asp.net. While I demonstrated how to replace the default page template with a custom one I "glossed" over the details of what you can do with it.
By using a custom Sitemap Provider you can create a dynamic Map of all of your WebServices methods. Please note I am not out to explain SiteMaps in this article. They list the pages in a site for use by some asp.net controls, like the menu control. That over simplified short description of a SiteMap is about as deep as I will be getting into the the general topic of SiteMaps.
Here is a quick peek at my sample web.config. The first SiteMap, "Help", is a simple "out of the box" XmlSiteMapProvider. We will take a look at the associated Help.sitemap file a little later on.
<siteMap> <providers> <add name="Help" type="System.Web.XmlSiteMapProvider" siteMapFile="help.sitemap"/> <add name="ObjectHelpDesk.WebServices.help_desk" type="SiteMapProviders.WebServiceSiteMapProvider,SiteMapProviders" webServiceType="ObjectHelpDesk.WebServices.help_desk,ObjectHelpDesk" path="~/help/help-desk.asmx"/> </providers> </siteMap>
The second SiteMap is a little more interesting. The type is our custom SiteMapProvider class. We give it 2 pieces of information, one is the path to the asmx file; the other is the class type of the WebService. I am using the same WebService I used in the pervious "WebService Documentation: Using a Custom Template" post. I moved the WebService code behind into an assembly; this makes locating the class from the web.config easy. Speaking of the code...
using System.Text; using System.Web; using System.Configuration; using System.Collections.Specialized; using System.Web.Services; using System.IO; using System.Web.Services.Description; using System.Collections; using System.Reflection; namespace SiteMapProviders { public class WebServiceSiteMapProvider : StaticSiteMapProvider { Type _webServiceType = null; SiteMapNode _root = null; String[] roles = { "" }; NameValueCollection _attributes = null; string path = ""; public override void Initialize(string name, NameValueCollection attributes) { base.Initialize(name, attributes); string typeName = attributes["webServiceType"]; if (String.IsNullOrEmpty(typeName)) throw new ConfigurationErrorsException("webServiceType is not configured for " + name); path = attributes["path"]; if (String.IsNullOrEmpty(path)) throw new ConfigurationErrorsException("path is not configured for " + name); attributes.Remove("path"); attributes.Remove("webServiceType"); if (attributes["roles"] != null) { roles = attributes["roles"].Split(','); attributes.Remove("roles"); } _attributes = attributes; _webServiceType = Type.GetType(typeName, false); if (_webServiceType == null) { throw new ConfigurationErrorsException(string.Format("webServiceType '{0}' was not found for {1}", typeName, name)); } } protected override SiteMapNode GetRootNodeCore() { if (_root == null) BuildSiteMap(); return _root; } public override SiteMapNode BuildSiteMap() { if (_root == null) //verify it is null { lock (new object())//create a lock { if (_root == null)//make sure no one else has already started it { //build the map here _root = new SiteMapNode(this, _webServiceType.ToString(), path, _webServiceType.Name, _webServiceType.Name + " documentation"); _root.Roles = roles; foreach (MethodInfo method in _webServiceType.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)) { if (method.DeclaringType!=_webServiceType) continue; string url = ""; string title = ""; string desc = ""; object[] customAttributes = method.GetCustomAttributes(typeof(WebMethodAttribute), true); if (customAttributes.Count() > 0) { foreach (WebMethodAttribute attribute in customAttributes) { if (!String.IsNullOrEmpty(attribute.Description)) desc = attribute.Description; if (!String.IsNullOrEmpty(attribute.MessageName)) title = attribute.MessageName; title = method.Name; }//foreach (WebMethodAttribute attribute in customAttributes) url = string.Format("{0}?op={1}", path, title); CreateNode(_root, title, desc, url, roles, _attributes, null, ""); }//if (customAttributes.Count() > 0) }//foreach (MethodInfo method }// if (_root == null) }//lock (new object()) }// if (_root == null) return _root; } private SiteMapNode CreateNode(SiteMapNode parent, string title, string desc, string url, string[] roles, NameValueCollection attributes, NameValueCollection explicitResourceKeys, string implicitResourceKey) { SiteMapNode node = new SiteMapNode(this, url, url, title, desc, roles, attributes, explicitResourceKeys, implicitResourceKey); AddNode(node, parent); return node; } } }
The Initialize method sets up the basics of the SiteMap. It grabs the path to the asmx and gets the Type of the WebService. It also grabs a comma separated list of role names and populates a roles variable for use later. This can be used with security trimming depending on your sites configuration (It is a SiteMap thing and outside of the scope of this post).
The next method is GetRootNodeCore. All it does is check to see if the _root variable is set and call BuildSiteMap if it is null.
I am going to skip BuildSiteMap for a moment; down at the bottom we have a simple private method CreateNode. CreateNode takes all of the information about a SiteMapNode, creates it and adds it to it's parent node.
And that brings us back to the BuildSiteMap method. If we check if the _root node has been created. If it has we do not need to do anything. Next we create a lock and check the _root again. Why 3 checks? A web application (or site) is multi threaded. Seven, One hundred, or a thousand people might all hit it at once. Because providers, including SiteMapProviders, are created once and last the lifetime of the application having several people create the _root node and build the entire site is bad voodoo. So by checking before we call BuildSiteMap we avoid hoping around code. By checking before the lock we are basically making sure that we still need to do it and the check after the lock ensures we did not already start building the SiteMap on a different thread while we were trying to lock people out.
So now that we have locked everyone out and are building the site map we need to create a Root Node (_root). We create the node setting the path to the asmx file path and the title to the WebService type name. We also slap out roles collection into the roles property of the node.
Next we do a little reflection. Why does everyone cringe when I say reflection? We loop through all the methods in the class type we got from the web.config. Next we check to see if it has a WebMethodAttribute set on it. Keep in mind [WebMethodAttribute()] and [WebMethod()] are the same thing. We grab the description from the attribute and the title from the method name. Notice there is a check setting the title to the "MessageName" of the WebMethodAttribute that is completely unused. In fact we step on it when we set the title to the method name. If you set the message name, you might want to put an "else" in between the two title sets. I have not had to use a message name yet and therefore have not tested if it is needed in the title or not. Next I create the SiteMapNodes URL from the path of the asmx and the method name (or title). Finally, I call CreateNode passing all of the important information.
Now if you have 15 different WebServices, you could add 15 SiteMap data sources to your page with a bunch of Menus; but there is an easier way. Let's look at the help.sitemap file from our XmlSiteMapProvider.
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0" > <siteMapNode url="~/help/default.aspx" title="Help" description=""> <siteMapNode url="" title="Web Services" description=""> <siteMapNode provider="ObjectHelpDesk.WebServices.help_desk"/> </siteMapNode> </siteMapNode> </siteMap>
The only node in this SiteMap that is relevant to us is in the middle; in fact it is missing a URL and title attribute. The only thing it has is a provider attribute. The provider attribute is the name of the provider to include at that position in the SiteMap. Now you can include all 15 different WebService SiteMap's by adding them to the web.config and creating a node in the help.sitemap.
Create a page with a menu control bound to the "help" SiteMap provider. Open the page.
Add 5 new WebMethods to a WebService, recompile and refresh the page. The new methods are already in the menu without any extra work.
Now you can add a nice menu to a custom template or, even better, a master page that you've set the template to use.
Happy coding