Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

NavBars - If only it was that simple!

5.00/5 (1 vote)
15 Sep 2009CPOL6 min read 21.9K  
Programmatically create NavBarLink objects so that they can be explicitly set to internal, therefore benefiting from security trimming and other URL management functions provided by SharePoint.

Introduction

Packaging and deployment is often a pain when it comes to SharePoint development. Don’t get me wrong, the infrastructure provided by Microsoft is very powerful, and flexible enough to accommodate practically anything, but sometimes, it doesn’t quite work the way you think it should.

One of the key elements of any web site is navigation. More often than not, there’s at least one menu bar containing links to other pages within the site. This is one of the areas where SharePoint can be very helpful. Whenever you create a page or any other content, more often than not, you’re given the option to automatically add a link to the site navigation. Once you’ve added links, you can rearrange them, subcategorise them, practically do what you like. Great you say, Bill’s boys have done a grand job there.

However, once you’ve built your site and got your navigation looking the way you want it, what happens when the time comes to package it all up as a site definition? At first glance, you may think: no problem, I’ll just add the links that I need in the NavBars section of onet.xml. There’s a whole page on MSDN that explains how this can be done. What could be simpler?

Unfortunately, when creating a site definition, using links in the NavBars section won’t work as expected because a link that’s created by a NavBarLink element is considered an external link. As a result, security trimming and alternate access mappings won’t work.

In order to resolve this problem, it’s necessary to programmatically create the NavBarLink objects so that they can be explicitly set to internal, therefore benefiting from security trimming and other URL management functions provided by SharePoint.

Again, unfortunately, it’s not as simple as creating a feature receiver to programmatically create the links. Onet.xml is processed in a particular order, and often the pages that you’re linking to will be created by a Modules > Module > File structure, similar to this one:

XML
<Modules>
    <Module Name="MyFiles" Url="$Resources:cmscore,List_Pages_UrlName;" Path="">
      <File Url="Default.aspx" Type="GhostableInLibrary" NavBarHome="TRUE" >
        <Property Name="Title" Value="Test File" />
        <Property Name="PublishingPageLayout" 
           Value="~SiteCollection/_catalogs/masterpage/TestTemplate.aspx, 
                  ~SiteCollection/_catalogs/masterpage/TestTemplate.aspx" />
        <Property Name="ContentType" Value="Test Page" />

Onet.xml is processed in the following order (thanks to Martin Hatch for the info):

  1. <SiteFeatures> in onet.xml
  2. Stapled Site Features (stapled using FeatureSiteTemplateAssociation)
  3. <WebFeature> in onet.xml
  4. Stapled Web Features (using FeatureSiteTemplateAssociation)
  5. <Lists> in onet.xml
  6. <Modules> in onet.xml

Since modules are processed last, when the code in our feature receiver tries to access our custom pages, an error will occur since they don’t exist yet! (Note: No error occurs when using NavBarLink elements because the links are considered external; SharePoint doesn’t verify that the content referred to actually exists.)

One possible way to resolve this problem is to take the following approach:

Step 1 – Move all pages and other content that is to be linked from the menu into a new feature

I often create a feature called MyProjectName_ContentPages for this purpose, the feature definition (feature.xml) refers to a ElementManifest file that contains the <Module> nodes that were previously in onet.xml.

Feature.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="6a5230f3-1b4c-4f8f-81b8-eef915bfdc56"
          Title="MyProject_ContentPages"
          Description="Description for MyProject_ContentPages"
          Version="12.0.0.0"
          Hidden="TRUE"
          Scope="Web"
          DefaultResourceFile="core"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="elements.xml"/>    
  </ElementManifests>
</Feature>

elements.xml

XML
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="MyModule" Url="$Resources:cmscore,List_Pages_UrlName;" Path="Pages">
    <File Url="MyFile1.aspx" Type="GhostableInLibrary" >
      <Property Name="Title" Value="Applicant Search" />
      <Property Name="PublishingPageLayout" 
         Value="~SiteCollection/_catalogs/masterpage/MyTemplate.aspx, 
                ~SiteCollection/_catalogs/masterpage/MyTemplate.aspx" />
      <Property Name="ContentType" Value="CBH Page" />
     ......
    </File>
    <File Url="MyFile2.aspx" Type="GhostableInLibrary" >
    .......
    </File>
  </Module>
  <Module Name="AnotherModule" Url="$Resources:cmscore,List_Pages_UrlName;" Path="Pages">
    ......
  </Module>
