Wednesday, December 17, 2008

MOSS - Programatically ordering Navigation

From the title of this post most people would say - When would you ever need to do this?

Well I recently worked on a MOSS internet site which had a setup which split the Authoring and the Live site into two completely separate Farms. This provided quite a few challenges including coding a custom Import Export functionality.

The Import and Export functionality worked really well Except one overlooked issue - Navigation Ordering.

What if the Author reordered the navigation nodes in the Authoring Site how will they get reordered on the Live site? Well if the Site was a WSS site and it used the TopNavigationBar and the QuickLaunch Sharepoint Navigation objects then it would be pretty straight forward.... But Moss comes with its own Navigation/SiteMap Providers which are -
GlobalNavSiteMapProvider, CurrentNavSiteMapProvider and the CombinedNavSiteMapProvider.

The CombinedNavSiteMapProvider is (as the name suggests) a combination of the Global and CurrentNavSiteMapProviders. The GlobalNavSiteMapProvider is linked to the TopNavigationBar and any reordering of the GlobalNavSiteMapProvider will result in the order being reflected in the TopNavigationBar object (Web.Navigation.TopNavigationBar). The CurrentNavSiteMapProvider is linked to the QuickLaunch and any reordering here will result in the order being reflected in the QuickLaunch object(Web.Navigation.QuickLaunch).

Now here is where the whole process(for programmatically syncing navigation) gets a little tougher. If your Web hasn't been reordered then simply running the code below will fail because the nodes won't of been reflected in the Web.Navigation object yet.. So the SPNavigationNodeCollection.Count will be zero!

SPSite Site = new SPSite("http://mysite/");

SPWeb Web = Site.RootWeb;

SPNavigationNodeCollection SPNavNodes = Web.Navigation.QuickLaunch;



foreach (SPNavigationNode ThisSPNavNode in SPNavNodes)

{

ThisSPNavNode.MoveToFirst(SPNavNodes);

}

So how does the SPNavigationNodeCollection (QuickLaunch or TopNavigationBar) get populated? The answer is to go to the Site Navigation Setings page and change the order then click ok. And as if by magic the same code above will produce nodes which can be reordered (and this reordering will reflect in the Site Navigation Setings page).

So our Problem was that because the Items on the Live server hadn't been reordered(No Authoring is done on this server) then the SPNavigationNodeCollection (that we needed for us to reorder the navigation) wasn't populated.

The way we wanted the navigation sync to work was by producing an XML file (From the Authoring server like the one below) and we wanted to use it to reorder the Live servers Navigation nodes.

<Navigation>



<Web Name="" ServerRelativeURL="/">

<NavItem Title="SubSite 1" Url="/SubSite1" />

<NavItem Title="SubSite 2" Url="/SubSite2" />

<NavItem Title="SubSite 3" Url="/SubSite3" />

</Web>

<Web Name="SubSite1" ServerRelativeURL="/SubSite1">

<NavItem Title="Child 1 of SubSite 1" Url="/SubSite1/Child1ofSubSite1" />

<NavItem Title="Child 2 of SubSite 1" Url="/SubSite1/Child2ofSubSite1" />

<NavItem Title="Child 3 of SubSite 1" Url="/SubSite1/Child3ofSubSite1" />

</Web>

<Web Name="SubSite2" ServerRelativeURL="/SubSite2">

<NavItem Title="Child 1 of SubSite 2" Url="/SubSite2/Child1ofSubSite2" />

<NavItem Title="Child 2 of SubSite 2" Url="/SubSite2/Child2ofSubSite2" />

<NavItem Title="Child 3 of SubSite 2" Url="/SubSite2/Child3ofSubSite2" />

</Web>

<Web Name="SubSite3" ServerRelativeURL="/SubSite3">

<NavItem Title="Child 1 of SubSite 3" Url="/SubSite3/Child1ofSubSite3" />

<NavItem Title="Child 2 of SubSite 3" Url="/SubSite3/Child2ofSubSite3" />

<NavItem Title="Child 3 of SubSite 3" Url="/SubSite3/Child3ofSubSite3" />

</Web>

<Navigation>

