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:
<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):
<SiteFeatures>
in onet.xml - Stapled Site Features (stapled using
FeatureSiteTemplateAssociation
) <WebFeature>
in onet.xml - Stapled Web Features (using
FeatureSiteTemplateAssociation
) <Lists>
in onet.xml <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
="1.0"="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
="1.0"="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):
<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
="1.0"="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:
="1.0"="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
ID
s. 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:
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
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:
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/");
foreach (XmlNode navbar in nav.SelectNodes("//ns:NavBar", mgr))
{
int id = int.Parse(navbar.Attributes["ID"].Value);
SPNavigationNode navBarNode = web.Navigation.GetNodeById(id);
if (navBarNode != null)
{
foreach (XmlNode navbarlink in navbar.SelectNodes("ns:NavBarLink", mgr))
{
string url = navbarlink.Attributes["Url"].Value;
string title = navbarlink.Attributes["Name"].Value;
SPNavigationNode newNode = new SPNavigationNode(title, url, false);
navBarNode.Children.AddAsLast(newNode);
}
navBarNode.Update();
}
else
{
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:
<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"/>
</Properties>
</Feature>
<Feature ID="6a5230f3-1b4c-4f8f-81b8-eef915bfdc56"/>
<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.