</Elements>

Step 2 – Configure default navigation settings

By default, SharePoint will automatically add links for each page that you add to the site during creation. Since we’re customizing the menu structure, it’s likely that we don’t want this behaviour (or we’d be adding duplicate links). We can disable the default behaviour in Onet.xml by making the following change (Here’s a link with more in depth info on how this works):

XML
<!-- Per-Web Portal Navigation Properties-->
<Feature ID="541F5F57-C847-4e16-B59A-B31E90E6F9EA">
 <Properties xmlns="http://schemas.microsoft.com/sharepoint/">
  <Property Key="InheritGlobalNavigation" Value="false"/>
  <Property Key="InheritCurrentNavigation" Value="false"/>
  <Property Key="IncludeSubSites" Value="false"/>
  <Property Key="IncludePages" Value="False"/> <!—Don’t show the pages, we'll add them next-->
 </Properties>
</Feature>

Step 3 – Create a feature to setup our Navigation

Now that we’ve disabled the default navigation options, our menu bars will be empty. To set them up the way we want them, we’ll need to create a new feature to hold our configuration and call our custom creation code. I like to use a feature called MyProject_Navigation for this.

Feature.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<Feature  Id="20018542-2728-42c1-a549-7d72d6ce2668"
          Title="MyProject_Navigation"
          Description="Provisions custom navigation"
          Version="12.0.0.0"
          Hidden="TRUE"
          Scope="Web"
          DefaultResourceFile="core"
          ReceiverAssembly="MyAssembly, Version=1.0.0.0, 
                            Culture=neutral, PublicKeyToken=212d6b0a9249ae1d"
          ReceiverClass="MyAssembly.NavigationReceiver"
          xmlns="http://schemas.microsoft.com/sharepoint/">
  <ElementManifests>
    <ElementManifest Location="elements.xml"/>
  </ElementManifests>
  <Properties>
    <Property Key="NavigationStructure" Value="NavBars.xml"/>
  </Properties>
</Feature>

This XML is pretty much the standard template that you’d get if using WSPBuilder to add a new ‘Feature with Receiver’ item. The only addition is the NavigationStructure property.

Step 4 – Define custom navigation

You’ll notice that the NavigationStructure property has a value of NavBars.xml. As you’ve probably guessed, that’s the name of an additional file that’ll be deployed with the feature, and I’m sure it’ll come as no surprise to learn that it contains the navigation configuration!

Here’s an example of a NavBars.xml file:

XML
<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/sharepoint/">
  <NavBars>
   <NavBar Name="SharePoint Top Navbar" ID="1002">-->

One thing that you may notice about this file is that the syntax is similar to that used in onet.xml and that the root node is Project instead of the usual Element. In fact, the namespace definition xmlns=http://schemas.microsoft.com/sharepoint/ on the Project node signifies that we’re importing the CAML schema, so the syntax is the same as that used in onet.xml. The main reason for doing this is to provide IntelliSense functionality in Visual Studio, making it easier to create the files.

Another thing to note is the NavBar IDs. You can add nodes to any navigation bar using this feature. It’s just a case of adding a NavBar node with the correct ID. MSDN has a bit more information although the ID list is incomplete. Someday I’ll get round to  posting a list of all the codes that are in use.

Step 5 – Add code to programmatically create the menu structure

I’ve separated the implementation of the navigation parser out into a separate static class. I’m a big fan of test driven development, and having a separate class with a method that can be called by the test script makes life much easier. (Of course, it is technically possible to create a test script to call the FeatureActivated method, but creating and populating an SPFeatureReceiverProperties object is no mean feat, and would require the services of a product such as TypeMock.)

In the feature receiver class file:

