Skip to content

The Chris Kent

Quick Fixes, Workarounds, and Other Neat Tricks

  • Home
  • About
  • Speaking
HomePosts tagged 'Managed Metadata'

Managed Metadata

Using Different Term Sets for Global and Current Navigation

March 14, 2016December 27, 2016 theChrisKent Client Settings, SharePoint CSOM, Current Navigation, Global Navigation, JSOM, Managed Metadata, Managed Navigation, quick launch, SharePoint, Taxonomy, TermSet, Top Bar Navigation, WebNavigationSettings
Applies to SharePoint 2013

Managed Navigation allows you to build navigation using managed metadata. It’s a cool feature but can get pretty confusing (and frustrating). One of the best features is setting up global navigation for your publishing site collection and having each subsite inherit the settings.

When it comes to navigation in SharePoint, the primary areas we are talking about are Global Navigation (Top Bar) and Current Navigation (Quick Launch). You can use Managed Navigation for both or either and choose to inherit both or either at the subsite level as well. Wowee!

The Problem

One of the frustrations, however, is trying to use separate termsets for your Global and Current navigation. The UI only allows you to select a single termset when you choose managed navigation for either area. The same termset is then used for both:

navigationsettings

The Almost Solution

On a subsite you can choose to inherit from the parent site one or both of your navigations. Then you can choose a different termset for that site. Rejoice!

For instance, if you wanted to share Global navigation across all your sites but wanted to have an independent Current navigation on your subsite, you would inherit the Global and choose Managed Navigation with your own termset for the Current. However, by using the termset on the subsite you cannot use it on any other site. So now, if you want to have a shared Global navigation as well as a shared Current navigation, you’re out of luck using the OOTB UI. Even if that wasn’t the case, you couldn’t have separate termsets on the root site.

This gives me a sad. If you do a quick search you’ll find all sorts of people struggling with this. In fact, maybe you came here from one of your own sad searches. Well be sad no longer!

The Solution

CSOM (or more specifically in the following examples, JSOM) to the rescue! Within the SP.Publishing.Navigation namespace there is an object called WebNavigationSettings that can be used to set the termsets independently of each other.

The bad news is you have to use code to do this. The good news is that it’s pretty simple code. The even better news is that you can easily undo the settings with the UI and the code is only needed to set the setting (it doesn’t have to stick around).

So, here’s the JSOM (this can be done with PowerShell or .NET using CSOM as well):

'use strict'
var NavSwitcher = NavSwitcher || {};

jQuery(function ($) {

    NavSwitcher.SafeLog = function (message) {
        if (window.console && window.console.log) {
            window.console.log(message);
        }
    };

    NavSwitcher.SiteConfig = function () {
        var safeLog = NavSwitcher.SafeLog;
        var ctx, wns, nav, tsId, tsGlobal;

        return {
            Configure: configure
        };

        function configure() {
        	tsId= $('#tsId').val();
			tsGlobal = $('#tsTypeG').prop('checked');

			if(tsId){
        		SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function(){
		            //Load up SP.Publishing
		            SP.SOD.registerSod('sp.publishing.js', SP.Utilities.Utility.getLayoutsPageUrl('sp.publishing.js'));
		            SP.SOD.executeFunc('sp.publishing.js','SP.Publishing.Navigation.NavigationTermSet',function(){

						safeLog('Setting Navigation...');
						ctx = SP.ClientContext.get_current();
						wns = new SP.Publishing.Navigation.WebNavigationSettings(ctx,ctx.get_web());
						ctx.load(wns);
						if(tsGlobal){
							nav = wns.get_globalNavigation();
						} else {
							nav = wns.get_currentNavigation();
						}
						ctx.load(nav);

						//load em up!
		                ctx.executeQueryAsync(
							Function.createDelegate(this, switchNavigation),
		    				Function.createDelegate(this, function(s, a){
		    					safeLog(a.get_message());
		    				}));
		            }); //end publishing sod
		    	}); //end clientcontext sod
	    	} else {
	    		safeLog('Provide an Id, yo!');
	    	}
        }

        function switchNavigation() {
        	try{
	        	safeLog('Initial nav object load complete');
	        	nav.set_termSetId(new SP.Guid(tsId));
	        	wns.update();
	        	ctx.load(wns);
	        	ctx.executeQueryAsync(
					Function.createDelegate(this, function(){
    					safeLog('Navigation Successfully Updated!');
    				}),
    				Function.createDelegate(this, function(s, a){
    					safeLog('**Navigation Update Failure!');
    					safeLog(a.get_message());
    				}));
	        }catch(e){
	        	safeLog('**Navigation Setting Failure!');
	        	safeLog(e);
	        }
        }

    }();

    $('#configButton').click(NavSwitcher.SiteConfig.Configure);
});