So we took the XML content and looped through each web node and from the bottom we added each NavItem to (in this case) the QuickLaunch SPNavigationNodeCollection if it didn't already exist. We then moved that node to the top of the ordering and also Timestamped it so we could delete none timestamped nodes later on (Old nodes which no longer exist). It also seemed (for reasons beyond me) that you need to add this node with a NodeTypes of Page (even when the NodesType is really a web!). If you don't add the node as NodeTypes.Page the synching with the relevant "NavSiteMapProvider" fails.

The code below is how we went about it


XmlDocument xmlDoc = LoadXML("OurNavigation.xml");

XmlNode XmlNodes = xmlDoc.SelectSingleNode("//Navigation");



if (XmlNodes != null)

{

foreach (XmlNode ThisXmlNode in XmlNodes)

{

SPWeb NavWeb = Site.OpenWeb(ThisXmlNode.Attributes["ServerRelativeURL"].Value.ToString());

if (NavWeb != null)

{

PublishingWeb pubWeb = PublishingWeb.GetPublishingWeb(NavWeb);

if (pubWeb != null)

{

NavWeb.AllowUnsafeUpdates = true;



SPNavigationNodeCollection SPNavNodes = NavWeb.Navigation.QuickLaunch;



//Imported navigation Nodes

XmlNodeList WebsNavNodes = ThisXmlNode.ChildNodes;



for (int i = (WebsNavNodes.Count - 1); i != -1; i--)

{

bool found = false;

foreach (SPNavigationNode ThisSPNavNode in SPNavNodes)

{



string ThisPageRelativeUrl = ThisSPNavNode.Url;

string ThisPageRelativeTitle = ThisSPNavNode.Title;

if (WebsNavNodes[i].Attributes["Url"].Value == ThisPageRelativeUrl && WebsNavNodes[i].Attributes["Title"].Value == ThisPageRelativeTitle)

{

ThisSPNavNode.Properties["TimeStamp"] = TimeStamp;

ThisSPNavNode.Properties["NodeType"] = "Page";

ThisSPNavNode.Update();

ThisSPNavNode.MoveToFirst(SPNavNodes);

found = true;

break;

}

}

if (!found)

{

SPNavigationNode SPNode = SPNavigationSiteMapNode.CreateSPNavigationNode(WebsNavNodes[i].Attributes["Title"].Value, WebsNavNodes[i].Attributes["Url"].Value, NodeTypes.Page, NavWeb.Navigation.QuickLaunch);

SPNode.Properties["TimeStamp"] = TimeStamp;

SPNode.Update();

}

}

}

else

{

//Log.Write("Navigation Sync - The publishing web is null. The Navigation Web's URL is - " + NavWeb.Url + ". Please investigate.");

}

NavWeb.Dispose();

}

else

{

//Log.Write("Navigation Sync - Navigation Web could not be opened! The URL - " + ThisXmlNode.Attributes["ServerRelativeURL"].Value.ToString() + " May be incorrect or the Web might have had problems when it was imported. Please investigate.");

}

}





//****Delete Non timestamped Navigation Items****

foreach (SPWeb ThisWeb in Site.AllWebs)

{

SPNavigationNodeCollection NavNodes = ThisWeb.Navigation.QuickLaunch;

for (int i = (NavNodes.Count - 1); i != -1; i--)

{

SPNavigationNode ThisNode = NavNodes[i];

if (ThisNode != null)

{

string ThisNodeTimeStamp = "";

if (ThisNode.Properties["TimeStamp"] != null)

{

ThisNodeTimeStamp = ThisNode.Properties["TimeStamp"].ToString();

}

else

{

ThisNodeTimeStamp = "NoTimestamp";

}



if (ThisNodeTimeStamp != TimeStamp)

{

//Delete The Nav Node as it isn't in the Navigation XML file

ThisNode.Delete();

}

}

}



ThisWeb.Update();

ThisWeb.Dispose();

}

}

else

{

Log.Write("Navigation Sync - Problem with the XML Navigation Doc");

}