C#
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
    //Get the list of NavBars. Rather than introducing a new schema we've hijacked CAML's 
    //Project element so that we can make use of the NavBar's collection
    SPSite site = properties.Feature.Parent as SPSite;
    SPWeb currentWeb = null;
    if (site != null)
    {
        currentWeb = site.RootWeb;
    }
    else
    {
        currentWeb = properties.Feature.Parent as SPWeb;
    }

    if (currentWeb != null)
    {

        string navStructure = properties.Feature.Properties["NavigationStructure"].Value;
        string fileName = Path.Combine(properties.Definition.RootDirectory, navStructure);
        NavigationParser.Parse(fileName, currentWeb);
    }
}

In a new class file:

C#
static class NavigationParser
{
    public static bool Parse(string filename, SPWeb web)
    {
        try
        {
            XmlReader rdr = XmlReader.Create(filename, null);

            XmlDocument nav = new XmlDocument();

            nav.Load(rdr);

            XmlNamespaceManager mgr = new XmlNamespaceManager(nav.NameTable);
            mgr.AddNamespace(string.Empty, "http://schemas.microsoft.com/sharepoint/");
            mgr.AddNamespace("ns", "http://schemas.microsoft.com/sharepoint/");


            //Get all the navbars
            foreach (XmlNode navbar in nav.SelectNodes("//ns:NavBar", mgr))
            {
                int id = int.Parse(navbar.Attributes["ID"].Value);

                //Does the navbar exist?
                SPNavigationNode navBarNode = web.Navigation.GetNodeById(id);

                if (navBarNode != null)
                {
                    //Check out the links
                    foreach (XmlNode navbarlink in navbar.SelectNodes("ns:NavBarLink", mgr))
                    {
                        string url = navbarlink.Attributes["Url"].Value;
                        string title = navbarlink.Attributes["Name"].Value;

                        //Since the whole point of this is to add security
                        //trimmed nodes, set all to external=false
                        SPNavigationNode newNode = new SPNavigationNode(title, url, false);
                        navBarNode.Children.AddAsLast(newNode);

                    }

                    navBarNode.Update();
                }
                else
                {
                    //throw an error. Cant use that menu id and can't create a new one cos
                    //we don't know where to put it
                    PortalLog.LogString("NavBar:{0} references in file {1} " + 
                      "does not exist. No NavBarLink's have been added", id, filename);
                }

            }
            return true;
        }
        catch (Exception e)
        {
            PortalLog.LogString(e.ToString());
            return false;
        }
    }
}

All in all, the code is pretty straightforward. The most significant line is the creation of a new SPNavigationNode. you’ll notice that the last parameter is set to false. In effect, this tells SharePoint that the link is internal, and gives us all the functionality that we were missing.

Step 6 - (Almost forgot!) Add a reference to the new features to your onet.xml

Since we’ve moved the creation of pages and navigation into two separate features, we’ll need to add a reference to these features to the onet.xml so that they’re activated when the new site is created. Generally, I add these after the Portal Navigation Properties feature that we amended above, so the whole section will look like:

XML
<!-- Per-Web Portal Navigation Properties-->
<Feature ID="541F5F57-C847-4e16-B59A-B31E90E6F9EA">
  <Properties xmlns="http://schemas.microsoft.com/sharepoint/">
    <Property Key="InheritGlobalNavigation" Value="false"/>
    <Property Key="InheritCurrentNavigation" Value="false"/>
    <Property Key="IncludeSubSites" Value="false"/>
    <Property Key="IncludePages" Value="False"/>
       <!--Dont show the pages, we'll add them next-->
  </Properties>
</Feature>
<!--Content Pages, must be added before the navigation-->
<Feature ID="6a5230f3-1b4c-4f8f-81b8-eef915bfdc56"/>
<!--Custom Navigation-->
<Feature ID="20018542-2728-42c1-a549-7d72d6ce2668"/>

Conclusion

I hope this article sheds some light on how NavBars are deployed using the SharePoint feature infrastructure and maybe saves some time for those creating custom site definitions. I have a load of other similar utilities that I’ll post on CodePlex once I get the time to tidy them up and package them properly.

As always, comments/criticisms/complaints are welcome. If there’s anything in here that doesn’t make sense, please let me know and I’ll seek to clarify.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)