Showing posts with label Navigation. Show all posts
Showing posts with label Navigation. Show all posts

Friday, September 7, 2018

How to delete navigation nodes with AuthoringLink types programmatically in Sharepoint

If you use structural navigation on your publishing Sharepoint site and navigation nodes are created as headings (NodeType = Heading), then it is quite straightforward to delete such navigation nodes programmatically. Here is how it can be done:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test"));
if (nodePage != null)
{
	nodePage.Delete();
}

In this example we delete navigation node with title “Test”. However if you will try to delete navigation nodes which were created as AuthoredLink* (see NodeTypes Enum):

  • AuthoredLink
  • AuthoredLinkPlain
  • AuthoredLinkToPage
  • AuthoredLinkToWeb

using the same code you will find that link is not get deleted. The workaround is to change NodeType property first to Heading and then delete the node:

var web = ...
var pweb = PublishingWeb.GetPublishingWeb(web);
var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
var nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test" &&
	(n.Properties != null && n.Properties["NodeType"] != null && n.Properties["NodeType"] is string &&
		(n.Properties["NodeType"] as string == "AuthoredLink" || (n.Properties["NodeType"] as string).StartsWith("AuthoredLink"))));

if (nodePage != null)
{
	nodePage.Properties["NodeType"] = "Heading";
	nodePage.Update();

	// reinitialize navigation nodes
	pweb = PublishingWeb.GetPublishingWeb(web);
	globalNavigation = pweb.Navigation.GlobalNavigationNodes;
	nodePage = globalNavigation.Cast<SPNavigationNode>().FirstOrDefault(n => (n.Title == "Test);
	if (nodePage != null)
	{
		nodePage.Delete();
	}
}

After that AuthoredLink navigation node will be successfully deleted.

Saturday, November 4, 2017

Problem with reusing taxonomy terms with navigation settings in different term sets in Sharepoint

Suppose that we have the following sub sites in the same Sharepoint Online site collection:

  • /en – publishing site
  • /content/en –authoring site

And we need to use the same managed metadata navigation on these publishing and authoring sub sites. In order to achieve that we need to create 2 navigation term sets (e.g. Navigation.en-US and NavigationContent.en-US) and configure navigation settings for the terms:

We can’t use same term set for 2 sub sites because Sharepoint allows to use navigation term set for single site only. As we need to create 2 term sets anyway we would like at least to reuse terms from 1st term set Navigation.en-US in 2nd term set NavigationContent.en-US like it is shown on the picture above (in order to have less maintenance work). And here we face with the problem: it seems like that during reusing navigation settings of the terms are not reused. I.e. if we create terms in 1st term set Navigation.en-US, then reuse them in 2nd term set NavigationContent.en-US and then configure navigation settings in Navigation.en-US – reused terms in NavigationContent.en-US won’t inherit changed navigation settings automatically as we would expect.

Workaround for this problem is quite simple: at first configure navigation settings in source term set (Navigation.en-US) and only after that reuse terms in 2nd term set (NavigationContent.en-US). In this case navigation settings will be inherited properly. But if you will change navigation settings after that in original terms from the source term set – they won’t be changed automatically in 2nd term set. I.e. you will need to change them in 2nd term set explicitly.

Wednesday, January 29, 2014

Fix problem with not saving navigation settings in Sharepoint or working with navigation on content database level

Some time when you work with publishing sites in Sharepoint you may encounter with strange problem: when you change navigation settings (e.g. hide some navigation node or rearrange them for global or current navigation) in Site settings > Navigation and click Ok, changes aren’t saved, i.e. when you will go to navigation settings page again you will see the same parameters as before. Interesting that problem exist only for navigation nodes in global and current navigation. Other navigation settings, like “Show sub sites” or “Show pages” are saved correctly. Another interesting moment is that if you will try to access navigation nodes programmatically (via PublishingWeb.Navigation.GlobalNavigationNodes or PublishingWeb.Navigation.CurrentNavigationNodes) they will be null even after you explicitly clicked Ok. If you worked with Sharepoint navigation a lot this fact will surprise you, because this is the known Sharepoint behavior when navigation nodes are populated only when you first time click Ok button in navigation settings page (see e.g. Change order of newly created publishing page or publishing site in navigation programmatically in Sharepoint).

This problem often happens when you export/import the site with stsadm utility (see Export: Stsadm operation and Import: Stsadm operation). This problem will lead us to the lower content database level and workarounds which will be described in this article will require manual change of content database. As you know MS doesn’t support such way of working with Sharepoint, so you may use it on your own risk. However information described here will help to understand internal Sharepoint mechanisms better, so I suggest to read it anyway.

Ok, if we will check the logs after we tried to save navigation settings, we may find the following errors:

Unable to retrieve TopNavigationBar SPNavigationNodeCollection from Web at: http://example.com. The SPNavigation store is likely corrupt.

or

Unable to retrieve QuickLaunch SPNavigationNodeCollection from Web at: http://example.com. The SPNavigation store is likely corrupt.

(first one about global navigation, second about current). These errors come from codebehind of AreaNavigationSettings.aspx applications layout page which is opened when you go to Site settings > Navigation: its AreaNavigationSettingsPage.OKButton_Click() method:

   1:  protected void OKButton_Click(object sender, EventArgs e)
   2:  {
   3:          ...
   4:          SPNavigationNodeCollection collection = null;
   5:          if (!publishingWeb.Navigation.InheritGlobal)
   6:          {
   7:              collection = publishingWeb.Navigation.GlobalNavigationNodes;
   8:              if (collection != null)
   9:              {
  10:                  PopulateNavigationNodeDictionary(dictionary, collection, 0, 1);
  11:              }
  12:              else
  13:              {
  14:                  ULS.SendTraceTag(0x38747331, ULSCat.msoulscat_CMS_Publishing,
  15:  ULSTraceLevel.Unexpected,
  16:  "Unable to retrieve TopNavigationBar SPNavigationNodeCollection from Web at: {0}." +
  17:  "The SPNavigation store is likely corrupt.",
  18:  new object[] { publishingWeb.Url });
  19:              }
  20:          ...
  21:          collection = publishingWeb.Navigation.CurrentNavigationNodes;
  22:          if (collection != null)
  23:          {
  24:              PopulateNavigationNodeDictionary(dictionary, collection, 0, 1);
  25:          }
  26:          else
  27:          {
  28:              ULS.SendTraceTag(0x38747332, ULSCat.msoulscat_CMS_Publishing,
  29:  ULSTraceLevel.Unexpected,
  30:  "Unable to retrieve QuickLaunch SPNavigationNodeCollection from Web at: {0}." +
  31:  "The SPNavigation store is likely corrupt.",
  32:  new object[] { publishingWeb.Url });
  33:          }
  34:          ...
  35:  }

As you can see it happens when PublishingWeb.Navigation.GlobalNavigationNodes or PublishingWeb.Navigation.CurrentNavigationNodes is null. If you will search for mentioned error messages, you will find that this problem may be caused by missing NavBar element in onet.xml:

   1:  <NavBars>
   2:      <NavBar Name="SharePoint Top Navbar" ID="1002">
   3:      </NavBar>
   4:   </NavBars>

If you exported and imported the site this information won’t help a lot because you don’t have access to onet.xml and in the source site navigation works properly.

Also you may find the following workaround for this problem: you need to execute the following SQL query in the content database:

   1:  INSERT INTO [NavNodes]
   2:  ([SiteId], [WebId], [Eid], [EidParent], [NumChildren], [RankChild],[ElementType],
   3:  [Url], [DocId], [Name], [DateLastModified], [NodeMetainfo], [NonNavPage],
   4:  [NavSequence], [ChildOfSequence])
   5:  SELECT DISTINCT SiteId, WebId ,1002 ,0 ,0 ,1 ,1 ,'', NULL, 'SharePoint Top Navbar',
   6:  getdate() ,NULL ,0 ,1 ,0
   7:  FROM NavNodes
   8:  WHERE WebId NOT IN (SELECT WebId FROM NavNodes WHERE Eid = 1002)

without explanation of what it does and why it will fix the problem. Also this method may fix only global navigation problem, while current navigation requires another query. I will try to fill in this space and will describe safer method which will fix both global and current navigation.

In order to continue we need to use some example. Let’s suppose that we have SPWeb with working navigation settings, which have several navigation nodes in global navigation and in current navigation:

Global navigation:

  • Site 1
  • Page 1

Current navigation:

  • Site 2
  • Page 2

Navigation nodes are stored in NavNodes table in content database, which has the following structure (it may be slightly different in different Sharepoint versions. This example uses schema from Sharepoint 2007 as most simple. But the same information may be used also in Sharepoint 2010 and 2013. They just have more columns in this table):

image

In order to query navigation nodes for particular web we need to know ID of parent SPSite and ID of SPWeb itself. They can be retrieved e.g. by PowerShell:

   1:  $w = Get-SPWeb http://example.com
   2:  $w.Site.ID
   3:  $w.ID

It will print the guids:

   1:  Guid
   2:  ----
   3:  {42514464-2EB2-4BB9-8A57-A5A6D80E3F67}
   4:   
   5:  Guid
   6:  ----
   7:  {92524380-DAE3-4CD6-B2C1-5A30C52ABC28}

(in our example SPSite.ID = {42514464-2EB2-4BB9-8A57-A5A6D80E3F67} and SPWeb.ID = {92524380-DAE3-4CD6-B2C1-5A30C52ABC28}). Now we can read the navigation items from NavNodes table:

   1:  select * from dbo.NavNodes
   2:  where WebId = '92524380-DAE3-4CD6-B2C1-5A30C52ABC28'

 

SiteId WebId Eid EidParent NumChildren RankChildren ElementType Url DocId Name DateLastModified NodeMetaInfo NonNavPage NavSequence ChildOfSequence
{Site ID}
{Web ID}
0 -1 1 0 0   NULL   {DateTime} NULL 0 0 0
{Site ID} {Web ID} 1025 0 2 0 1   NULL Quick launch {DateTime} NULL 1 1 0
{Site ID} {Web ID} 1002 0 2 1 1   NULL SharePoint Top Navbar {DateTime} NULL 0 1 0
{Site ID} {Web ID} 1000 0 0 2 0 NULL {Guid} default.aspx {DateTime} NULL 0 0 0
{Site ID} {Web ID} 2002 1002 0 0 0 NULL {Guid} Site 1 {DateTime} {binary} 0 0 1
{Site ID} {Web ID} 2003 1002 0 1 0 NULL {Guid} Page 1 {DateTime} {binary} 0 0 1
{Site ID} {Web ID} 2009 1025 0 0 0 NULL {Guid} Site 2 {DateTime} {binary} 0 0 1
{Site ID} {Web ID} 2017 1025 0 1 0 NULL {Guid} Page 2 {DateTime} {binary} 0 0 1

It is classical representation of hierarchical data in relational database table: there are 2 columns for creating parent-child relationship: Eid and EidParent, plus NumOfChildren which contains number of children for each node for improving performance I guess (in our example both global and local navigation contains 2 nodes). Root node has Eid = 0 and EidParent = –1. After that we have 2 special root nodes for current and global navigation: with Eid = 1025 and Eid = 1002 respectively. This is the key moment for understanding the workaround. These 2 special navigation nodes always should exist in NavNodes table for SPWeb (at least for publishing web). If they don’t exist, navigation won’t be saved like described in the beginning of the article. And this is what may happen when you export and import your site with stsadm. If you will try to get navigation nodes using the query above, you will get only root level node with Eid = 0 (and may be node for default.aspx with Eid = 1000), but without root nodes for global and current navigation with Eid = 1002 and Eid = 1025.

Now if you will get back to workarounds mentioned earlier you will see why it is important to have NavBar with ID = 1002 in onet.xml and why mentioned SQL insert works: it finds all WebId which don’t have node with Eid = 1002 and inserts it manually. However doing it for all SPWebs in one operation is not safe. First of all remember that any manual change in content database is not supported, and second – it may not be needed depending on web template used for web site. So it is better to minimize affected sites and make changes only for single problematic SPWeb. Also mentioned workarounds fix problem only for global navigation. Let’s address these problems and create more safer fix (if this word is relevant in case when we want to do manual changes in content database):

   1:  -- global navigation
   2:  INSERT INTO [NavNodes]
   3:  ([SiteId], [WebId], [Eid], [EidParent], [NumChildren], [RankChild],[ElementType],
   4:  [Url], [DocId], [Name], [DateLastModified], [NodeMetainfo], [NonNavPage],
   5:  [NavSequence], [ChildOfSequence])
   6:  values ('42514464-2EB2-4BB9-8A57-A5A6D80E3F67',
   7:  '92524380-DAE3-4CD6-B2C1-5A30C52ABC28' ,1002 ,0 ,0 ,1 ,1 ,'', NULL,
   8:  'SharePoint Top Navbar',getdate() ,NULL ,0 ,1 ,0)
   9:   
  10:  -- current navigation
  11:  INSERT INTO [NavNodes]
  12:  ([SiteId], [WebId], [Eid], [EidParent], [NumChildren], [RankChild],[ElementType],
  13:  [Url], [DocId], [Name], [DateLastModified], [NodeMetainfo], [NonNavPage],
  14:  [NavSequence], [ChildOfSequence])
  15:  values ('42514464-2EB2-4BB9-8A57-A5A6D80E3F67',
  16:  '92524380-DAE3-4CD6-B2C1-5A30C52ABC28',1025 ,0 ,0 ,0 ,1 ,'', NULL,
  17:  'Quick launch',getdate() ,NULL ,1 ,1 ,0)

After that go to Site settings > Navigation, made changes in global or current navigation nodes and click save. This time changes should be preserved. If you will select all navigation nodes from NavNodes table, you will see that now it contains all structure, and if you will try to work with navigation programmatically via PublishingWeb.Navigation.GlobalNavigationNodes or PublishingWeb.Navigation.CurrentNavigationNodes, you will be able to do it. BTW it also explains the fact how internally navigation nodes are filled when you click Ok on navigation settings page first time.

This is all for the problem with not saved navigation in Sharepoint. It was interesting investigation and I hope that even you won’t use it on practice, it will help to understand Sharepoint navigation better.

Monday, January 20, 2014

Change order of newly created publishing page or publishing site in navigation programmatically in Sharepoint

Most probably you already know about interesting behavior of Sharepoint navigation (since it exists from Sharepoint 2007): when you try to work with navigation nodes programmatically by accessing PublishingWeb.Navigation.GlobalNavigationNodes (or CurrentNavigationNodes), this collection will be empty until you will go to Site settings > Navigation and click Ok button. After this collection is filled and you may start really working with it. There is also number of workarounds for this already which allow to prefill navigation nodes and thus work with them using object model during provisioning (starting from Gary Lapointe’s gl-setnavigationnodes stsadm extension or PowerShell script from Waldek Mastykarz. If you will search you will find couple more solution). These solutions may be combined by one criteria: they solve problem for provisioning, but what about maintenance?

Will try to illustrate the problem: suppose that we provisioned site collection with number of sub sites and pages on the root site, so navigation (for this article it doesn’t matter is it current or global navigation) looks like this after provisioning:

image

Blue color means that nodes are provided dynamically by PortalSiteMapProvider, but not from PublishingWeb.Navigation.GlobalNavigationNodes collection. If we will use workarounds for prefilling navigation or if we will go to Site settings > Navigation and will click Ok, then navigation will look like this:

image

Green color means that navigation nodes are retrieved from PublishingWeb.Navigation.GlobalNavigationNodes collection, i.e. not constructed dynamically. After that we create new publishing web or new publishing page e.g. via feature. Interesting that after this navigation again uses dynamic node for this newly created sub site/page:

image

I.e. navigation is constructed both from PublishingWeb.Navigation.GlobalNavigationNodes collection and dynamically from PortalSiteMapProvider. It introduces problem e.g. if we need to change the order of the corresponding navigation node (using SPNavigationNode.Move method) we won’t be able to do it because there won’t be such node in it. Solution is the following: at first we need to mark corresponding publishing page or web as excluded from navigation:

   1: var page = pweb.GetPublishingPages().FirstOrDefault(
   2:     p => string.Compare(p.Name, pageName, true) == 0);
   3: if (page != null)
   4: {
   5:     page.IncludeInGlobalNavigation = false;
   6: }

Here are methods which we can use depending on the scenario:

After this PortalSiteMapProvider won’t show this page in navigation:

image

After that create new navigation node for the page or web programmatically and move it to the correct place:

   1: PublishingWeb pweb = ...;
   2: var globalNavigation = pweb.Navigation.GlobalNavigationNodes;
   3:  
   4: var node = SPNavigationSiteMapNode.CreateSPNavigationNode(title, url,
   5:     NodeTypes.AuthoredLinkPlain, globalNavigation);
   6: var dt = DateTime.Now;
   7: node.Properties["CreatedDate"] = dt;
   8: node.Properties["LastModifiedDate"] = dt;
   9: node.Properties["Description"] = "";
  10: node.Properties["Target"] = "";
  11: node.Update();
  12:  
  13: var nodeSite = globalNavigation.Cast<SPNavigationNode>().
  14:     FirstOrDefault(n => n.Title == "Test");
  15: if (nodeSite != null)
  16: {
  17:     node.Move(globalNavigation, nodeSite);
  18: }

In this example we moved new node for our page after node which corresponds to page/site with title “Test”. After this navigation looks like this:

image

I.e. we have navigation node for the new page in the right place in navigation, while dynamic node is hidden. Hope that this trick will help you in the working with Sharepoint navigation.

Wednesday, October 30, 2013

Declaratively configure navigation settings in Sharepoint publishing sites

If you have custom web template based on OTB publishing site (CMSPUBLISHING) you can configure navigation settings declaratively by adding NavigationProperties feature (id = 541F5F57-C847-4e16-B59A-B31E90E6F9EA) to your onet.xml and defining properties, e.g.:

   1: <Feature ID="541F5F57-C847-4e16-B59A-B31E90E6F9EA">
   2:   <Properties xmlns="http://schemas.microsoft.com/sharepoint/">
   3:     <Property Key="InheritGlobalNavigation" Value="true"/>
   4:     <Property Key="IncludeSubSites" Value="true"/>
   5:     <Property Key="IncludePages" Value="false"/>
   6:     <Property Key="IncludeInGlobalNavigation" Value="false"/>
   7:     <Property Key="IncludeInCurrentNavigation" Value="false"/>
   8:   </Properties>
   9: </Feature>

What other properties are supported? In order to answer on this question let’s check NavigationFeatureHandler feature receiver (from Microsoft.SharePoint.Publishing assembly). In its FeatureActivated method it uses private method ApplyNavigationProperties which does actual job:

   1:  
   2: private static void ApplyNavigationProperties(PublishingWeb publishingWeb,
   3: SPFeaturePropertyCollection properties)
   4: {
   5:     if (!publishingWeb.Web.AllProperties.ContainsKey("NavigationPropertiesSet"))
   6:     {
   7:         CmsSecurityUtilities.RunWithWebCulture(publishingWeb.Web, delegate {
   8:             CommonUtilities.ConfirmNotCentralAdminWebApp(publishingWeb.Web);
   9:             for (int j = 0; j < properties.Count; j++)
  10:             {
  11:                 SPFeatureProperty property = properties[j] as SPFeatureProperty;
  12:                 switch (property.Name)
  13:                 {
  14:                     case "InheritGlobalNavigation":
  15:                         publishingWeb.Navigation.InheritGlobal =
  16:                             bool.Parse(property.Value);
  17:                         break;
  18:  
  19:                     case "InheritCurrentNavigation":
  20:                         publishingWeb.Navigation.InheritCurrent =
  21:                             bool.Parse(property.Value);
  22:                         break;
  23:  
  24:                     case "ShowSiblings":
  25:                         publishingWeb.Navigation.ShowSiblings =
  26:                             bool.Parse(property.Value);
  27:                         break;
  28:  
  29:                     case "IncludeSubSites":
  30:                     {
  31:                         bool flag = bool.Parse(property.Value);
  32:                         publishingWeb.Navigation.GlobalIncludeSubSites = flag;
  33:                         publishingWeb.Navigation.CurrentIncludeSubSites = flag;
  34:                         break;
  35:                     }
  36:                     case "IncludePages":
  37:                     {
  38:                         bool flag2 = bool.Parse(property.Value);
  39:                         publishingWeb.Navigation.GlobalIncludePages = flag2;
  40:                         publishingWeb.Navigation.CurrentIncludePages = flag2;
  41:                         break;
  42:                     }
  43:                     case "GlobalIncludeSubSites":
  44:                         publishingWeb.Navigation.GlobalIncludeSubSites =
  45:                             bool.Parse(property.Value);
  46:                         break;
  47:  
  48:                     case "GlobalIncludePages":
  49:                         publishingWeb.Navigation.GlobalIncludePages =
  50:                             bool.Parse(property.Value);
  51:                         break;
  52:  
  53:                     case "CurrentIncludeSubSites":
  54:                         publishingWeb.Navigation.CurrentIncludeSubSites =
  55:                             bool.Parse(property.Value);
  56:                         break;
  57:  
  58:                     case "CurrentIncludePages":
  59:                         publishingWeb.Navigation.CurrentIncludePages =
  60:                             bool.Parse(property.Value);
  61:                         break;
  62:  
  63:                     case "GlobalDynamicChildLimit":
  64:                         publishingWeb.Navigation.GlobalDynamicChildLimit =
  65:                             int.Parse(property.Value);
  66:                         break;
  67:  
  68:                     case "CurrentDynamicChildLimit":
  69:                         publishingWeb.Navigation.CurrentDynamicChildLimit =
  70:                             int.Parse(property.Value);
  71:                         break;
  72:  
  73:                     case "OrderingMethod":
  74:                         publishingWeb.Navigation.OrderingMethod =
  75: (OrderingMethod) Enum.Parse(typeof(OrderingMethod), property.Value);
  76:                         break;
  77:  
  78:                     case "AutomaticSortingMathod":
  79:                     case "AutomaticSortingMethod":
  80:                         publishingWeb.Navigation.AutomaticSortingMethod =
  81: (AutomaticSortingMethod) Enum.Parse(typeof(AutomaticSortingMethod), property.Value);
  82:                         break;
  83:  
  84:                     case "SortAscending":
  85:                         publishingWeb.Navigation.SortAscending =
  86:                             bool.Parse(property.Value);
  87:                         break;
  88:  
  89:                     case "IncludeInGlobalNavigation":
  90:                         publishingWeb.IncludeInGlobalNavigation =
  91:                             bool.Parse(property.Value);
  92:                         break;
  93:  
  94:                     case "IncludeInCurrentNavigation":
  95:                         publishingWeb.IncludeInCurrentNavigation =
  96:                             bool.Parse(property.Value);
  97:                         break;
  98:                 }
  99:             }
 100:             publishingWeb.SetBooleanProperty("NavigationPropertiesSet", true);
 101:             publishingWeb.Update();
 102:         });
 103:     }
 104: }

So here we can see all supported properties:

Supported navigation properties
InheritGlobalNavigation
InheritCurrentNavigation
ShowSiblings
IncludeSubSites
IncludePages
GlobalIncludeSubSites
GlobalIncludePages
CurrentIncludeSubSites
CurrentIncludePages
GlobalDynamicChildLimit
CurrentDynamicChildLimit
OrderingMethod
AutomaticSortingMathod/AutomaticSortingMethod
SortAscending
IncludeInGlobalNavigation
IncludeInCurrentNavigation

You can use these properties in your onet.xml files.