>
Blog
Book
Portfolio
Search

2/27/2009

12001 Views // 0 Comments // Not Rated

Customizing SharePoint (WSS and MOSS) Navigation From Scratch

My Lord, it's been a while! 2009 has gotten off to a tumultuous start for me: water damage in my condo, two home improvement projects, two failed mortgage refinances, two internal projects at work, and two public facing SharePoint web sites. With all that going on, when could I have had to philosophize about SharePoint?

When it comes to public facing SharePoint, we get into all kinds of things beyond what you need to do for a standard intranet. These include cross-browser support, an unwavering standard for look & feel, and more AJAX than I care to think about.

In the two aforementioned SharePoint projects, both called for heavily customized navigation. And I don't just mean changing the order of the top bar's links. We're talking gradient images, precise CSS-based layout, and, of course, the ability to traverse sites, sub sites, and pages.

The major problem is (and this will be no surprise) that SharePoint comes up with some...interesting...HTML for it's navigation menus. Basically, the default is that each menu item is a table nested in an outer table. You do have the opportunity to set some CSS class names and set some properties on the data sources, but, especially when we're talking about custom branding or public facing sites, this isn't generally going to be enough to implement the client's designs.

I've come up with a really quick way to hook into the SharePoint navigation subsystem and output your HTML from scratch, allowing for the most flexibility in your customization requirements. But first, just a bit of high-ish level overview of how navigation works in SharePoint.

It all starts in the web.config, where there are a number of providers defined in the system.net section. They look like this:

Code Listing 1

  1. <siteMap defaultProvider="CurrentNavSiteMapProvider" enabled="true">
  2. <providers>
  3. <add name="SPNavigationProvider" type="Microsoft.SharePoint.Navigation" ... />
  4. <add name="SPSiteMapProvider" type="Microsoft.SharePoint.Navigation" ... />
  5. <add name="SPContentMapProvider" type="Microsoft.SharePoint.Navigation" .... />
  6. <add name="SPXmlContentMapProvider" siteMapFile="_app_bin/layouts.sitemap" ... />
  7. <add name="AdministrationQuickLaunchProvider" description="QuickLaunch" ... />
  8. <add name="SharedServicesQuickLaunchProvider" description="QuickLaunch" ... />
  9. <add name="GlobalNavSiteMapProvider" description="CMS provider for Global" ... />
  10. <add name="CombinedNavSiteMapProvider" description="CMS provider for Combined" ... />
  11. <add name="CurrentNavSiteMapProvider" description="CMS provider for Current" ... />
  12. <add name="CurrentNavSiteMapProviderNoEncode" description="CMS provider" ... />
  13. <add name="SiteDirectoryCategoryProvider" description="Site Directory" ... />
  14. <add name="MySiteMapProvider" description="MySite provider" ... />
  15. <add name="MySiteLeftNavProvider" description="MySite Left Nav provider" ... />
  16. <add name="UsagePagesSiteMapProvider" description="Provider for navigation" ... />
  17. </providers>
  18. </siteMap>

If you are using WSS only, then the navigation, breadcrumb, and menu controls defined in default.master are directly wired up to the providers. In MOSS, there is another layer of indirection: data sources. In these master pages (and, with MOSS, layout pages as well) you see these navigation controls wired to a PortalSiteMapDataSource, which is then itself connected to one of the aforementioned providers.

This provides the interface to tie into all of the addition navigation customization you can do in MOSS. If you really want to dig to the deepest level here and completely circumvent all menu rendering, then you can directly code against static methods in PortalSiteMapDataSource, as well as several other goodies in the Microsoft.SharePoint.Publishing.Navigation namespace. You can also tie into the publishing infrastructure for additional tweeking. Here are some examples...

Directly iterate the current site's child navigation nodes, completely from scratch:

Code Listing 2

  1. for (int n = 0; n < PortalSiteMapProvider.CurrentNavSiteMapProvider.CurrentNode.ChildNodes.Count; n++)
  2. {
  3. ...
  4. }

Code against the custom links and ordering set up in a MOSS site's navigation settings page:

Code Listing 3

  1. using (SPSite site = new SPSite(SPContext.Current.Site.ID))
  2. {
  3. using (SPWeb web = site.OpenWeb("path to web"))
  4. {
  5. PublishingWeb pub = PublishingWeb.GetPublishingWeb(web);
  6. for (int n = 0; n < pub.CurrentNavigationNodes.Count; n++)
  7. {
  8. ...
  9. }
  10. }
  11. }