If you are interested in exporting your Navigation to an XML file similar to the one above then you can use the code below. Because we needed to use the "NavSiteMapProviders" to get the Navigation (We couldn't use the QuickLaunch or the TopNavigationBar as we couldn't be sure they were populated) then we had to use some sort of page so we were working within the SPContext. The way I got around this was to code a server control (code below) and then insert this in to a page which I then savedc to the _layouts directory (C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS).




protected override void OnLoad(EventArgs e)
{
try
{
SPSite Site = SPContext.Current.Site;
StringBuilder NavDoc = new StringBuilder();
NavDoc.Append("<Navigation>");
PortalSiteMapProvider _provider;

SiteMapProvider siteMapProvider = SiteMap.Providers["CurrentNavSiteMapProvider"];

_provider = siteMapProvider as PortalSiteMapProvider;
_provider.IncludePages = PortalSiteMapProvider.IncludeOption.Always;
_provider.IncludeSubSites = PortalSiteMapProvider.IncludeOption.Always;
_provider.IncludeHeadings = false;

SPWebCollection Webs = Site.AllWebs;

foreach (SPWeb Web in Webs)
{
NavDoc.Append("<Web Name=\"" + Web.Name + "\" ServerRelativeURL=\"" + Web.ServerRelativeUrl + "\">");
SiteMapNode siteNode = _provider.FindSiteMapNode(Web.ServerRelativeUrl);
SiteMapNodeCollection nodes = _provider.GetChildNodes(siteNode);

foreach (SiteMapNode node in nodes)
{
NavDoc.Append("<NavItem Title=\"" + node.Title + "\" Url=\"" + node.Key + "\" />");
}
NavDoc.Append("</Web>");
Web.Dispose();

}

NavDoc.Append("</Navigation>");

XmlDocument ThisXMLdoc = new XmlDocument();

ThisXMLdoc.InnerXml = NavDoc.ToString();

//Below I save the file with the Helper class
ImportExport.XmlHelper.SaveToFile(ThisXMLdoc, ImportExportFilePath + ImportExportFileName + ".xml");
netcreds = XmlHelper.GetFtpCredentials(RootWeb);
MoveFileToFtp();
}
catch(Exception ex)
{
//Log.Write("Error Exporting the Navigation Nodes. Exception - " + ex.Message.ToString());
}
}

To call the page I used the code below from a job in the TimerJobSchedule list(See previous post - http://my-sharepointer.blogspot.com/2008/12/sharepoint-timerjobscheduler-easier-way.html)


string NavUrl = SiteUrl + "/_layouts/mrwaNavigationExport.aspx";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(NavUrl);
request.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Delegation;
request.UseDefaultCredentials = true;
request.PreAuthenticate = true;
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

Hope it helps
Damian

1 comment:

  1. Thanks for this..

    I have implemented something similar, exporting webs and all their contents (including navigation settings) into an XML file, and then allowing me to import it on another server.

    The navigation ordering was the most painful part of the whole thing. I eventually got it to work - turns out my problem was that I was calling node.MoveToLast() before calling node.Update(), which didn't seem to be doing anything. Thanks to your post, I tried putting the Update() before the MoveToLast(), and it all works now.

    Just one thing - in my exported XML I include a lot more properties of the web and the navigation node, including all the web's navigation settings. I have discovered that if you can replicate all the web's navigation settings, and all the properties of the navigation nodes, it's possible to use the web.Navigation.QuickLaunch and web.Navigation.TopNavigationBar (or the equivalent publishingweb.GlobalNavigationNodes and publishingweb.CurrentNavigationNodes).

    The main property to note in this situation is the web.Navigation.UseShared property - which basically indicates whether or not your QuickLaunch or TopNavigationBar's will be "populated". The interesting thing is, that if, for the web, "Show Subsites" or "Show Pages" is enabled, that the subsites and/or pages won't be in the collections, even though they appear in the nav. To re-order these items, I have discovered that you can add items into the collections with NodeType of "Page" or "Area". You can then re-order these nodes as necessary, and they will "override" the "inherited" ones from "show subsites" or "show pages".

    I hope this helps someone because the navigation ordering battle took me 3 days to win.

    -Adrian

    ReplyDelete