In the code above I’m using jQuery to pull the values from a simple HTML page I’ve got loaded in a Content Editor Web Part (see below). This is totally not necessary. This is just some Proof of Concept code you can strip out and learn from. It’s really just the configure and switchNavigation functions above that matter, but we’ll cover it all.

Lines 1-4 are just boiler plate code to establish a namespace and grab that jQuery ready event. Lines 6-10 establish a SafeLog function that lets me write to the console without worrying about crashing IE. Sheesh. None of this is required for this specific solution.

Lines 12-18 are just me using the Module Pattern. The important thing to note is my “private” variables in line 14. I set them up here so that I can access them when I’m doing all the CSOM load calls and then also in the Callback function.

In lines 21 and 22, I pull the values from my HTML form. The Id (tsId) is expected to be the termset GUID (as a string). The tsGlobal variable just indicates if we are setting the global navigation termset or the current navigation termset. These could be replaced with a hardcoded string and boolean if you wanted.

Gathering the Objects

Now for some SODs (Script on Demand)! SODs let you delay your code until the proper scripts have been loaded. In addition, you can request SharePoint to load some it might not otherwise load on a particular page. In this case, we have 2 script files we need to have loaded before our code runs: sp.js and sp.publishing.js.

The sp.js script file is going to load on the page so we can just use the executeFunc method to call our code once it does. This is needed so that we can use CSOM (The SP.ClientContext namespace is essential for that). We do this in line 25.

The sp.publishing.js script file may or may not load on a given page. So in line 27 we use the registerSod method to ensure it will get loaded. This is immediately followed by another executeFunc call in line 28 that will call our function once SP.Publishing.Navigation.NavigationTermSet has loaded.

Now that all the objects are available to us, it’s time to run our actual logic. We grab the current context in line 31. Then we grab the WebNavigationSettings for the current web in line 32. We of course have to load that up in line 33.

Since I’ve made which navigation (Global vs Current) configurable, I do a check to see which one was requested in line 34 and load the corresponding StandardNavigationSettings object in lines 35 or 37 respectively. Then of course we load it up too in line 39.

Now that we’ve got our shell objects, it’s time to fill them with a call to executeQueryAsync in lines 42-46. On successful load we’ll go the switchNavigation function and on failure we’ll write a sad message to the console.

Making the Change

The switchNavigation function in lines 54-72 is where the “magic” happens (we change a setting, wow)! Since the Id I’m working with is a string, I create an SP.Guid from it when setting the StandardNavigationSettings termSetId property in line 57. However, if you already had that guid (say from pulling directly from the TaxonomySession) you could use that directly.

Now that we’ve set the value, time to tell SharePoint. So line 58 updates the WebNavigationSettings object (there is no update method on the StandardNavigationSettings object), loads it up in line 59, and sets up another call to the executeQueryAsync method in lines 60-67.

That’s it. Once that code runs, the navigation settings will be updated. You might have to wait a minute or refresh a couple of times (caching), but there you go!

results

Other Stuff

Getting the TermSetId

I used a string for the TermSetId which I then used as a GUID. This works great. A better option, however, might be to pull this directly from the Term Store blah blah blah. But, if you’re just looking to set this up quickly, you can get the GUID as a string from the Term Store Management Tool (The link is in Site Settings). Just click on the Term Set you want and copy that Unique Identifier in the General tab:

termsetguid

The HTML

Here’s the HTML I used. I just put it inside a content editor web part then clicked the configButton. Feel free to use this, but more likely you’ll be writing this as part of a larger solution.