Finally, let's go back to the quick-and-easy way when assuming that the current navigation is correct, and we just want to control the HTML more explicitly. First, create a new user control designed to live directly on the master page. To do this, follow these steps:

  • Create a custom master page with code behind.  The best way to start is to copy-and-paste the HTML from an existing one.  Check out this link for some tips and downloads.  
  • In the markup, remove the "AutoEventWireup" attribute from the "<%@ Master %>" element.  In this same line, change the "Inherits" element to use the fully qualified name of the assembly.  This makes code-behind work. 
  • Edit the master page to fit your requirements. 
  • Create a normal ASP.NET user control to build your menu.  Decide between CSS and table layouts, and create / update any custom CSS files. 
  • In the master page, register your user control at the top of the file using the following standard convention: <%@ Register TagPrefix="<name of web app or whatever>" TagName="<friendly name of user control>" src="~/_controltemplates/<custom folder, if used>/<name of user control>.ascx" %> 
  • Then, place your user control as you would normally in the master page, preferably in one of the navigation content place holders, so your layout pages can override it if necessary. 
  • Upload (and check in, and publish) any custom CSS files to the style library (generally found at "http://<server name>/Style%20Library/Forms/AllItems.aspx"). 
  • Upload (and check in, and publish, and approve, and offer as a sacrifice to God - gotta love the MOSS publishing features!) your master page to the Master Page Gallery (which, by default, is "http://<server name>/_catalogs/masterpage/Forms/AllItems.aspx"). 
  • Deploy the .ASCX file to the control templates folder on the server (which, by default, is "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\CONTROLTEMPLATES").  It's a best practice to put a custom folder under this path for each web application, to achieve maximum separation. 
  • Deploy the .DLLs to the GAC and do an IISRESET. 
  • Set your site to use your new master page (in "http://<server name>/_Layouts/ChangeSiteMasterPage.aspx"). 

Now that everything's wired up, we can finally get to this code! My technique is to pass the data source from the master page directly the user control. The control then creates a temporary menu control, binds it to the data source, then iterates it (and it's children) to build whatever UI is deemed best-suited for the look & feel.

Here's the shell of the user control, with some practical customization ideas / examples:

Code Listing 4

  1. public void RenderMenu(object dataSource)
  2. {
  3. //initialization - use table layout
  4. TableRow tr = new TableRow();
  5. this.tblGlobalNav.Rows.Clear(); //declared in user control's markup
  6. Menu m = new Menu();
  7. m.DataSource = dataSource;
  8. m.DataBind();
  9. //build a cell for each item
  10. foreach (MenuItem mi in m.Items)
  11. {
  12. //filter out loose pages (only show sub sites)
  13. if (!mi.DataPath.EndsWith(".aspx"))
  14. {
  15. //menu item
  16. this.AddCell(mi, tr);
  17. //if we want "flyout" sub menus, we can recurse sub webs
  18. foreach (MenuItem child in mi.ChildItems)
  19. {
  20. ...
  21. }
  22. }
  23. }
  24. //add to table
  25. this.tblGlobalNav.Rows.Add(tr);
  26. }
  27. private void AddCell(MenuItem mi, TableRow tr)
  28. {
  29. //initialization
  30. TableCell tc = new TableCell();
  31. HyperLink hl = new HyperLink();
  32. //css
  33. tc.CssClass = "CellNav";
  34. hl.CssClass = "LinkNav";
  35. //link
  36. hl.Text = mi.Text;
  37. hl.NavigateUrl = mi.NavigateUrl;
  38. tc.Controls.Add(hl);
  39. tr.Cells.Add(tc);
  40. }

And what the mater page's code behind will look like:

Code Listing 5

  1. protected void Page_Load(object sender, EventArgs e)
  2. {
  3. //global nav
  4. this.gnGlobalNavigation.RenderMenu(this.mainMenuDS);
  5. //top nav
  6. this.tnTopNavigation.RenderMenu(this.siteMenuDS);
  7. }

One last thing: what about WSS? All this master page stuff and publishing stuff and custom navigation stuff (and basically cool stuff) is MOSS-only. That's not to say that there's nothing we can do in WSS to customize these aspects of your portal.

You can hack up default.master (on the file system at "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\12\TEMPLATE\GLOBAL\default.master") to add your own functionality beyond what you can do with themes and web parts. Also, the trusty SPWeb object does have some hooks into WSS navigation. Check out the Navigation property of an instantiated web, and you'll see access to the top, global, and quick launch nav, and more!

That's it! Have fun navigating!

3 Tags

No Files

No Thoughts

Your Thoughts?

You need to login with Twitter to share a Thought on this post.


Loading...