<!DOCTYPE html>

<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title></title>
</head>
<body>
<table>
<tr>
<td><strong>TermSetId:</strong></td>
<td><input type="text" id="tsId" value=""/></td>
</tr>
<tr>
<td><input type="radio" value="C" name="tsType" checked="checked"/><label for="C">Current</label></td>
<td><input type="radio" id="tsTypeG" value="G" name="tsType" /><label for="G">Global</label></td>
</tr>
</table>
<input type="button" id="configButton" value="Set Nav TermSet" />

    <script type="text/javascript" src="/sites/navpin/Style%20Library/ns/jquery-2.2.0.min.js"></script>
    <script type="text/javascript" src="/sites/navpin/Style%20Library/ns/navswitcher.js"></script>
</body>
</html>

Wowsers!

Rate this:

Share this:

  • Twitter
  • Facebook
  • More
  • Print
  • Email
  • Tumblr
  • Pinterest
  • Reddit
  • LinkedIn

Like this:

Like Loading...
Leave a comment

Changing a UserControl in the 14 Hive Using a Custom Timer Job

May 10, 2012May 9, 2012 theChrisKent .NET, List/Library Settings, MetaDataNavExpansion, SharePoint Managed Metadata, MetaData Navigation, MetaDataNavExpansion, MetaDataNavTree, SharePoint, SPServiceJobDefinition, Timer Job, User Control, WireBear
Applies To: SharePoint 2010, .NET Framework (C#, VB.NET)

As mentioned in a previous post, I’ve recently put together a solution for automatically updating the MetaDataNavTree.ascx User Control to default the MetaData Navigation expansion to include the actual taxonomy items. You can download the solution as well as the source for free from CodePlex here: WireBear MetaDataNavExpansion.

This post is very similar to my post Updating an XML File in the 14 Hive Using a Custom Timer Job and assumes you know some basics about custom timer job creation (If not, check my other post Implementing a Custom SharePoint Timer Job). Either way, most of the code will be given right here anyway.

The goal for this timer job is to either backup the MetaDataNavTree.ascx file in the 14\TEMPLATE\CONTROLTEMPLATES folder with the SharePoint Hive and to adjust the Tree’s ExpandDepth property from 0 to 2 or to restore the backup previously made. Here is the entire MetaDataNavExpansionJob class:

Imports Microsoft.SharePoint.Administration
Imports Microsoft.SharePoint.Utilities
Imports System.Text.RegularExpressions

Public Class MetaDataNavExpansionJob
    Inherits SPServiceJobDefinition

#Region "Properties"

    Private _userControlPath As String
    Public ReadOnly Property UserControlPath() As String
        Get
            If String.IsNullOrEmpty(_userControlPath) Then _userControlPath = SPUtility.GetGenericSetupPath("TEMPLATE\CONTROLTEMPLATES\MetadataNavTree.ascx")
            Return _userControlPath
        End Get
    End Property

    Private _userControlBackupPath As String
    Public ReadOnly Property UserControlBackupPath() As String
        Get
            If String.IsNullOrEmpty(_userControlBackupPath) Then _userControlBackupPath = SPUtility.GetGenericSetupPath("TEMPLATE\CONTROLTEMPLATES\MetadataNavTree.ascx.bak")
            Return _userControlBackupPath
        End Get
    End Property

    Private Const InstallingKey As String = "DocIconJob_InstallingKey"
    Private Property _installing() As Boolean
        Get
            If Properties.ContainsKey(InstallingKey) Then
                Return Convert.ToBoolean(Properties(InstallingKey))
            Else
                Return True
            End If
        End Get
        Set(ByVal value As Boolean)
            If Properties.ContainsKey(InstallingKey) Then
                Properties(InstallingKey) = value.ToString
            Else
                Properties.Add(InstallingKey, value.ToString)
            End If
        End Set
    End Property

#End Region

    Public Sub New()
        MyBase.New()
    End Sub

    Public Sub New(JobName As String, service As SPService, Installing As Boolean)
        MyBase.New(JobName, service)
        _installing = Installing
    End Sub

    Public Overrides Sub Execute(jobState As Microsoft.SharePoint.Administration.SPJobState)
        AdjustMetaDataNavExpansion()
    End Sub

    Private Sub AdjustMetaDataNavExpansion()
        If _installing Then
            If My.Computer.FileSystem.FileExists(UserControlPath) Then
                'Backup the original
                My.Computer.FileSystem.CopyFile(UserControlPath, UserControlBackupPath, True)
                Dim contents As String = My.Computer.FileSystem.ReadAllText(UserControlPath)

                'Replace the Expansion with First Level Expansion
                My.Computer.FileSystem.WriteAllText(UserControlPath, Regex.Replace(contents, "ExpandDepth=""\d+""", "ExpandDepth=""2"""), False)
            End If
        Else
            If My.Computer.FileSystem.FileExists(UserControlBackupPath) Then
                'Restore the original
                My.Computer.FileSystem.MoveFile(UserControlBackupPath, UserControlPath, True)
            End If
        End If
    End Sub

End Class

Lines 8-44 are just the declaration of and logic needed to persist some properties. Again, more information can be found in my previous post, but basically I am using the SPJobDefinition’s Properties HashTable to store my own properties as specified in the constructor. Except for in the case of the UserControlPath and UserControlBackupPath properties which are really just wrapping up some logic to get a reference to specific files in the 14 Hive’s TEMPLATE\CONTROLTEMPLATES directory using the SPUtility class.

The Execute method beginning in line 55 is what is called when the Timer Job actually runs. I override this method to ensure my custom code gets called instead. My custom code really begins in the AdjustMetaDataNavExpansion method starting at line 59.

If this job is installing (Running on Solution Activation), the MetaDataNavTree.ascx file is copied to MetaDataNavTree.ascx.bak as a backup of the original in line 63. The UserControl file is then read in as text and a regular expression searches for and replaces the ExpandDepth=”SomeNumber” property and replaces it with ExpandDepth=”2″. This is all done and saved back into the file in lines 66-67.

If this job is uninstalling (Running on Solution Deactivation), the backup file (MetaDataNavTree.ascx.bak) created on activation is restored in line 72.

To run this from activation and deactivation I simply copy my design from the PDFdocIcon project and create and run a new version of the job. Since this code is nearly identical to what I’ve already explained, I won’t go into detail but I will save you some time and have copied it below. This is the Main.EventReceiver:

Option Explicit On
Option Strict On

Imports System
Imports System.Runtime.InteropServices
Imports System.Security.Permissions
Imports Microsoft.SharePoint
Imports Microsoft.SharePoint.Security
Imports Microsoft.SharePoint.Administration

''' <summary>
''' This class handles events raised during feature activation, deactivation, installation, uninstallation, and upgrade.
''' </summary>
''' <remarks>
''' The GUID attached to this class may be used during packaging and should not be modified.
''' </remarks>

<GuidAttribute("a885d247-f5e8-4456-abd2-6cfebb2bdfde")> _
Public Class MainEventReceiver
    Inherits SPFeatureReceiver

    Public Sub RunMetaDataNavExpansionJob(Installing As Boolean, properties As SPFeatureReceiverProperties)
        Dim JobName As String = "MetaDataNavExpansionJob"

        'Ensure job doesn't already exist (delete if it does)
        Dim query = From job As SPJobDefinition In properties.Definition.Farm.TimerService.JobDefinitions Where job.Name.Equals(JobName) Select job
        Dim myJobDefinition As SPJobDefinition = query.FirstOrDefault()
        If myJobDefinition IsNot Nothing Then myJobDefinition.Delete()

        Dim myJob As New MetaDataNavExpansionJob(JobName, SPFarm.Local.TimerService, Installing)

        'Get that job going!
        myJob.Title = String.Format("Configuring MetaData Navigation for {0} Expansion", IIf(Installing, "First Level", "Default"))
        myJob.Update()
        myJob.RunNow()
    End Sub

    Public Overrides Sub FeatureActivated(ByVal properties As SPFeatureReceiverProperties)
        RunMetaDataNavExpansionJob(True, properties)
    End Sub

    Public Overrides Sub FeatureDeactivating(ByVal properties As SPFeatureReceiverProperties)
        RunMetaDataNavExpansionJob(False, properties)
    End Sub

End Class

I hope you’re beginning to see that automating any manual changes to the 14 Hive can follow the Service Timer Job Solution pattern I’ve now demonstrated twice. This isn’t true for everything (Web.config changes should be done through the object model or a config.something.xml file, workflow actions can have their own file, etc.), but for those little one off things that don’t have a better alternative, this is a great way to take care of it.

Rate this:

Share this:

  • Twitter
  • Facebook
  • More
  • Print
  • Email
  • Tumblr
  • Pinterest
  • Reddit
  • LinkedIn

Like this:

Like Loading...
Leave a comment

Changing the Default Expansion of MetaData Navigation on Initial Page Load

May 9, 2012 theChrisKent .NET, Information Management, List/Library Settings, MetaDataNavExpansion, SharePoint .NET, Managed Metadata, MetaData Navigation, MetaDataNavTree, SharePoint, Taxonomy, Timer Job, Tree, WireBear
Applies To: SharePoint 2010

We’ve recently begun making fairly heavy use of the MetaData Navigation available for lists. It’s intuitive and easy to use and our users are really liking it. But one minor, but frequently mentioned, irritation brought up by nearly everyone who tried to use the site was that the navigation elements were collapsed by default when they went to the site.

This isn’t a big deal for most people since they get used to where the navigation elements are (just expand the folder), but does create an extra barrier for new users as they try and figure out how to use the site. Microsoft has taken an awesome feature and traded it’s potentially intuitive use to account for some potential performance issues.

The issue seems to be that navigation using taxonomies with several top level items would significantly delay initial page load. This is certainly true, but the solution is not to cripple all uses of MetaData Navigation, the solution is to not use it with those types of taxonomies! Not only would you have had that performance issue on initial expansion anyway, having that many top level items makes for bad navigation. If your taxonomy has several top level items (2000+) then it is either not a good candidate for MetaData navigation or you need to group those items into sub-nodes.

In searching for an answer to this problem I came across this answer on technet by Entan Ming. In it he gives the manual steps to take care of this issue. Here is a brief summary of the steps that must be performed manually on every server:

  1. Open the MetadataNavTree.ascx file in your 14 Hive (TEMPLATE\CONTROLTEMPLATES) using notepad
  2. Change the line ExpandDepth=”0″ to ExpandDepth=”2″
  3. Save the changes and refresh the page(s) with MetaData Navigation

These are easy to do and you can follow them and be done. However, just like PDF Icon Mapping there are some problems with this approach:

  • Manual changes can often be error-prone
  • The change must be performed on every server
  • The change must be performed whenever a new server is added to the farm
  • The change will have to be redone in the event of disaster recovery

So, using the same technique I use for PDF Icon Mapping entries, I’ve created a SharePoint solution to do this automatically.

You can find this solution over on CodePlex as WireBear MetaDataNavExpansion. It is free for personal and commercial use (License here). You can also find basic installation instructions as well. It’s super easy to setup since it’s just a standard SharePoint Solution that you globally deploy.

The full source code is available on CodePlex and I’ll write up another article about what’s really happening, but here’s a general summary:

  • On Activation and Deactivation a one time Service Timer Job is run.
  • On Activation, the Timer Job creates a backup of the MetaDataNavTree.ascx file within the 14\TEMPLATE\CONTROLTEMPLATES folder.
  • The MetaDataNavTree has it’s tree’s ExpandDepth property changed from 0 to 2
  • When Deactivating, the Timer Job restores the original MetaDataNavTree.ascx file

It’s pretty straightforward. It just automates the manual steps listed above. This allows it to be applied automatically to any new servers added to the farm and will be reapplied in the event of disaster recovery.

Here’s what it looks like on initial page load

Standard: With MetaDataNavExpansion:
   
“Der… What do I do?” “WOWEE! This site is amazing!”

Rate this:

Share this:

  • Twitter
  • Facebook
  • More
  • Print
  • Email
  • Tumblr
  • Pinterest
  • Reddit
  • LinkedIn

Like this:

Like Loading...
3 Comments

Disable/Remove Enterprise Keywords from a list or library

February 27, 2012February 29, 2012 theChrisKent List/Library Settings, SharePoint Document Library, Enterprise Keywords, Managed Metadata, SharePoint
Applies to: SharePoint

I was experimenting with the tagging features of SharePoint 2010. Part of that experiment was adding an Enterprise Keywords column to my document library. After playing with it and realizing the Tag Cloud is nearly worthless, I don’t like the global nature of the keywords for our specific application, and I prefer the standard term set key filters portion of the managed metadata navigation to the Tags portion – I wanted to remove this feature.

So I went to Library Settings > Enterprise Metadata and Keywords Settings. This is the same place I turned it on. There were 2 checkboxes then to enable the Enterprise Keywords column and also to publish to social tags. I had just checked both when I wanted to turn it on and the column magically appeared in my library and content type. I assumed I would just uncheck this same box to remove it (hence the entire concept of a checkbox).

Unfortunately going to the page showed I could uncheck the bottom one to turn off social tag publishing but the Enterprise Keywords checkbox was disabled. Fortunately this was dev and I wasn’t too concerned, so I could experiment. The solution is pretty easy, but I’d have been wary to try it in production. So here it is if you are stuck in production and worried:

Just click on the Enterprise Keywords column in your list of columns and select delete. When you go back to the Enterprise Metadata and Keywords Settings page everything will be unchecked and you’re good to go. Obviously all that data will be lost on every item, but if you are disabling this feature you were probably cool with that anyway.

Rate this:

Share this:

  • Twitter
  • Facebook
  • More
  • Print
  • Email
  • Tumblr
  • Pinterest
  • Reddit
  • LinkedIn

Like this:

Like Loading...
1 Comment

Office Development

Recent Posts

  • Getting the Binary Value of an ASCII Character in Power Apps
  • Converting Integers to Binary in Power Apps
  • Converting Binary to Integers in Power Apps
  • Thank you M365 Collaboration Conference!
  • Custom Icon Buttons in Power Apps with Hover Color

Popular Posts

  • Applying Column Formats to Multi-line Text Fields
  • Custom Icon Buttons in Power Apps with Hover Color
  • Use Local Files in CefSharp
  • Applying pdfmark To PDF Documents Using GhostScript
  • Generate List Formatting Elements in a Loop Using forEach

Archives

  • May 2022
  • April 2022
  • May 2021
  • June 2020
  • April 2020
  • November 2019
  • May 2019
  • April 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • August 2018
  • July 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • January 2018
  • December 2017
  • November 2017
  • October 2017
  • September 2017
  • August 2017
  • July 2017
  • June 2017
  • May 2017
  • April 2017
  • March 2017
  • February 2017
  • January 2017
  • December 2016
  • November 2016
  • October 2016
  • September 2016
  • August 2016
  • July 2016
  • June 2016
  • May 2016
  • April 2016
  • March 2016
  • February 2016
  • January 2016
  • August 2015
  • April 2015
  • February 2015
  • September 2014
  • August 2014
  • July 2014
  • June 2014
  • May 2014
  • April 2014
  • March 2014
  • February 2014
  • January 2014
  • December 2013
  • November 2013
  • October 2013
  • August 2013
  • March 2013
  • February 2013
  • January 2013
  • November 2012
  • October 2012
  • September 2012
  • August 2012
  • July 2012
  • June 2012
  • May 2012
  • April 2012
  • March 2012
  • February 2012
Create a website or blog at WordPress.com
  • LinkedIn
  • Twitter
  • Follow Following
    • The Chris Kent
    • Join 1,093 other followers
    • Already have a WordPress.com account? Log in now.
    • The Chris Kent
    • Customize
    • Follow Following
    • Sign up
    • Log in
    • Report this content
    • View site in Reader
    • Manage subscriptions
    • Collapse this bar
 

Loading Comments...
 

    loading Cancel
    Post was not sent - check your email addresses!
    Email check failed, please try again
    Sorry, your blog cannot share posts by email.
    %d bloggers like this: