SharePoint and other geeky stuff

Menu

Skip to content
  • Home
  • About Me

Tag Archives: Managed Metadata

Power BI and filtering on Taxonomy/Managed Metadata terms.

Posted on July 3, 2017 by cann0nf0dder

Power BI is a good way of display results from inside SharePoint in a dashboard/report. I’m using Power BI desktop to set up my report, which can be downloaded from the Microsoft Site: https://powerbi.microsoft.com/en-us/desktop/

If you have ever imported data from SharePoint, you might have noticed that the Managed Metadata columns do not get pulled through the way you want for reporting. You end up with Id’s and GUIDs. This guide will show you how to bring in the term label and how to setup your Power BI report so that you can slice the data on Managed Metadata.

The Term Store Management tool (Which can be found in Site Settings -> Site Administration -> Term Store management, for a site or in the SharePoint admin centre for global) holds your TermStore, Groups, TermSets and Terms.

In the screen shot above, I have:

  • Term Store called Taxonomy_IC4zNx….
  • Term Group called CannonFodder
  • TermSets called Age Ranges, Animation Types, Broadcasters etc.
  • Terms which you can see underneath Countries.

For my demo I have created two TermSets called Countries (which you can see above) and Genre.

SharePoint data.

I have a SharePoint list that I have created using data from movie information from a website called The Numbers. (Ref: http://www.the-numbers.com/movies/ ). My list has 4 columns:

  • Title – Single Line of Text (Title of movie)
  • Total Worldwide Box Office – Currency
  • Genre – Managed Metadata
  • Country – Managed Metadata

I have added 5 items of data, taking the top ten from 5 different countries. Example screenshot below.

Power BI setup

After downloading an up to date copy of Power BI desktop and installed it, when you open it, first thing you need to do is get some data. So click Get data

After you have clicked Get data a dialog will appear and you are given a choice of data to connect to. About half way down the list, there is a connection to SharePoint List, select it and then click Connect.

Enter the Site’s URL in the dialog. If prompted, please sign in with your organisational account. After you have signed in successfully, another dialog will appear with a list of all the Lists and Libraries available on the site. (including the hidden lists). I have selected the Movies list. I have also selected the TaxonomyHiddenList, this is very important as this is where all your taxonomy terms are stored for the site that have been used within lists. After selecting your two lists, click Edit. Here we are going to do some tidying first with the data before starting to create reports.

Query Editor

The query editor in Power BI, allows you to manipulate data. Here I would remove columns that I’m not going to be using, ensure the columns have been brought in using the right data type.

With the Movies list first, by clicking on the Choose Columns button on the ribbon, I can select that only the following columns are used.

I have selected just the columns that are displayed in my SharePoint list and deleted all the others. Click OK

You should now be seeing a screen similar to mine below. You will notice that Genre and Country are displayed as records

By clicking on the expanding arrows next to Genre/Country a pop-out will show. This allows you to expand this column to show the columns found within the record. You only need to have TermGuid ticked here. However, to explain my point further that Power BI shows only ID’s and GUID’s I will select all for my screen shots. By keeping the Original Column Name as a Prefix, this will help me identify which columns are for Genre and when I repeat the process for Country which columns are for country.

After expanding both columns you can see that the column label, where you would have expected an English word, is actually a number.

On the left-hand side of the screen where the Queries are, right click TaxonomyHiddenList and create a Reference from it. Do this twice.

By right clicking on the TaxonomyHiddenList (2), rename this to Genre, and with TaxonomyHiddenList (3), rename this to Country. The reason why I have created 2 references instead of 1 duplicate, is so that I can always have the full TaxonomyHiddenList available to me. The two referenced tables will be filtered to only bring back values for the corresponding TermSets.

Now I need to know the TermSetId for Country and Genre. This can be easily obtained directly in your SharePoint site in the Term Store Management Tool. Find the TermSet called Country and click it, then under Unique Identifier you can find the TermSetId. Take a copy of this for both Country and Genre.

Back in Power BI, in the Query for Country first, find the column called IdForTermSet. Click the dropdown button, and you want to select the GUID that matches to the Unique Identifier you found in the last step for Country. Click OK.


Your query will now only show the rows for the given TermSetId, in my screenshot below this is for Country. I then repeat in the Genre query for Genre.


Then for both Country and Genre I remove all columns apart from Title and IdForTerm. Use Term<LCID> instead of Title if you are displaying from a different language.

Lastly, I rename the Title columns to Genre and Country in the corresponding query. Then I click the Close & Apply button from the ribbon bar.

Relationship

The next step is configuring the relationship between the TermSet table and the Movie table. At the left hand side of the page, select the Relationship icon. You can see from the screen that Power BI has tried it’s best to match up your relationship, however you will notice that it is incorrect.

Click on Manage Relationships in the ribbon bar. On the Mange relationships dialog, delete both relationships that are currently there. Then click New to create a new relationship. The first table you should select, would be Movies, then click on the column called Country.TermGuid. Then for the second table, select country, and click the column called IdForTerm. Set the cardinality to Many to one, and I’m going to set the Cross Filter direction to single. (Note:
If you se the Cross Filter direction to both, when you use slicers for Country and Genre later on, it will filter the other slicer to only show the values left in the current data set)

I’m am then repeating the process for Genre. Then Close the Manage Relationship dialog.

Displaying data

Now I have set up the relationship correctly, when I want to display the Country/Genre, I can pull the information from the Country or Genre list. For example, I’m adding a Table to my report. I drag in Title and Total World Wide Box Office from the movie table. Then I drag in Genre from the Genre table, and Country from the Country Table.

I can also add two slicers to the page, one is Country, and the other is Genre. I can then filter the data further, in the below screenshot, I’m filtering by United Kingdom and Thriller/Suspense films.

There is plenty more I can do with this data now, now that I have the correct label terms for Taxonomy/Managed Metadata columns in my Power BI.

Advertisement

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in SharePoint | Tagged Managed Metadata, PowerBI, SharePoint

Merging JavaScript using JQuery extend

Posted on November 29, 2014 by cann0nf0dder

In a previous blog, I showed how to use a configuration page that attaches a Site Column to a Managed Metadata TermSet. This was done using JavaScript. With SharePoint Online and sandbox solutions, if you are unable to configure SharePoint using declarative XML, the only option you have left (apart from manually point and clicking after deployment) is using JavaScript.

In all the Sandbox solutions I have created there is always a core.wsp file that has a bunch of configuration. Recently I created an additional wsp that on first load I needed to run the same configuration scripts, however the configuration data needed to be different. For example, different quick launch navigation, different branding, different logo at the top left of the screen. This was a perfect scenario for using the JQuery extend to merge the JavaScript files together.

The demo today won’t actually show you any configuration code, however it will show you two solutions. The first solution, will display JavaScript on a page. This JavaScript is a copy of the main JavaScript configuration. After uploading the second solution the second JavaScript configuration will be merged with the first and displayed on the page. This is all done using the following code

if (CF.SharePoint.CoreSiteConfiguration.Data != null) {
    jQuery.extend(CF.SharePoint.CoreSiteConfiguration.Data, CF.SharePoint.SiteConfiguration.Data);
};

Before I start walking through the solution, I want to thank Pumbaa80 on StackOverflow who provided me with the syntaxHighlight(json) function which allowed me to display JavaScript within a webpage prettified.

 

Solution 1 MergingJavascript.wsp

All the files from the first solution can be seen above. I’m just using JQuery library. The configuration data is in a file called CF.SharePoint.CoreSiteConfiguration.js and looks like below:

var CF = CF || {};
CF.SharePoint = CF.SharePoint || {};
CF.SharePoint.CoreSiteConfiguration = CF.SharePoint.CoreSiteConfiguration || {};
 
CF.SharePoint.CoreSiteConfiguration.Data = {
    Branding: {
        ColorPaletteUrl: "/_catalogs/theme/15/CF.spcolor",
        FontSchemeUrl: null,
        MasterPageUrl: "/_catalogs/masterpage/CF/CF.master",
        CustomMasterPageUrl: "/_catalogs/masterpage/CF/CF.master",
        BackgroundImageUrl: null
    },
    TaxonomyFields: [
        { Id: '{F5F171D8-47C7-4698-8B28-78A86A95A2E8}', Name: 'CF_TaxCountries' },
        { Id: '{C03CC67A-4E66-4723-9D0F-3D7CB117DA0C}', Name: 'CF_TaxNavigation' },
    ],

    ContentTypeBindings: [
        { ContentTypeId: "0x0101003C7036AFEEB24CFC95926C7E222CE2BC", ContentTypeName: "CF Document", ListTitle: "Documents", ListUrl: "Shared Documents" }
    ],

    DefaultContentTypes: [
        { ContentTypeId: "0x0101003C7036AFEEB24CFC95926C7E222CE2BC", ContentTypeName: "CF Document", ListTitle: "Documents", ListUrl: "Shared Documents" }
    ],

    RequiredSharePointJSFiles: [
        "sp.js",
        "sp.ui.dialog.js",
        "sp.taxonomy.js",
        "sp.publishing.js"
    ]
};

I’ve declared a namespace at the top of the JavaScript file, then it’s just a bunch of objects declared within CF.SharePoint.CoreSiteConfiguration.Data.

I upload jQuery and my configuration.js file to a folder called Assets at the site collection. You can see in my solution, I have a Module called MO_Assets and a folder called Scripts. The elements file for this Module is as follows:

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="Assets">
    <File Path="MO_Assets\Scripts\jquery-2.1.1.min.map" Url="Assets/Scripts/jquery-2.1.1.min.map" ReplaceContent="TRUE" />
    <File Path="MO_Assets\Scripts\jquery-2.1.1.min.js" Url="Assets/Scripts/jquery-2.1.1.min.js" ReplaceContent="TRUE" />
    <File Path="MO_Assets\Scripts\CF.SharePoint.CoreSiteConfiguration.js" Url="Assets/Scripts/CF.SharePoint.CoreSiteConfiguration.js" ReplaceContent="TRUE"/>
  </Module>
</Elements>

 

This JavaScript is inserted into my page using a Custom Action CA_JavascriptConfiguration.
Take note: JQuery is Sequence 10, the CoreSiteConfiguraiton.js is inserted as Sequence 11.

<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction ScriptSrc="~SiteCollection/assets/Scripts/jquery-2.1.1.min.js"
              Location="ScriptLink"
              Sequence="10">
  </CustomAction>
  <CustomAction ScriptSrc="~SiteCollection/assets/Scripts/CF.SharePoint.CoreSiteConfiguration.js"
                                Location="ScriptLink"
                                Sequence="11">
  </CustomAction>
</Elements>

Lastly I have an aspx page that will display the JavaScript. This page is just a standard SharePoint aspx page you might write.

<%@ Assembly Name="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint.WebPartPages" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Page Language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" EnableViewState="false" meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document" %>
 
<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server" >
</asp:Content>
 
<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
               Display JavaScript Page
</asp:Content>
 
<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderSearchArea" runat="server" />
 
<asp:Content ID="Content4" ContentPlaceHolderID="PlaceHolderMain" runat="server">
                <SharePoint:FormDigest ID="FormDigest1" runat="server"/>
</asp:Content>

Within the PlaceHolderMain after the FormDigest1 we add a <pre> box to display the code.

<pre id="DisplayText"></pre>

Within the PlaceHolderAdditionalPageHead we will add the CSS and the Javascript that will get the ConfigurationData, then Stringify it, then lastly syntax highlight it to prettify it for the demo, before adding it to the <pre> html tag ID “DisplayText”. Note: If the JavaScript within this page was the actual configuration functions, it would probably be in a separate file, then added to the page using a Custom Action. This custom action sequence would need to be higher that the JQuery and the Configuration data we added earlier. Ideally, it should be a few sequence’s lower, so that you can add more JavaScript files before your configuration code if needed. In this case, because it is on the page, it is ran after the custom actions anyway.

<style type="text/css">
        #DisplayText {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
        .string { color: green; }
        .number { color: darkorange; }
        .boolean { color: blue; }
        .null { color: magenta; }
        .key { color: red; }
    </style>

 

<script type="text/javascript">
        function syntaxHighlight(json) {
            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
            return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
                var cls = 'number';
                if (/^"/.test(match)) {
                    if (/:$/.test(match)) {
                        cls = 'key';
                    } else {
                        cls = 'string';
                    }
                } else if (/true|false/.test(match)) {
                    cls = 'boolean';
                } else if (/null/.test(match)) {
                    cls = 'null';
                }

                return '<span class="' + cls + '">' + match + '</span>';
            });
        }
               

        function loadFunction() {
            var str = JSON.stringify(CF.SharePoint.CoreSiteConfiguration.Data, undefined, 4);
            jQuery("#DisplayText").append(syntaxHighlight(str));
        };

        _spBodyOnLoadFunctionNames.push("loadFunction");
    </script>

The Page module has an elements file, here I’m just adding the DisplayJavascript.aspx to the web file which will only show up in the RootWeb.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Module Name="MO_Pages" Path="MO_Pages" RootWebOnly="TRUE">
    <File Path="DisplayJavascript.aspx" Url="DisplayJavascript.aspx" Type="Ghostable" ReplaceContent="TRUE" />
  </Module>
</Elements>

 

After adding the Module, Custom action and the Page to the Web Feature, we can package up the WSP and deploy to our teamsite. Activate the Web Feature, mine is called CFSP Merging JavaScript. Then navigate to your site [sitecollection]/DisplayJavascript.aspx.

Solution 2 AdditionalJavaScript.

Ok, so we have the basic configuration in place. This would be the JavaScript objects that would be used during configuration. However in a second web site, we still want the Core solution to be installed, but when we configure we want the configuration script to use a different/merged set of JavaScript objects. This demo solution is made of up just the new JavaScript objects and a custom action. Below is the solution structure.

The Module MO_SiteConfigurationFile has a JavaScript file which is added to the same Assets library of the original Site Configuration data file. This file doesn’t overwrite the Site Configuration, it is just added to the folder.

var CF = CF || {};
CF.SharePoint = CF.SharePoint || {};
CF.SharePoint.SiteConfiguration = CF.SharePoint.SiteConfiguartion || {};
 
CF.SharePoint.SiteConfiguration.Data = {
    Branding: {
       ColorPaletteUrl: "/_catalogs/theme/15/CF.orange.spcolor",
       FontSchemeUrl: "/_catalogs/theme/15/CF.spfont",
       MasterPageUrl: "/_catalogs/masterpage/CF/CFWithLeftNav.master",
       CustomMasterPageUrl: "/_catalogs/masterpage/CF/CFWithLeftNav.master",
       BackgroundImageUrl: null
   },

    NonPublishingNavigation: {
        QuickLaunchNavigation: {
            List: [
                { Title: 'Home', Url: '/' },
                { Title: 'Notebook', Url: '/SiteAssets/$WebTitle Notebook' },
                { Title: 'Documents', Url: '/Documents/Forms/AllItems.aspx' },
                { Title: 'Calendar', Url: '/Lists/Calendar/calendar.aspx' },
                { Title: 'Tasks', Url: '/Lists/Tasks/AllItems.aspx' },
                { Title: 'Site Contents', Url: '/_layouts/15/viewlsts.aspx' }
            ],
            KeepRecent: false
        }
    },

    RequiredSharePointJSFiles: [
        "sp.js",
        "sp.ui.dialog.js",
        "sp.taxonomy.js",
        "sp.publishing.js",
        "sp.userprofiles.js"
    ]    
}
 

//Merge new and updated objects over existing objects.
if (CF.SharePoint.CoreSiteConfiguration.Data != null) {
    jQuery.extend(CF.SharePoint.CoreSiteConfiguration.Data, CF.SharePoint.SiteConfiguration.Data);
};

You will notice that I’m using a different namespace. CF.SharePoint.SiteConfiguration, where the configuration data file of the core solution was CF.SharePoint.CoreSiteConfiguration. The Branding is different, I’m using a different colour file, different master pages too. I’m not making any changes to the original Taxonomy Fields, Content Type Bindings, or Default Content Types. I have added configuration for the Quick Launch Navigation, and the RequiredSharePointJSFiles has an extra value “sp.userprofiles.js”. Note: When you are merging the files, an object structure needs to be copied over as a whole, and any values that are to remain the same need to be included in the new file. Otherwise it will assume this value is replace everything.

For example the RequiredSharePointJSFiles.

If in my new configuration data file I just put:

RequiredSharePointJSFiles: [
        "sp.userprofiles.js"
    ]    

When merged, only that value would be remaining, it merges at the top object level (Branding, TaxonomyFields, ContentTypeBindings, DefautlContentTypes, NonPublishingNavigation, RequiredSharePointJSFiles). Therefore to ensure all the original values are there including your new ones, you need to include everything you want to keep at that top level too.

RequiredSharePointJSFiles: [
        "sp.js",
        "sp.ui.dialog.js",
        "sp.taxonomy.js",
        "sp.publishing.js",
        "sp.userprofiles.js"
    ]    

Lastly at the bottom of this file, we are checking if the original CoreSiteConfiguraiton exist, and if so Merge CF.SharePoint.SiteConfiguration.Data into CF.SharePoint.CoreSiteConfiguration.Data, using the jQuery.extend function. The merge is happening at the .Data level.

The element file for the SiteConfigurationFile module as stated earlier is added to the same asset folder we added our JavaScripts file to in the first solution.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"&gt;
<Module Name="MO_SiteConfigurationFile">
<File Path="MO_SiteConfigurationFile\CF.SharePoint.Configuration.Data.js" Url="Assets/Scripts/CF.SharePoint.Configuration.Data.js" ReplaceContent="TRUE"/>
</Module>
</Elements>

The custom action inserts this JavaScript into the page. Note: The Sequence number for CF.SharePoint.Configuration.Data.js is higher in value than CF.SharePoint.SiteConfiguration.Data.js meaning it is loaded after the CF.SharePoint.SiteConfiguration.Data.js file. As stated earlier this sequence number needs to be a higher value than the original configuration data script file, but lower than the actual configuration script. In our case, our configuration script is on the page, meaning it will be loaded last.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction
        ScriptSrc="~SiteCollection/assets/Scripts/CF.SharePoint.Configuration.Data.js?version=1.0.0.0"
        Location="ScriptLink"
        Sequence="12">
  </CustomAction>
</Elements>

Add both these modules to the solutions web feature. My feature title is called CFSP.AdditionalJavaScript. Package up your solution and then deploy to your site where your original solution already is.

Activate the web feature, then navigate back to the page located at [sitecollection]/DisplayJavascript.aspx.

You can now see that file files have truly been merged together. We never touched the original JavaScript code on the DisplayJavaScript.aspx page, but yet the file that had a different namespace is clearly merged in with the original configuration data. Branding now has the new values, the NonPublishingNavigation is now there, and the RequiredSharePointJSFiles have the “sp.userprofiles.js” included.

To prove that it has truly merged, using Console in Chrome, the NonPublishingNavigation is a new value that is actually part of the Namespace CF.SharePoint.SiteConfiguration.Data, but if you type in CF.SharePoint.CoreSiteConfiguration.Data, you can clearly see that NonPublishingNavigation is part of this namespace now.

Link to this solution can be found on my OneDrive

 

 

 

 

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, SharePoint | Tagged JavaScript, JQuery, Managed Metadata, SharePoint 2013, SharePoint Online

Importing Taxonomy to SharePoint using Powershell

Posted on November 29, 2014 by cann0nf0dder

In my previous post I showed you how to Export your Taxonomy from SharePoint, either on premise or Online. It was a PowerShell script that using CSOM reads in the Taxonomy and saves the information to an XML file.

However to import this XML file back into SharePoint, you will require the Import PowerShell script too. This blog post will explain the separate functions within the Import PowerShell. At the end of this blog post you can download the full PowerShell file from my OneDrive. When this PowerShell is run it will check to ensure that the Group, TermSet, Term is not already there, if it cannot find it then it will add it. This can be useful if you were trying to merge (assuming ID’s are the same), or restore terms that may have been deleted from an Export you done as a back up.

Breaking the PowerShell file down.

Parameters

There are 5 different parameters and they are all Manatory.

AdminUser

The user who has administrative access to the term store. (e.g., On-Prem: Domain\user or for 365: user@sp.com)

AdminPassword

The password for the Admin User.

AdminUrl

The URL of Central Admin for On-Prem or Admin site for 365

FilePathOfExportXMLTerms

The path of the Exported XML file, which you have created using the Export Script.

PathToSPClientdlls

The script requires to call the following dlls:
Microsoft.SharePoint.Client.dll
Microsoft.SharePoint.Client.Runtime.dll
Microsoft.SharePoint.Client.Taxonomy.dll

(e.g., C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI) Or you can download SharePoint Client SDK which will have the dll’s

Load and Connect to SharePoint Function

This function will return a SPContext, it will use CSOM to connect to the SharePoint Admin site. There is a line of code that checks if the URL contains “.sharepoint.com”, we can assume that if it does then it is an online SharePoint tenant and will require to connect using a different sign in method. The function requires the SharePoint admin URL, User, Password and path to the client dlls. This is also the same function and code used in the Export script.


function LoadAndConnectToSharePoint($url, $user, $password, $dllPath){
 #Convert password to secure string
 $securePassword = ConvertTo-SecureString $password -AsPlainText -Force
 #Get SPClient Dlls Path
 $spClientdllsDir = Get-Item $dllPath
 #Add required Client Dlls
 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.dll"
 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.Runtime.dll"
 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.Taxonomy.dll"
 $spContext = New-Object Microsoft.SharePoint.Client.ClientContext($url)

 if($url.Contains(".sharepoint.com")) # SharePoint Online
 {
&nbsp;&nbsp;&nbsp;&nbsp;$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, $securePassword)
 }
 else # SharePoint On-premises
 {
&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials = new-object -TypeName System.Net.NetworkCredential
&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.UserName = $user.Split('\')[1]
&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.Password = $password
&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.Domain = $user.Split('\')[0]

 &nbsp;&nbsp;&nbsp;[System.Net.CredentialCache]$credentials = new-object -TypeName System.Net.CredentialCache
&nbsp;&nbsp;&nbsp;&nbsp;$uri = [System.Uri]$url
&nbsp;&nbsp;&nbsp;&nbsp;$credentials.Add($uri, "NTLM", $networkCredentials)
}

#See if we can establish a connection
 $spContext.Credentials = $credentials
 $spContext.RequestTimeOut = 5000 * 60 * 10;
 $web = $spContext.Web
 $site = $spContext.Site
 $spContext.Load($web)
 $spContext.Load($site)
 try
 {
    $spContext.ExecuteQuery()
    Write-Host "Established connection to SharePoint at $Url OK" -foregroundcolor Green
 }
 catch
 {
    Write-Host "Not able to connect to SharePoint at $Url. Exception:$_.Exception.Message" -foregroundcolor red
    exit 1
 }

 return $spContext
}

Get TermStore Info

This function using the SPContext will return the TermStore. If you have multiple termstores (which you might have in your on prem environment) you might need to modify the code not to just bring back the termstore at index 0. This is also the same function and code used in the Export script.

function Get-TermStoreInfo($spContext){
 $spTaxSession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($spContext)
 $spTaxSession.UpdateCache();
 $spContext.Load($spTaxSession)

 try
 {
 $spContext.ExecuteQuery()
 }
 catch
 {
  Write-host "Error while loading the Taxonomy Session " $_.Exception.Message -ForegroundColor Red
  exit 1
 }

 if($spTaxSession.TermStores.Count -eq 0){
  write-host "The Taxonomy Service is offline or missing" -ForegroundColor Red
  exit 1
 }

 $termStores = $spTaxSession.TermStores
 $spContext.Load($termStores)

 try
 {
  $spContext.ExecuteQuery()
  $termStore = $termStores[0]
  $spcontext.Load($termStore)
  $spContext.ExecuteQuery()
  Write-Host "Connected to TermStore: $($termStore.Name) ID: $($termStore.Id)"
 }
 catch
 {
  Write-host "Error details while getting term store ID" $_.Exception.Message -ForegroundColor Red
  exit 1
 }

 return $termStore
}

Get Terms To Import

This function loads the XML file into a Linq XDocument and then returns the file. This will allow us to read in and loop through the XML.

function Get-TermsToImport($xmlTermsPath){
 [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null

 try
 {
     $xDoc = [System.Xml.Linq.XDocument]::Load($xmlTermsPath, [System.Xml.Linq.LoadOptions]::None)
     return $xDoc
 }
 catch
 {
      Write-Host "Unable to read ExportedTermsXML. Exception:$_.Exception.Message" -ForegroundColor Red
      exit 1
 }
}

Create Groups

This function get passed the SPContext, the TermStore and the XML file. From the XML file it grab all Group nodes and iterates over them, by grabbing relevant information such as Name, Description, ID. Once it has the ID it calls the SharePoint TermStore you are connecting to, and attempts to get the Group by ID. If nothing is returned then it means the group doesn’t exist and the group is added to the TermStore. If it already exist it moves directly onto creating the TermSets for the group.

function Create-Groups($spContext, $termStore, $termsXML){
     foreach($groupNode in $termsXML.Descendants("Group"))
     {
        $name = $groupNode.Attribute("Name").Value
        $description = $groupNode.Attribute("Description").Value;
        $groupId = $groupNode.Attribute("Id").Value;
        $groupGuid = [System.Guid]::Parse($groupId);
        Write-Host "Processing Group: $name ID: $groupId ..." -NoNewline

        $group = $termStore.GetGroup($groupGuid);
        $spContext.Load($group);

        try
        {
            $spContext.ExecuteQuery();
        }
        catch
        {
            Write-host "Error while finding if " $name " group already exists. " $_.Exception.Message -ForegroundColor Red
            exit 1
        }

&nbsp;&nbsp;&nbsp;&nbsp;    if ($group.ServerObjectIsNull) {
            $group = $termStore.CreateGroup($name, $groupGuid);
            $spContext.Load($group);
            try
            {
                $spContext.ExecuteQuery();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;        write-host "Inserted" -ForegroundColor Green
            }
            catch
            {
                Write-host "Error creating new Group " $name " " $_.Exception.Message -ForegroundColor Red
                exit 1
            }
        }
&nbsp;&nbsp;&nbsp;&nbsp;    else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;    write-host "Already exists" -ForegroundColor Yellow
&nbsp;&nbsp;&nbsp;&nbsp;    }

&nbsp;&nbsp;&nbsp;&nbsp;    Create-TermSets $termsXML $group $termStore $spContext

     }

     try
     {
         $termStore.CommitAll();
         $spContext.ExecuteQuery();
     }
     catch
     {
       Write-Host "Error commiting changes to server. Exception:$_.Exception.Message" -foregroundcolor red
       exit 1
     }
}

Create TermSets

This function is called from within the Create Groups function. The XML file, current Group, TermStore and SharePoint Context is passed into the function. Firstly from the XML file we grab all TermSets that are related to the current Group. Then we iterate over these TermSets. For the given TermSet we grab all the information in the XML, and then try and get the TermSet using the ID from the TermStore. As with the groups function, if the TermSet doesn’t exists, then the TermSet is created. If when the TermSet is being created an error occurs, a flag is marked so that we can inform the user that an error occurred, but not affect the running and importing of the rest of the XML. If the TermSet does exist then it skips the creation, and checks to see if there are any Terms and call the Create-Term function.

function Create-TermSets($termsXML, $group, $termStore, $spContext) {

    $termSets = $termsXML.Descendants("TermSet") | Where { $_.Parent.Parent.Attribute("Name").Value -eq $group.Name }

&nbsp;&nbsp;&nbsp;&nbsp;foreach ($termSetNode in $termSets)
    {
        $errorOccurred = $false
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$name = $termSetNode.Attribute("Name").Value;
        $id = [System.Guid]::Parse($termSetNode.Attribute("Id").Value);
        $description = $termSetNode.Attribute("Description").Value;
        $customSortOrder = $termSetNode.Attribute("CustomSortOrder").Value;
        Write-host "Processing TermSet $name ... " -NoNewLine

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$termSet = $termStore.GetTermSet($id);
        $spcontext.Load($termSet);

        try
        {
            $spContext.ExecuteQuery();
        }
        catch
        {
            Write-host "Error while finding if " $name " termset already exists. " $_.Exception.Message -ForegroundColor Red
            exit 1
        }

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($termSet.ServerObjectIsNull)
        {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$termSet = $group.CreateTermSet($name, $id, $termStore.DefaultLanguage);
            $termSet.Description = $description;

            if($customSortOrder -ne $null)
            {
                $termSet.CustomSortOrder = $customSortOrder
            }

           $termSet.IsAvailableForTagging = [bool]::Parse($termSetNode.Attribute("IsAvailableForTagging").Value);
           $termSet.IsOpenForTermCreation = [bool]::Parse($termSetNode.Attribute("IsOpenForTermCreation").Value);

            if($termSetNode.Element("CustomProperties") -ne $null)
            {
                foreach($custProp in $termSetNode.Element("CustomProperties").Elements("CustomProperty"))
                {
                   $termSet.SetCustomProperty($custProp.Attribute("Key").Value, $custProp.Attribute("Value").Value)
                }
            }

           try
            {
                $spContext.ExecuteQuery();
            }
            catch
            {
                Write-host "Error occured while create Term Set" $name $_.Exception.Message -ForegroundColor Red
                $errorOccurred = $true
            }

            write-host "created" -ForegroundColor Green
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;write-host "Already exists" -ForegroundColor Yellow
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}

        if(!$errorOccurred)
        {
            if ($termSetNode.Element("Terms") -ne $null)
            {
              foreach ($termNode in $termSetNode.Element("Terms").Elements("Term"))
               {
                  Create-Term $termNode $null $termSet $termStore $termStore.DefaultLanguage $spContext
               }
            }
        }
    }
}

Create Term

The Create Term can be called from Create Terms or recursively from itself. It is passed the XML Term, the parent term (which is used for recursive calls, otherwise null), the TermSet that the Term is from, the TermStore, language of the Term and the SPContext. This script just uses your TermStore default language, but with some small code change (I’ll leave that for you) you can allow it to use any language. This function reads in the Terms XML values, then tries and get the Term by ID from the TermStore. If the Term doesn’t exist it will process the term and add it to SharePoint either to the TermSet, or to another Term as a SubTerm. If the Term already exists then it will skip the creation and check to see if it has any subterms in the XML and recursively call itself, otherwise it will return up the stack for the next Term/TermSet/Group.


function Create-Term($termNode, $parentTerm, $termSet, $store, $lcid, $spContext){
    $id = [System.Guid]::Parse($termNode.Attribute("Id").Value)
    $name = $termNode.Attribute("Name").Value;
    $term = $termSet.GetTerm($id);
    $errorOccurred = $false

    $spContext.Load($term);
    try
    {
        $spContext.ExecuteQuery();
    }
    catch
    {
        Write-host "Error while finding if " $name " term id already exists. " $_.Exception.Message -ForegroundColor Red
        exit 1
    }

     write-host "Processing Term $name ..." -NoNewLine
    if($term.ServerObjectIsNull)
    {
&nbsp;&nbsp;&nbsp;&nbsp;    if ($parentTerm -ne $null)
        {
            $term = $parentTerm.CreateTerm($name, $lcid, $id);
        }
        else
        {
           $term = $termSet.CreateTerm($name, $lcid, $id);
        }

        $customSortOrder = $termNode.Attribute("CustomSortOrder").Value;
        $description = $termNode.Element("Descriptions").Element("Description").Attribute("Value").Value;
        $term.SetDescription($description, $lcid);
        $term.IsAvailableForTagging = [bool]::Parse($termNode.Attribute("IsAvailableForTagging").Value);

        if($customSortOrder -ne $null)
        {
            $term.CustomSortOrder = $customSortOrder
        }

        if($termNode.Element("CustomProperties") -ne $null)
        {
            foreach($custProp in $termNode.Element("CustomProperties").Elements("CustomProperty"))
            {
                $term.SetCustomProperty($custProp.Attribute("Key").Value, $custProp.Attribute("Value").Value)
            }
        }

        if($termNode.Element("LocalCustomProperties") -ne $null)
        {
            foreach($localCustProp in $termNode.Element("LocalCustomProperties").Elements("LocalCustomProperty"))
           {
               $term.SetLocalCustomProperty($localCustProp.Attribute("Key").Value, $localCustProp.Attribute("Value").Value)
           }
        }

       try
        {
            $spContext.Load($term);
            $spContext.ExecuteQuery();
&nbsp;&nbsp;&nbsp;&nbsp;        write-host " created" -ForegroundColor Green
&nbsp;&nbsp;&nbsp;&nbsp;    }
        catch
        {
            Write-host "Error occured while create Term" $name $_.Exception.Message -ForegroundColor Red
            $errorOccurred = $true
        }
    }
    else
    {
     write-host "Already exists" -ForegroundColor Yellow
    }

   if(!$errorOccurred)
    {
&nbsp;&nbsp;&nbsp;&nbsp;    if ($termNode.Element("Terms") -ne $null)
        {
            foreach ($childTermNode in $termNode.Element("Terms").Elements("Term"))
            {
                Create-Term $childTermNode $term $termSet $store $lcid $spContext
            }
        }
    }
}

How to Import XML into SharePoint with the PowerShell command.

This example use a SharePoint online account/site, but you can easily point it to an on premise site. To view all details of the PowerShell file has a help file which can be called ./Import-Taxonomy.ps1 – help. This will import the entire XML into your given SharePoint site, it will add any information that is missing from the given TermStore.

  ./Import-Taxonomy.ps1 -AdminUser user@sp.com -AdminPassword password -AdminUrl https://sp-admin.onmicrosoft.com -FilePathOfExportXMLTerms c:\myTerms\exportedterms.xml -PathToSPClientdlls "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI"

Screenshot as Importing to SharePoint Online.

Screen shot of Terms in my On Premise environment.

Screen shot of the same terms imported into my SharePoint Online environment.

Link to the PowerShell file can be found on my One Drive.

Link to blog about Exporting Taxonomy from SharePoint.

Again, lastly I would like to thank tow of my colleagues who without their initial work I wouldn’t have been able to create this final PS1 file. Kevin Beckett and Luis Manez (http://geeks.ms/blog/imanez)

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, PowerShell, SharePoint | Tagged Managed Metadata, PowerShell, SharePoint 2013, SharePoint Online

Exporting Taxonomy from SharePoint using Powershell

Posted on November 12, 2014 by cann0nf0dder

In a previous post I showed you how to connect your Site Managed Metadata Column to your Term Set in your Taxonomy using JavaScript. The code I put in that blog relies on Taxonomy Term Set ID to be hard coded in the JavaScript. This is not really ideal if you are going through multiple environments. One solution to this was to change my JavaScript code to make additional calls to find the TermSet name instead of using the ID. Even if you did do this, it is most likely your term store is quite large, and re-typing from one environment to another could take some time. Especially if you have a Store with multiple groups, TermSets, Terms and sub terms.

Above is a small example from my Developer machine of a Group and TermSets. Countries alone has around 240 terms. Think you will agree not easy to type out again in another environment. (Dev, Test, Staging, Production).

There are tools currently out there, and a very good tool is by Gary Lapointe, which can be downloaded under the header ‘SharePoint 2013 STSADM Commands and PowerShell Cmdlets WSP Files’ http://blog.falchionconsulting.com/index.php/downloads/ . With Gary Lapointe code, you first need to install the WSP onto the SharePoint server you wish to Export/Import to, then using powershell you can export to XML, and then use that XML to re-import back to another SharePoint environment with Gary Lapointe solution also installed on.

Import-SPTerms – http://www.falchionconsulting.com/PowerShellViewer/Default.aspx?Version=SP2013&Cmdlet=Import-SPTerms

Export-SPTerms – http://www.falchionconsulting.com/PowerShellViewer/Default.aspx?Version=SP2013&Cmdlet=Export-SPTerms

As I said this is a very good tool, and the XML gave me a good jumping off point, however it has some shortcomings for my requirements, and these are the main two that meant I had to create my own tool.

  • It On Prem Only, no good to export/import to SharePoint Online
  • When using the term store for SharePoint navigation, the additional information were not exported.

Using the similar XML design of what Gary Lapointe created, I created a PowerShell solution that can export an entire TermStore or a given Term Group. I will explain the separate functions within the powershell to you. At the end of this blog post you can download the full powershell file from my onedrive.

Breaking the PowerShell file down

Parameters

There are 7 different parameters and 6 of them are Manatory.

AdminUser

The user who has adminitrative access to the term store. (e.g., On-Prem: Domain\user or for 365: user@sp.com)

AdminPassword

The password for the Admin User.

AdminUrl

The URL of Central Admin for On-Prem or Admin site for 365

PathToExportXMLTerms

The path you wish to save the XML Output to. This path must exist.

XMLTermsFileName

The name of the XML file to save. If the file already exists then it will be overwritten.

PathToSPClientdlls

The script requires to call the following dlls:
Microsoft.SharePoint.Client.dll
Microsoft.SharePoint.Client.Runtime.dll
Microsoft.SharePoint.Client.Taxonomy.dll

(e.g., C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI) Or you can download SharePoint Client SDK which will have the dll’s

GroupToExport

An optional parameter, if included only the Group will be exported. If omitted then the entire termstore will be written to XML.

Load and Connect to SharePoint Function

This function will return an SPContext, it will use CSOM to connect to the SharePoint Admin site. There is a line of code that checks if the URL contains “.sharepoint.com”, we can assume that if it does then it is an online SharePoint tenant and will require to connect using a different sign in method. The function requires the SharePoint admin URL, User, Password and path to the client dlls.


function LoadAndConnectToSharePoint($url, $user, $password, $dllPath){

 #Convert password to secure string

 $securePassword = ConvertTo-SecureString $password -AsPlainText -Force

 #Get SPClient Dlls Path

 $spClientdllsDir = Get-Item $dllPath

 #Add required Client Dlls

 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.dll"

 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.Runtime.dll"

 Add-Type -Path "$($spClientdllsDir.FullName)\Microsoft.SharePoint.Client.Taxonomy.dll"

 $spContext = New-Object Microsoft.SharePoint.Client.ClientContext($url)

 if($url.Contains(".sharepoint.com")) # SharePoint Online

 {&nbsp;&nbsp;&nbsp;&nbsp;

&nbsp;&nbsp;&nbsp;&nbsp;$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($User, $securePassword)

 }

 else # SharePoint On-premises

 {&nbsp;&nbsp;&nbsp;&nbsp;

&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials = new-object -TypeName System.Net.NetworkCredential

&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.UserName = $user.Split('\')[1]

&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.Password = $password

&nbsp;&nbsp;&nbsp;&nbsp;$networkCredentials.Domain = $user.Split('\')[0]

&nbsp;&nbsp;&nbsp;&nbsp;[System.Net.CredentialCache]$credentials = new-object -TypeName System.Net.CredentialCache

&nbsp;&nbsp;&nbsp;&nbsp;$uri = [System.Uri]$url

&nbsp;&nbsp;&nbsp;&nbsp;$credentials.Add($uri, "NTLM", $networkCredentials)

}

 #See if we can establish a connection

 $spContext.Credentials = $credentials

 $spContext.RequestTimeOut = 5000 * 60 * 10;

 $web = $spContext.Web

 $site = $spContext.Site

 $spContext.Load($web)

 $spContext.Load($site)

 try

 {

    $spContext.ExecuteQuery()

    Write-Host "Established connection to SharePoint at $Url OK" -foregroundcolor Green

}

catch

{

    Write-Host "Not able to connect to SharePoint at $Url. Exception:$_.Exception.Message" -foregroundcolor red

    exit 1

}

 return $spContext

}

Get TermStore Info

This function using the SPContext will return the TermStore. If you have multiple termstores (which you might have in your on prem environment) you might need to modify the code not to just bring back the termstore at index 0.

function Get-TermStoreInfo($spContext){
 $spTaxSession = [Microsoft.SharePoint.Client.Taxonomy.TaxonomySession]::GetTaxonomySession($spContext)
 $spTaxSession.UpdateCache();
 $spContext.Load($spTaxSession)

 try
 {
 $spContext.ExecuteQuery()
 }
 catch
 {
  Write-host "Error while loading the Taxonomy Session " $_.Exception.Message -ForegroundColor Red
  exit 1
 }

 if($spTaxSession.TermStores.Count -eq 0){
  write-host "The Taxonomy Service is offline or missing" -ForegroundColor Red
  exit 1
 }

 $termStores = $spTaxSession.TermStores
 $spContext.Load($termStores)

 try
 {
  $spContext.ExecuteQuery()
  $termStore = $termStores[0]
  $spcontext.Load($termStore)
  $spContext.ExecuteQuery()
  Write-Host "Connected to TermStore: $($termStore.Name) ID: $($termStore.Id)"
 }
 catch
 {
  Write-host "Error details while getting term store ID" $_.Exception.Message -ForegroundColor Red
  exit 1
 }
 return $termStore
}

Get XML TermStore Template To File

This creates an empty XML template, very similar to Gary Lapointe XML file. I’m passing in the Term Store Name and the path to where I want to save the .XML file. The code creates a template.xml file which is a temporary file. This blank template is used later on. It should contain all the relevant bits of information about the term store. Through XPath, which you will see later, we can recursively node sections (e.g, Custom Properties, Terms, TermSets, Groups etc.)

function Get-XMLTermStoreTemplateToFile($termStoreName, $path){
 ## Set up an xml template used for creating your exported xml
 $xmlTemplate = '&lt;TermStores&gt;
 &lt;TermStore Name="' + $termStoreName + '" IsOnline="True" WorkingLanguage="1033" DefaultLanguage="1033" SystemGroup="c6fb3e37-0997-42b1-8e3c-2706a36adbc4"&gt;
 &lt;Groups&gt;
 &lt;Group Id="" Name="" Description="" IsSystemGroup="False" IsSiteCollectionGroup="False"&gt;
 &lt;TermSets&gt;
 &lt;TermSet Id="" Name="" Description="" Contact="" IsAvailableForTagging="" IsOpenForTermCreation="" CustomSortOrder="False"&gt;
 &lt;CustomProperties&gt;
 &lt;CustomProperty Key="" Value=""/&gt;
 &lt;/CustomProperties&gt;
 &lt;Terms&gt;
 &lt;Term Id="" Name="" IsDeprecated="" IsAvailableForTagging="" IsKeyword="" IsReused="" IsRoot="" IsSourceTerm="" CustomSortOrder="False"&gt;
 &lt;Descriptions&gt;
 &lt;Description Language="1033" Value="" /&gt;
 &lt;/Descriptions&gt;
 &lt;CustomProperties&gt;
 &lt;CustomProperty Key="" Value="" /&gt;
 &lt;/CustomProperties&gt;
 &lt;LocalCustomProperties&gt;
 &lt;LocalCustomProperty Key="" Value="" /&gt;
 &lt;/LocalCustomProperties&gt;
 &lt;Labels&gt;
 &lt;Label Value="" Language="1033" IsDefaultForLanguage="" /&gt;
 &lt;/Labels&gt;
 &lt;Terms&gt;
 &lt;Term Id="" Name="" IsDeprecated="" IsAvailableForTagging="" IsKeyword="" IsReused="" IsRoot="" IsSourceTerm="" CustomSortOrder="False"&gt;
 &lt;Descriptions&gt;
 &lt;Description Language="1033" Value="" /&gt;
 &lt;/Descriptions&gt;
 &lt;CustomProperties&gt;
 &lt;CustomProperty Key="" Value="" /&gt;
 &lt;/CustomProperties&gt;
 &lt;LocalCustomProperties&gt;
 &lt;LocalCustomProperty Key="" Value="" /&gt;
 &lt;/LocalCustomProperties&gt;
 &lt;Labels&gt;
 &lt;Label Value="" Language="1033" IsDefaultForLanguage="" /&gt;
 &lt;/Labels&gt;
 &lt;/Term&gt;
 &lt;/Terms&gt;
 &lt;/Term&gt;
 &lt;/Terms&gt;
 &lt;/TermSet&gt;
 &lt;/TermSets&gt;
 &lt;/Group&gt;
 &lt;/Groups&gt;
 &lt;/TermStore&gt;
 &lt;/TermStores&gt;'

try
{
 #Save Template to disk
 $xmlTemplate | Out-File($path + "\Template.xml")

 #Load file and return
 $xml = New-Object XML
 $xml.Load($path + "\Template.xml")
 return $xml
 }
 catch{
 Write-host "Error creating Template file. " $_.Exception.Message -ForegroundColor Red
 exit 1
 }
}
 

Get XML File Object Templates

Using Global variables, the template XML file is being broken down into sections, so at the given time when I’m exporting the TermSet I can grab a node, fill it in, and then add it to the final XML file. Using XPath I’m grabbing, Group, TermSet, Term, Term Label, Term Description, Term Custom Properties and Tern Local Custom properties.

function Get-XMLFileObjectTemplates($xml){
 #Grab template elements so that we can easily copy them later.
 &nbsp; $global:xmlGroupT = $xml.selectSingleNode('//Group[@Id=""]')
&nbsp;&nbsp;&nbsp;&nbsp;$global:xmlTermSetT = $xml.selectSingleNode('//TermSet[@Id=""]')
&nbsp;&nbsp;&nbsp;&nbsp;$global:xmlTermT = $xml.selectSingleNode('//Term[@Id=""]')
 $global:xmlTermLabelT = $xml.selectSingleNode('//Label[@Value=""]')
 $global:xmlTermDescriptionT = $xml.selectSingleNode('//Description[@Value=""]')
 $global:xmlTermCustomPropertiesT = $xml.selectSingleNode('//CustomProperty[@Key=""]')
 $global:xmlTermLocalCustomPropertiesT = $xml.selectSingleNode('//LocalCustomProperty[@Key=""]')
}

Get Groups

This function will be called from the actual Export Taxonomy Function. A call would have already been made to obtain all Groups within the TermStore and passed into this function. It will loop through each Group, ignoring system groups, and if the parameter $GroupToExport was supplied it will skip all groups until it finds the given group. Once found it will grab a clone of GroupXMLTemplate and fill out the required information, then add the clone to the template XML in the correct place. It will then try to load any TermSets for the group.

function Get-Groups($spContext, $groups, $xml, $groupToExport){
 #Loop through all groups, ignoring system Groups
 $groups | Where-Object { $_.IsSystemGroup -eq $false} | ForEach-Object{

 #Check if we are getting groups or just group.
 if($groupToExport -ne "")
 {
 if($groupToExport -ne $_.name){
 #Return acts like a continue in ForEach-Object
 return;
 }
 }

 #Add each group to export xml by cloning the template group,
 #populating it and appending it
 $xmlNewGroup = $global:xmlGroupT.Clone()
 $xmlNewGroup.Name = $_.name
 $xmlNewGroup.id = $_.id.ToString()
 $xmlNewGroup.Description = $_.description
 $xml.TermStores.TermStore.Groups.AppendChild($xmlNewGroup) | Out-Null

 write-Host "Adding Group " -NoNewline
 write-Host $_.name -ForegroundColor Green

 $spContext.Load($_.TermSets)
 try
 {
 $spContext.ExecuteQuery()
 }
 catch
 {
 Write-host "Error while loaded TermSets for Group " $xmlNewGroup.Name " " $_.Exception.Message -ForegroundColor Red
 exit 1
 }

 Get-TermSets $spContext $xmlNewGroup $_.Termsets $xml
 }
}

Get Term Sets

This function is called from Get-Groups Function. A call was made in Get-Groups to obtain all TermSets within the Group and passed into this function. It will loop through each TermSet, grab a clone of TermSetXMLTemplate and fill out the required information, skipping CustomSortOrder and Custom properties if they do not exist, or grabbing a clone of their template and filling in the information. Then add the clone to the template XML in the correct place. It will then try to load any Terms for the termset.

You will notice on the 6th line down I am replacing an ampersand with another. This is because the one that SharePoint is a different character to ampersand you get when you press & on your keyboard. This just tidies the data in the XML and prevents problems with string matching in importing.

function Get-TermSets($spContext, $xmlnewGroup, $termSets, $xml){
 $termSets | ForEach-Object{
 #Add each termset to the export xml
 $xmlNewSet = $global:xmlTermSetT.Clone()
 #Replace SharePoint ampersand with regular
 $xmlNewSet.Name = $_.Name.replace("&", "&amp;")

 $xmlNewSet.Id = $_.Id.ToString()

 if ($_.CustomSortOrder -ne $null)
 {
 $xmlNewSet.CustomSortOrder = $_.CustomSortOrder.ToString()
 }

 foreach($customprop in $_.CustomProperties.GetEnumerator())
 {
 ## Clone Term customProp node
 $xmlNewTermCustomProp = $global:xmlTermCustomPropertiesT.Clone()&nbsp;&nbsp;&nbsp;&nbsp;

 $xmlNewTermCustomProp.Key = $($customProp.Key)
 $xmlNewTermCustomProp.Value = $($customProp.Value)
 $xmlNewSet.CustomProperties.AppendChild($xmlNewTermCustomProp)&nbsp;| Out-Null
 }

 $xmlNewSet.Description = $_.Description.ToString()
 $xmlNewSet.Contact = $_.Contact.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;$xmlNewSet.IsOpenForTermCreation = $_.IsOpenForTermCreation.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;$xmlNewSet.IsAvailableForTagging = $_.IsAvailableForTagging.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;$xmlNewGroup.TermSets.AppendChild($xmlNewSet) | Out-Null

 Write-Host "Adding TermSet " -NoNewline
 Write-Host $_.name -ForegroundColor Green -NoNewline
 Write-Host " to Group " -NoNewline
 Write-Host $xmlNewGroup.Name -ForegroundColor Green

 $spContext.Load($_.Terms)
 try
 {
 $spContext.ExecuteQuery()
 }
 catch
 {
 Write-host "Error while loading Terms for TermSet " $_.name " " $_.Exception.Message -ForegroundColor Red
 exit 1
 }
 # Recursively loop through all the terms in this termset
&nbsp;&nbsp;&nbsp;&nbsp;Get-Terms $spContext $_.Terms $xml
 }

Get Terms

This function is called from Get-TermSets Function. A call was made in Get-TermSets (Or Get-Terms if collection is a sub terms) to obtain all Terms within the TermSet and passed into this function. It will loop through each Terms, grab a clone of TermXMLTemplate and fill out the required information. A Term has lots of repeating data for it, such as Custom Properties, Local Properties etc, which uses given XMLTemplates to store information then append to the XML term. Lastly Labels, TermSets information, Parent information and subTerms require an additional context load. The parent and TermSet information is used to ensure if sub terms, they are added to the correct location within the XML Template. The loading of the Terms (sub terms) for this current term is then passed back into Get-TermSet function, recursively calling itself until no sub terms can be found. *Hopefully the code is more explanatory to you then my description here.

#Terms could be either the original termset or parent term with children terms
 $terms | ForEach-Object{
 #Create a new term xml Element
 $xmlNewTerm = $global:xmlTermT.Clone()
 #Replace SharePoint ampersand with regular
 $xmlNewTerm.Name = $_.Name.replace("&", "&amp;")
 $xmlNewTerm.id = $_.Id.ToString()
 $xmlNewTerm.IsAvailableForTagging = $_.IsAvailableForTagging.ToString()
 $xmlNewTerm.IsKeyword = $_.IsKeyword.ToString()
 $xmlNewTerm.IsReused = $_.IsReused.ToString()
 $xmlNewTerm.IsRoot = $_.IsRoot.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;$xmlNewTerm.IsSourceTerm = $_.IsSourceterm.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;$xmlNewTerm.IsDeprecated = $_.IsDeprecated.ToString()

 if($_.CustomSortOrder -ne $null)
 {
 $xmlNewTerm.CustomSortOrder = $_.CustomSortOrder.ToString()
 }

 #Custom Properties
 foreach($customprop in $_.CustomProperties.GetEnumerator())
 {
 # Clone Term customProp node
 $xmlNewTermCustomProp = $global:xmlTermCustomPropertiesT.Clone()&nbsp;&nbsp;&nbsp;&nbsp;

 $xmlNewTermCustomProp.Key = $($customProp.Key)
 $xmlNewTermCustomProp.Value = $($customProp.Value)
 $xmlNewTerm.CustomProperties.AppendChild($xmlNewTermCustomProp)&nbsp; | Out-Null
 }

 #Local Properties
 foreach($localProp in $_.LocalCustomProperties.GetEnumerator())
 {
 # Clone Term LocalProp node
 $xmlNewTermLocalCustomProp = $global:xmlTermLocalCustomPropertiesT.Clone()&nbsp;&nbsp;&nbsp;&nbsp;

 $xmlNewTermLocalCustomProp.Key = $($localProp.Key)
 $xmlNewTermLocalCustomProp.Value = $($localProp.Value)
 $xmlNewTerm.LocalCustomProperties.AppendChild($xmlNewTermLocalCustomProp)&nbsp;| Out-Null
 }

 if($_.Description -ne ""){
 $xmlNewTermDescription = $global:xmlTermDescriptionT.Clone()
 $xmlNewTermDescription.Value = $_.Description
 $xmlNewTerm.Descriptions.AppendChild($xmlNewTermDescription) |Out-Null
 }

 $spContext.Load($_.Labels)
 $spContext.Load($_.TermSet)
 $spContext.Load($_.Parent)
 $spContext.Load($_.Terms)

 try
 {
 $spContext.ExecuteQuery()
 }
 catch
 {
 Write-host "Error while loaded addition information for Term " $xmlNewTerm.Name " " $_.Exception.Message -ForegroundColor Red
 exit 1
 }

 foreach($label in $_.Labels)
 {
 ## Clone Term Label node
 $xmlNewTermLabel = $global:xmlTermLabelT.Clone()
 $xmlNewTermLabel.Value = $label.Value.ToString()
 $xmlNewTermLabel.Language = $label.Language.ToString()
 $xmlNewTermLabel.IsDefaultForLanguage = $label.IsDefaultForLanguage.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; $xmlNewTerm.Labels.AppendChild($xmlNewTermLabel) | Out-Null
 }

 # Use this terms parent term or parent termset in the termstore to find it's matching parent
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# in the export xml
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($_.parent.Id -ne $null) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; # Both guids are needed as a term can appear in multiple termsets
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parentGuid =&nbsp;$_.parent.Id.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parentTermsetGuid =&nbsp;$_.Termset.Id.ToString()
 #$_.Parent.Termset.Id.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
 else
 {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; $parentGuid =&nbsp;$_.Termset.Id.ToString()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

 # Get this terms parent in the xml
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent = Get-TermByGuid $xml&nbsp;$parentGuid $parentTermsetGuid&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

 $parentGuid = $null;

 #Append new Term to Parent
 $parent.Terms.AppendChild($xmlNewTerm) | Out-Null

 Write-Host "Adding Term " -NoNewline
 Write-Host $_.name -ForegroundColor Green -NoNewline
 Write-Host " to Parent " -NoNewline
 Write-Host $parent.Name -ForegroundColor Green

 #If this term has child terms we need to loop through those
 if($_.Terms.Count -gt 0){
 #Recursively call itself
 Get-Terms $spContext $_.Terms $xml
 }
 }
}

Get Terms By Guid

This function is used with Get-Terms. All it is doing is returning the corresponding selected nodes from the XML template, passing in either the TermGuid, or Parent TermSet Guid.

function Get-TermByGuid($xml, $guid, $parentTermsetGuid) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if ($parentTermsetGuid) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp; $xml.selectnodes('//Term[@Id="' + $guid + '"]')
&nbsp;&nbsp;&nbsp;&nbsp;} else {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return&nbsp; $xml.selectnodes('//TermSet[@Id="' + $guid + '"]')
&nbsp;&nbsp;&nbsp;&nbsp;}
}

Clean Template

The Clean template function is called just before saving the completed XML to disk. This ensures any empty elements are removed from the XML.

function Clean-Template($xml) {
    #Do not cleanup empty description nodes (this is the default state)

 ## Empty Term.Labels.Label
&nbsp;&nbsp;&nbsp;&nbsp;$xml.selectnodes('//Label[@Value=""]') | ForEach-Object {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent = $_.get_ParentNode()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent.RemoveChild($_)&nbsp;&nbsp;| Out-Null
&nbsp;&nbsp;&nbsp;&nbsp;}
 ## Empty Term
 $xml.selectnodes('//Term[@Id=""]') | ForEach-Object {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent = $_.get_ParentNode()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent.RemoveChild($_)&nbsp;&nbsp;| Out-Null
&nbsp;&nbsp;&nbsp;&nbsp;}
 ## Empty TermSet
&nbsp;&nbsp;&nbsp;&nbsp;$xml.selectnodes('//TermSet[@Id=""]') | ForEach-Object {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent = $_.get_ParentNode()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent.RemoveChild($_)&nbsp;&nbsp;| Out-Null
&nbsp;&nbsp;&nbsp;&nbsp;}
 ## Empty Group
&nbsp;&nbsp;&nbsp;&nbsp;$xml.selectnodes('//Group[@Id=""]') | ForEach-Object {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent = $_.get_ParentNode()
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;$parent.RemoveChild($_)&nbsp;&nbsp;&nbsp;| Out-Null
&nbsp;&nbsp;&nbsp;&nbsp;}
 ## Empty Custom Properties
 $xml.selectnodes('//CustomProperty[@Key=""]') | ForEach-Object {
 $parent = $_.get_ParentNode()
 $parent.RemoveChild($_) | Out-Null
 }

 ## Empty Local Custom proeprties
 $xml.selectnodes('//LocalCustomProperty[@Key=""]') | ForEach-Object {
 $parent = $_.get_ParentNode()
 $parent.RemoveChild($_) | Out-Null
 }

 $xml.selectnodes('//Descriptions')| ForEach-Object {
 $childNodes = $_.ChildNodes.Count
 if($childNodes -gt 1)
 {
 $_.RemoveChild($_.ChildNodes[0]) | Out-Null
 }
 }

 While ($xml.selectnodes('//Term[@Id=""]').Count -gt 0)
 {
 #Cleanup the XML, remove empty Term Nodes
 $xml.selectnodes('//Term[@Id=""]').RemoveAll() | Out-Null
 }

Export Taxonomy

This function ties all the other functions together really. It starts at calling the groups, then once that has pushed down and iterated through all the other methods, the XML is tidied, then saved to the filename given in the parameters. All temporary file are deleted.

function ExportTaxonomy($spContext, $termStore, $xml, $groupToExport, $path, $saveFileName){
 $spContext.Load($termStore.Groups)
 try
 {
 $spContext.ExecuteQuery();
 }
 catch
 {
 Write-host "Error while loaded Groups from TermStore " $_.Exception.Message -ForegroundColor Red
 exit 1
 }

 Get-Groups $spContext $termStore.Groups $xml $groupToExport

 #Clean up empty tags/nodes
 Clean-Template $xml

 #Save file.
 try
 {
 $xml.Save($path + "\NewTaxonomy.xml")

 #Clean up empty &lt;Term&gt; unable to work out in Clean-Template.
 Get-Content ($path + "\NewTaxonomy.xml") | Foreach-Object { $_ -replace "&lt;Term&gt;&lt;\/Term&gt;", "" } | Set-Content ($path + "\" + $saveFileName)
 Write-Host "Saving XML file " $saveFileName " to " $path

 #Remove temp file
 Remove-Item($path + "\Template.xml");
 Remove-Item($path + "\NewTaxonomy.xml");
 }
 catch
 {
 Write-host "Error saving XML File to disk " $_.Exception.Message -ForegroundColor Red
 exit 1
 }
}

How to Export TermSet with the PowerShell command.

Both these examples use a SharePoint online account/site, but you can easily point it to an on premise site. To view all details the powershell file has a help file which can be called ./Export-Taxonomy.ps1 -help

This exports the entire termstore.

    ./Export-Taxonomy.ps1 -AdminUser user@sp.com -AdminPassword password -AdminUrl https://sp-admin.onmicrosoft.com -PathToExportXMLTerms c:\myTerms -XMLTermsFileName exportterms.xml -PathToSPClientdlls &amp;quot;C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI&amp;quot;

This exports just the Term Store Group ‘Client Group Terms’

    ./Export-Taxonomy.ps1 -AdminUser user@sp.com -AdminPassword password -AdminUrl https://sp-admin.onmicrosoft.com -PathToExportXMLTerms c:\myTerms -XMLTermsFileName exportterms.xml -PathToSPClientdlls &amp;quot;C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI&amp;quot; -GroupToExport 'Client Group Terms'

When running the script, (as you can see below) the progress of the exporting.

Powershell Export

Link to the powershell file can be found on my One Drive

In my next blog I’ll explain about Importing Taxonomy into SharePoint using SharePoint.

Lastly I would like to thank two of my colleagues who without their initial work I wouldn’t have been able to create this final PS1 file. Kevin Beckett (Has no public blog) and Luis Manez (http://geeks.ms/blog/lmanez)

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, PowerShell, SharePoint | Tagged Managed Metadata, PowerShell, SharePoint 2013, SharePoint Online

Creating a Site Column with Managed Metadata using JavaScript

Posted on October 24, 2014 by cann0nf0dder

A while ago now, and one of my popular blog posts I created a post called Creating a Site Column with Managed Metadata, this showed you how to create a Managed Metadata Site Column and tie it up to a Term Set. With the use of feature activate events hooking up the site column the Term Set was easily achieved.

With SharePoint Online, using a Sandbox solution we are unable to use any C# code. Therefore we are left to use JavaScript to tie up the column and the TermSet. In this blog post I will walk through creating a Managed Metadata column and using a custom configuration page to get the Managed Metadata column working.

First thing you will need to do, is ensure you have a term set created in your environment. As you can see below I have created a group called Cannonfodder with a term set call Countries. Take note of the Unique Identifier of your term set you will require this later on in the code. You might not want to hard code this value into your code, which is understandable if you are going from one environment to another. You could just use the name of your term set, but if you have the same name in different groups you might encounter unexpected results. A solution to this issue would be to ensure you export and import Taxonomy Term Store Groups from one environment to another, as this would retain the Unique Identifier. My next blog post is about this.


Here we are going to walk through creating a Sandbox solution, creating a Managed Metadata column via a feature, then we are going to build upon it to create a list definition and list instance.

  • Open Visual Studio, and create a new Empty SharePoint Project Name the project ManagedMetadataSB.
  • Use your SharePoint Site for debugging, I’m using the Development Site in 365, and Deploy as a Sandbox Solution.

     

Creating Site Column.

  • In Solution Explorer right click your project and select Add -> New Item.
  • Add a Site Column. I’ve named my Column CO_Column. This will also create a Feature for you.
  • Open the Elements.xml file and add the following to create 2 fields. The first field is of type Note field, this is a hidden field that contains a reference to the actual term in the term set that was selected, and this is required for each Managed Metadata Column. You must always put the note field first. The reason behind this is, when SharePoint reads in the XML, if it reads the Taxonomy Field first, it will automatically create the notes field for you, which you will not know the GUID, or have any control over the internal name etc. The second field is of type TaxonomyFieldType is the actual field we are trying to create (TaxonomyFieldTypeMulti for multiple selection, don’t forget to add Multi = True to the element field too). The Show Field will always point to Term1033 for any Managed Metadata Column. You will need to also include the Customization section. This will be identical for every Managed Metadata Column you create, except the Guid which is the same GUID ID of the Note Hidden Field ID.

     

    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
     <Field
     ID="{8228CEF8-3021-4CD9-82F2-36CC027E52AA}"
     Type="Note"
     DisplayName="CF_SBCountryTax_0"
     StaticName="CF_SBCountryTax_0"
     Name="CF_SBCountryTax_0"
     ShowInViewForms="FALSE"
     Required="FALSE"
     Hidden="TRUE"
     CanToggleHidden="TRUE"
     Overwrite="TRUE"
     Group="Cannonfodder"
     xmlns="http://schemas.microsoft.com/sharepoint/">
     </Field>
    
     <Field Type="TaxonomyFieldType"
     DisplayName="Country"
     Description="Country List"
     List="Lists/TaxonomyHiddenList"
     WebId="~sitecollection"
     ShowField="Term1033"
     Required="FALSE"
     EnforceUniqueValues="FALSE"
     Group="Cannonfodder"
     ID="{3F937DB0-4D86-4EE5-B553-07AC64B070BB}"
     StaticName="CF_SBCountry"
     Name="CF_SBCountry"
     Overwrite="TRUE"
     xmlns="http://schemas.microsoft.com/sharepoint/">
     <Customization>
     <ArrayOfProperty>
     <Property>
     <Name>TextField</Name>
     <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
     p4:type="q6:string"
     xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">{8228CEF8-3021-4CD9-82F2-36CC027E52AA}</Value>
     </Property>
     </ArrayOfProperty>
     </Customization>
     </Field>
    </Elements>
    

Creating Content Type.

  • Right click your project and select Add -> New Item.
  • Add a Content Type. I’ve named mine CT_Document Location and
    inherit from Document content type.
  • In the GUI added both your TaxonomyField and your Note field. Also include two additional fields Taxonomy Catch All Column (TaxCatchAll) and Taxonomy Catch All Column 1 (TaxCatchAllLabel) . The TaxCatchAll and TaxCatchAllLabel columns are lookup fields pointing to the special hidden list in the site collection (TaxonomyHiddenList). They point to the CatachAllData and CatachAllDataLabel fields in the hidden list and are used within each list that has a column of type Managed Metadata.

  • Give your Content type a name, description and Group.

Create your List Definition

  • Right click your project and select Add -> New Item.
  • Add List. Give your list a name, I’ve named mine Reference Document and Create a Customizable list based on Document Library, and Use a standard document template for this library.
  • On the GUI, Click the Content Types button.
  • Add your content type to the Content Type Settings box. At this point I always Click OK. Then reopen the Content Type Settings box to set the default. If I don’t close the dialog after adding my content type, I find when I try to set it as default Visual Studio sometimes removes items/properties I don’t expect it to. After setting my Content Type to default, I remove Document to ensure only my one will appear, select the row and press the delete key.

  • After click OK to the Content Type Settings dialog, you will find your columns from your content type will be added to the Columns in the GUI.
  • If you select the Views tab, you will see that your managed metadata column will be in the default view.
  • If you select the List tab, you can update the Title, List Url, and Description if you want.

Event Receivers

With Taxonomy we need to add event receivers on item adding and item updating. The events are SharePoint Taxonomy events, and they ensure the hidden columns are updated whenever the vales within the Managed Metadata column changes.

  • Right click your project and select Add -> New Item.
  • Add Empty Element. Put the following XML. Remember to put the ListTemplateId equals to the List Definition Type. This can be found by viewing the Elements.xml file for the List Definition.
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <Receivers ListTemplateId="10001">
 <Receiver>
 <Name>TaxonomyItemSynchronousAddedEventReceiver</Name>
 <Synchronization>Synchronous</Synchronization>
 <Type>ItemAdding</Type>
 <SequenceNumber>10000</SequenceNumber>
 <Assembly>Microsoft.SharePoint.Taxonomy, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
 <Class>Microsoft.SharePoint.Taxonomy.TaxonomyItemEventReceiver</Class>
 <Data></Data>
 </Receiver>
 <Receiver>
 <Name>TaxonomyItemUpdatingEventReceiver</Name>
 <Synchronization>Synchronous</Synchronization>
 <Type>ItemUpdating</Type>
 <SequenceNumber>10000</SequenceNumber>
 <Assembly>Microsoft.SharePoint.Taxonomy, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
 <Class>Microsoft.SharePoint.Taxonomy.TaxonomyItemEventReceiver</Class>
 <Data></Data>
 </Receiver>
 </Receivers>
</Elements>

Features

One of the gotcha’s I have experience in the past is that when you create a list instance with Managed Metadata and you put both the Definition and the Instance within the same feature, the list instance doesn’t get instantiated. Therefore add a new feature to the Features folder.

  • Set one feature to Site and name it Site – Assets. Set the other feature to Web and name it Web – Assets.
  • In the Site feature, add your Column, Content Type and List Definition.
  • In the Web feature, add your List instance, and Event Receiver.
  • Grab the ID of your Site Feature (Where your list Definition is), and add the ID to your List Instance.
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <ListInstance Title="Reference Document Sandbox"
 OnQuickLaunch="TRUE"
 TemplateType="10001"
 FeatureId="[GUID of Site Feature of List Definition]"
 Url="Lists/SBReferenceDocument"
 Description="My List Instance">
 </ListInstance>
</Elements>

If you deploy now you will find that nothing ties up. Your column, content type and list will exist, but it will not be connected to the Term Set. Easiest way to prove this:

  • Goto your Reference Document Library.
  • Upload a document.
  • You will notice that the Country/Managed Metadata Field is greyed out.

This is where the JavaScript file comes in. Really, ideally, you would want this script to run as a configuration script that would run when an admin user hits the page, perhaps more configuration stuff will happen, such as setting default content types, branding, setting up navigation etc, this would be somehow tied into the master page, or inserted by a custom action link. In this demo I will show you a configuration page, which you could also extend. If the configuration hasn’t been ‘configured’ then every user would redirect to this page until the configuration has been complete. For now this is just a demo, and a simple page with a config button will be enough to explain how to connect the Manage metadata column to the term set.

JavaScript, Property Bag and configuration page.

I’m going to add a simple aspx page to my solution. HTML and JavaScript will be self-contained within this page for simplicity, but in a live environment, you would probably separate them out.

  • Right click on the project and add Module. Name this Assets.
  • Right click Manage NuGet Packages… It is a much easier way of adding libraries such as Knockout, JQuery and Underscore etc.
  • Search for JQuery, and Underscore and add them to the solution.
  • Now move this Javascript files and put them within the Assets module in a folder called Scripts.
  • The elements file for the module, make sure that all <File> have ReplaceContent=”True”. This ensures any update to the files will be successfully updated over.
  • Right click on the project and add Module. Name this ConfigurationPage.
  • Rename the .txt file to Config.aspx
  • Change the text within the text file to the following. This is a typical test start page. It will inherit your master page, and be a basic page so that you can add JavaScript/HTML.
<%@ Assembly Name="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint.WebPartPages" %>
<%@ Register TagPrefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="WebPartPages" Namespace="Microsoft.SharePoint.WebPartPages" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Page Language="C#" MasterPageFile="~masterurl/default.master" Inherits="Microsoft.SharePoint.WebPartPages.WebPartPage,Microsoft.SharePoint,Version=15.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c" EnableViewState="false" meta:webpartpageexpansion="full" meta:progid="SharePoint.WebPartPage.Document" %>

<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server" >

</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server">
 Configuration Page
</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderSearchArea" runat="server" />

<asp:Content ID="Content4" ContentPlaceHolderID="PlaceHolderMain" runat="server">
 <SharePoint:FormDigest ID="FormDigest1" runat="server"/>

</asp:Content>
  • Within the ContentID=”Content4″ under the SharePoint:FormDigest add the following HTML.
<div id="outerContainer" style="display:none">
<div class="cellOuter">
 <div class="inner">Taxonomy Fields Configured</div>
 <div class="inner"><span id="taxConfigured" class="cross"></span></div>
 </div>
 <div class="cellOuter">
 <div class="inner"></div>
 <div class="inner"></div>
 </div>
 <div class="cellOuter">
 <div class="inner"></div>
 <div class="buttons">
 <input class="configButton" type="button" title="Configure Site" name="Run Configuration" onclick="CF.SiteConfiguration.Provisioning.Execute();" value="Configure Site" />
 </div>
 </div>
</div>

<div id="error" style="display:none">
 There has been an error. Unable to configure this site at this time.
</div>
  • The above HTML is a simple table that displays the configuration item, in this case “Taxonomy Configuration” and a button, that when pressed it will call our JavaScript we are yet to write. I’m using class “Tick” and “Cross” to display an icon to inform the user if Taxonomy is already been configured or not. At this point lets create our CSS that this page will use.
  • Within the Assets Module create a folder called CSS and add a style sheet. I’ve called mine CF.css
  • Put the following CSS code. This enough css to create a table like format, and will display a cross or a tick depending on the class used. If you download the solution at the bottom of this blog post you will be able to use the same jpeg files as me for the Cross and Tick. Add these to your Assets Module under a folder called Images.
.cross{
       background:#fff url(../../assets/images/cross.jpg) no-repeat right center;
       display:block;
       height:24px;
       width:24px;
       float:right
}
.tick{
       background:#fff url(../../assets/images/tick.jpg) no-repeat right center;
       display:block;
       height:24px;
       width:24px;
       float:right;
}

#outerContainer{display:table}
.cellOuter{display:table-row}
.inner{display:table-cell;width:50%;}
.buttons{display:table-cell;float:right;margin-top:20px;margin-right:3px;}
  • To ensure that this code can only run once, I have created a property bag item called TaxonomyFieldsConfigured and set the value to false. The JavaScript will change this to True once the Site column has been configured.
  • To create the Property Bag item, right click the project and select Add -> New Item.
  • Select Empty Elements. Give it the name of PB_Config.
  • We want our property bag to just be on the Site Collection (Root Web), as the Site Column will only be configured at the Root Web. Within the Elements.xml file of PB_Config put the following. This will create a property bag value called TaxonomyFieldsConfigured.
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <PropertyBag ParentType="Web" RootWebOnly="TRUE">
 <Property Name="TaxonomyFieldsConfigured" Value="false" Type="string"/>
 </PropertyBag>
</Elements>
  • Going back to the Config.aspx we need to add the following reference to JQuery.js, Underscore.js and our CSS. Within the ContentPlaceHolderID=”PlaceHolderAdditionalPageHead” add the following.
 <script type="text/javascript" src="assets/JS/jquery-2.1.1.min.js"></script>
 <script type="text/javascript" src="assets/JS/underscore.min.js"></script>
 <link id="CFCssLink" rel="Stylesheet" type="text/css" href="assets/css/CF.css" runat="server" />
  • The above will ensure that JQuery, UnderScore (used for array handling) and the CSS will be loaded on the page.
  • Underneath the above code within the PlaceHolderAdditionalPageHead the following JavaScript will do the following: (Comments within the code to explain each step).
    • Check On Load of the page if the Taxonomy Configuration has already taken place.
      • If it has, by checking the property bag, it will disable the Config button to prevent configuration from happening again, it will also update the page to the user to show that configuration has already taken place by showing a tick next to the Taxonomy Configuration text.
      • If it hasn’t, by checking the property bag, the code will display the config button, and indicate to the user that the configuration hasn’t taken place yet.
    • On Click of the Config button, it will load relevant required SharePoint Javascript libraries, such as SP.Taxonomy.js before calling the relevant code to tie up the column with the Term Set. On successful update it will then update the property bag and disable the Config button to prevent the user from running the code a second time.
    • As stated at the start of my blog, you could find the TermSet name by Name instead of ID, this will allow you to move this code through environments without needing to import and export your termsets with the code. The below JavaScript doesn’t show this, as I use TermSet ID. The downside of using Name instead of ID is that you could have two groups with the same TermSet name, meaning you might grab the wrong one. Also you would need to obtain all TermSet names and loop through them looking for a match to grab the TermSetID, this would be additional call to SharePoint.
    <script type="text/javascript">
        //Create NameSpace
        var CF = CF || {};
        CF.SiteConfiguration = CF.SiteConfiguration || {};
        CF.Data = CF.Data || {};
        //On Load function calls the Provision Load Function
        //This will check to see if the Configuration has already taken place.
        CF.onLoadFunction = function () {
            CF.SiteConfiguration.Provisioning.Load();
        }

        //A Constant that contains the Property Bag Value. This allows the code to get/set the value.
        CF.Data.Constants = {
            CFTaxonomyFieldConfigured: "TaxonomyFieldsConfigured"
        }

        //ID contains the Term Set Id, Name contains the internal name of the Site Column.
        //This is written as an array, therefore if there is more that one Site Column to connect, you          just need to add more Id's/Name's for this to work.
        CF.Data.TaxonomyFields = [
            { Id: '69c09582-e13d-4662-a675-abee73995586', Name: 'CF_SBCountry' }
        ];

        //Provisioning Code, self contained variables and functions.
        CF.SiteConfiguration.Provisioning = function () {
            var ctx,
                web,
                properties,
                currentDlg,
                dlgTitle,
                dlgMsg,
                dlgWidth,
                dlgHeight;

            //Load function, gets all web properties.
            var load = function () {
                ctx = SP.ClientContext.get_current();
                web = ctx.get_web();
                properties = web.get_allProperties();
                ctx.load(web);
                ctx.load(properties);
                ctx.executeQueryAsync(onLoadSuccess, onFailedCallback);
            };

            //On Success of getting web Properties.
            var onLoadSuccess = function () {
                var allProps = properties.get_fieldValues();
                var taxConfigured;

                //Success, so show Configuration Grid.
                jQuery("#outerContainer").show();

                //Check if property bag of Taxonomy Field Exists.
                if (allProps[CF.Data.Constants.CFTaxonomyFieldConfigured] != null) {
                    taxConfigured = properties.get_item(CF.Data.Constants.CFTaxonomyFieldConfigured);
                }
                else {
                    //If doesn't exist then show that it's configured, and disable button.
                    jQuery("#taxConfigured").removeClass("cross").addClass("tick");
                    jQuery(".configButton").attr("disabled", "disabled");
                    return;
                }

                //Property bag value found.
                if (taxConfigured == "true") {
                    //If true, already been configured. Update screen for user.
                    jQuery("#taxConfigured").removeClass("cross").addClass("tick");
                    jQuery(".configButton").attr("disabled", "disabled");
                }
            };

            //Error handler.
            var onFailedCallback = function (sender, args) {
                // Timeout on dialog for demo purposes so you can see it, you wouldn't put this in your code.
                setTimeout(function () {
                    if (currentDlg != null) {
                        //Close dialog if exists.
                        currentDlg.close();
                    }

                    //Display an error message to user that there is an issue.
                    var errorStatus = SP.UI.Status.addStatus("Error", "There has been a problem configuring this site. " + args.get_message(), true);

                    SP.UI.Status.setStatusPriColor(errorStatus, "red");
                    //Hide the Configuration Table to user.
                    jQuery("#outerContainer").hide();
                    //Show Error message.
                    jQuery("#error").show();
                }, 4000);
            };

            //Function to configure, title, message, height and width of pop up dialog.
            var configureDlg = function (title, message, height, width) {
                if (currentDlg != null) {
                    currentDlg.close();
                }

                dlgTitle = title;
                dlgMsg = message;
                dlgHeight = height;
                dlgWidth = width;
                currentDlg =  SP.UI.ModalDialog.showWaitScreenWithNoClose(dlgTitle, dlgMsg, dlgHeight, dlgWidth);
            };

            //Callback if Connecting Taxonomy is successful.
            var taxonomySuccessCallBack = function () {
                setTimeout(function () {
                    if (currentDlg != null) {
                        currentDlg.close();
                    }

                    jQuery("#taxConfigured").removeClass("cross").addClass("tick");
                    jQuery(".configButton").attr("disabled", "disabled");
                }, 4000);
            };

            //Utility to update the property bag. Passing in the Property Key, Value to update to, Success and Failure callback.
            var updatePropertyBag = function (key, value, successCallback, failedCallback) {
                ctx = SP.ClientContext.get_current();
                properties = ctx.get_web().get_allProperties();
                properties.set_item(key, value);
                ctx.get_web().update();
                ctx.load(properties);
                ctx.executeQueryAsync(successCallback, failedCallback);
            };

            //Check to see if there is any Taxonomy Fields to configure.
            var configureTaxonomyFields = function (successCallBack, failedCallback) {
                ctx = SP.ClientContext.get_current();
                var fieldNames;
                if (CF.Data.TaxonomyFields == null || CF.Data.TaxonomyFields.length == 0) {
                    successCallBack();
                    return;
                }

                //There are fields to configure call the function.
                connectMMFieldsToSP(CF.Data.TaxonomyFields, successCallBack, failedCallback);
            };

            //Main code that connects the Site columns to the Term sets.

            var connectMMFieldsToSP = function (taxFields, success, failed) {
                ctx = SP.ClientContext.get_current();
                //load the termstore.
                var termStore = SP.Taxonomy.TaxonomySession.getTaxonomySession(ctx).getDefaultKeywordsTermStore();
                ctx.load(termStore);
                //Using underscore, loop through each site column and load them to modify.
                _.each(taxFields, function (current) {
                    var field = ctx.get_site().get_rootWeb().get_fields().getByInternalNameOrTitle(current.Name);
                    ctx.load(field);
                });

                //Call query to get given field Site columns
                ctx.executeQueryAsync(function () {
                    //Using Underscore to loop through all site columns again now loaded, and update with TermStore ID and TermSet.
                    _.each(taxFields, function (current) {
                        var field = ctx.get_site().get_rootWeb().get_fields().getByInternalNameOrTitle(current.Name);
                        var taxField = ctx.castTo(field, SP.Taxonomy.TaxonomyField);
                        taxField.set_sspId(termStore.get_id().toString());
                        taxField.set_termSetId(current.Id.toString().toUpperCase());
                        taxField.updateAndPushChanges(true);
                    });

                    //Execute Changes.
                    ctx.executeQueryAsync(success, failed);
                },
                failed);
           };

            //Init function that will load the Taxonomy.js on the page first, followed by calling code to tie up the Columns to Term Sets, lastly it will update the Property bag to prevent code running again.
            var init = function () {
                SP.SOD.registerSod('sp.taxonomy.js', SP.Utilities.Utility.getLayoutsPageUrl('sp.taxonomy.js'));
                SP.SOD.executeFunc('sp.taxonomy.js', 'SP.Taxonomy.TaxonomySession', function () {
                    configureDlg("Configuring Taxonomy on your site...", "</br>Please wait</br></br>;Please do not close your browser window.&quot;, 300, 1000);
                   configureTaxonomyFields(updatePropertyBag(CF.Data.Constants.CFTaxonomyFieldConfigured, &quot;true&quot;, taxonomySuccessCallBack, onFailedCallback), onFailedCallback);
                });
            };
            return {
                Execute: init,
                Load: load
            }
        }();

        //After all SharePoint code has run, it will call the onLoadFunction.
        _spBodyOnLoadFunctionNames.push("CF.onLoadFunction");
    </script>

Assets Element file.

I’ve mentioned in this post add stuff to the Assets module but haven’t actually shown you the final Elements.xml file. Within my Asset file I have put css, images, and Scripts within separate folders. (You may have noticed this from declaring Scripts and CSS link in the PalceHolderAdditonalPageHead of the Config.aspx page). Here is my final Elements.xml file.

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
 <Module Name="Assets">
 <File Path="MO_Assets\Scripts\jquery-2.1.1.min.js" Url="Assets/JS/jquery-2.1.1.min.js" ReplaceContent="TRUE"/>
 <File Path="MO_Assets\Scripts\jquery-2.1.1.min.map" Url="Assets/JS/jquery-2.1.1.min.map" ReplaceContent="TRUE" />
 <File Path="MO_Assets\Scripts\underscore.min.map" Url="Assets/JS/underscore-min.map" ReplaceContent="TRUE" />
 <File Path="MO_Assets\Scripts\underscore.min.js" Url="Assets/JS/underscore.min.js" ReplaceContent="TRUE" />
 <File Path="MO_Assets\Images\cross.jpg" Url="Assets/Images/cross.jpg" ReplaceContent="TRUE" />
 <File Path="MO_Assets\Images\tick.jpg" Url="Assets/Images/tick.jpg" ReplaceContent="TRUE" />
 <File Path="MO_Assets\css\CF.css" Url="Assets/css/CF.css" ReplaceContent="TRUE" />
 </Module>
</Elements>

Updating the Features.

With the new modules/elements/pages since last time I told you to deploy, you will need to update the features.

  • In the Site feature, you should now have Column, Content Type, List Definition, and Assets Module.
  • In the Web feature, you should now have List instance, and Event Receiver, Configuration Page and Property Bag.

Deploy

You can now deploy your solution to your SharePoint site. Either using Visual Studio or publishing your solution and uploading you WSP.

Once deployed, ensure that both your Site and Web features are both activated then navigate to your Config.aspx page. (e.g. https://%5BYour365Site%5D.sharepoint.com/config.aspx)

As you can see from above, the first time you hit the page, it has already done a check to see if the property bag value “TaxonomyFieldsConfigured” has been set to true. The value has equalled false as we haven’t configured yet and the Configure Site button is enabled.

By clicking the Configure Site button our Configuring Dialog will appear while the JavaScript runs.

Once complete the Taxonomy Fields Configured task will be updated with a “Tick” and the Configure Site will be disabled. Even if you refresh the page, you won’t be able to run the Configure Site button again.

If you now navigate to your list instance, in my case “Reference Document Sandbox” and upload a document, the managed metadata column will now be available for editing.

You can download the Visual Studio solution from my one drive.

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, SharePoint | Tagged Development, JavaScript, Managed Metadata, SharePoint 2013, SharePoint Online

Custom Controls for publishing pages

Posted on December 15, 2013 by cann0nf0dder

What are we trying to achieve.

In this example, I am creating a publishing site, where, each page will show off different products that my fictional company sells. Two of the metadata columns on my library list will be a Person/Group field, and the other one will be a Managed Metadata field.

These two fields when added to my custom Page Layout will look like the following on the page.

However, I wish to style them a bit. Currently the contacts are links, but they are going to Microsoft default page for users without mysites http://<site>/_layouts/userdisp.aspx?ID=33, and the Countries list is just a long list. I would rather have this as a bulleted list. Therefore by the end of this blog post, I will show you how to change the above to look something similar to below.

The links for the Contacts will now point to my custom page http://<site>/_layouts/persondetails.aspx?accountName= i%3a0%23.w%7ccannonfodder%5cpaul.matthews

Step to follow.

To perform this demo, first I created a basic out the box publishing site. (http://cfpub) I’ve then created a SharePoint project within Visual Studio and pointed it to the site for deployment.

Create content type.

First thing we need to do is create our custom fields and content type (CFProduct). I have created 3 fields (4 in total as one is a managed metadata field). These fields are added to a site feature.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Field Type="TaxonomyFieldType" DisplayName="Countries" Required="FALSE" Group="CannonFodder"
         Description="Countries you can find this product in." ShowField="Term1033" ID="{A748B2A7-D77F-4b4c-BDE9-DE460ADDEF2D}"
         StaticName="CFCountries" Name="CFCountries"/>
 <Field Type="Note" DisplayName="Countries_0" StaticName="CFCountriesTaxHTField0" Name="CFCountriesTaxHTField0" ID="{3C6D8848-30BA-4c0f-A205-585F32593731}"
         ShowInViewForms="FALSE" Group="CannonFodder" Required="FALSE" Hidden="TRUE"/>;
 <Field Type="UserMulti" DisplayName="Contacts" List="UserInfo" Required="FALSE" EnforceUniqueValues="FALSE"
         ShowField="ImnName" UserSelectionMode="PeopleOnly" Description="People you can contact about this product." UserSelectionScope="0"
         Group="CannonFodder" ID="{E008B0AE-A928-4c4e-B7A0-3FE0A1D43E7D}" Mult="TRUE" StaticName="CFContacts" Name="CFContacts"/>
 <Field Type="HTML" DisplayName="Details" Required="TRUE" Group="CannonFodder" Description="Details about the product" EnforceUniqueValues="FALSE"
         RichText="TRUE" RichTextMode="ThemeHtml" Sortable="FALSE" ID="{E97C1A12-2C5A-411A-B1A7-950716596887}"
         StaticName="CFDetails" Name="CFDetails"  SourceID="http://schemas.microsoft.com/sharepoint/v3" UnlimitedLengthInDocumentLibrary="TRUE"/>
</Elements>

Next I’ve created a content type called CFProducts. This content type is based on the Page content type of SharePoint. This is also added to the site feature.

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <!-- Parent ContentType: Page (0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF39) -->
  <ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900ff1b27e63a604e3d9c0cf419fcd6f42e"
               Name="CFProduct Page"
               Group="Cannonfodder CT"
               Description="A Product to be displayed"
               Inherits="TRUE"
               Version="0">
  <FieldRefs>
     <FieldRef ID="{A748B2A7-D77F-4b4c-BDE9-DE460ADDEF2D}" Required="FALSE" Name="CFCountries" />
     <FieldRef ID="{3C6D8848-30BA-4c0f-A205-585F32593731}" Required="FALSE" Name="CFCountriesTaxHTField0" Hidden="TRUE"/>
     <FieldRef ID="{E008B0AE-A928-4c4e-B7A0-3FE0A1D43E7D}" Required="FALSE" Name="CFContacts"/>
     <FieldRef ID="{E97C1A12-2C5A-411A-B1A7-950716596887}" Required="TRUE" Name="CFDetails"/>
  </FieldRefs>
</ContentType>
</Elements>

Write the code to link up the managed metadata on deployment.

This step I’m not going to write out here as I’ve already described how to do it once in the following blog https://cann0nf0dder.wordpress.com/2013/04/01/creating-a-site-column-with-managed-metadata/ under the heading Programmatically create a Managed Metadata Column.

Create your custom Page Layout page.

Add a Module to your project and delete the text file that Visual Studio automatically puts with it. Now add an Application Page to the module. (When you create the application page, first of all it will add it to the layouts folder, once created just cut and paste into your module). Call this page ProductPage.aspx. This module is a site feature as well.

In your C# file of your ProductPage change the base class from LayoutsPageBase to PublishingLayoutPage. You will also need the using reference of using Microsoft.SharePoint.Publishing;

In you aspx file add the following Register tags

<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Taxonomy" Namespace="Microsoft.SharePoint.Taxonomy" Assembly="Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="CF" Namespace="CFSP.CustomPageLayoutControls.Controls" Assembly="$SharePoint.Project.AssemblyFullName$" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>

Below is the whole aspx page layout. What I have done is create a basic table structure on my page, with Title, CFDetails, CFContacts, and CFCountries.

<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="PublishingWebControls" Namespace="Microsoft.SharePoint.Publishing.WebControls" Assembly="Microsoft.SharePoint.Publishing, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Taxonomy" Namespace="Microsoft.SharePoint.Taxonomy" Assembly="Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="asp" Namespace="System.Web.UI" Assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Assembly Name="Microsoft.Web.CommandUI, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ProductPage.aspx.cs" Inherits="CFSP.CustomPageLayoutControls.ProductPage" %>

<asp:Content ID="PageHead" ContentPlaceHolderID="PlaceHolderAdditionalPageHead" runat="server">
	 <SharePoint:CssRegistration ID="CssRegistration1" name="<% $SPUrl:~sitecollection/Style Library/~language/Core Styles/page-layouts-21.css %>" runat="server"/>
	<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit" id="editmodestyles">
		<!-- Styles for edit mode only-->
		<SharePoint:CssRegistration ID="CssRegistration2" name="<% $SPUrl:~sitecollection/Style Library/~language/Core Styles/edit-mode-21.css %>"
			After="<% $SPUrl:~sitecollection/Style Library/~language/Core Styles/page-layouts-21.css %>" runat="server"/>
	</PublishingWebControls:EditModePanel>
               <!—The style sheet can be obtained from the solution on my skydrive at the end of the post -->
	<link rel="stylesheet" type="text/css" href="/_Layouts/CFSP.CustomPageLayoutControl/css/CFSP.CPLC.css?rev=1.0.0.0"/>
</asp:Content>

<asp:Content ID="Main" ContentPlaceHolderID="PlaceHolderMain" runat="server">
 <div class="product-page contentpage-wrapper">       
 <div class="content-webpart-wrapper">
  <table cellpadding="4" cellspacing="0" border="0" width="100%">
	<tr>
	 <td valign="top" style="padding:0;width:60%">
		<table cellpadding="4" cellspacing="0" border="0" width="100%" height="100%" class="productcenter-webpart-wrapper">
			<tr>
				<td id="_invisibleIfEmpty" name="_invisibleIfEmpty" valign="top" class="left-column">
				   <div class="header">
					  <!-- Title and Address controls -->
					  <div class="title">
						 <SharePoint:TextField FieldName="Title" runat="server" />
					  </div>
				   </div>
				   <div class="description">
					  <PublishingWebControls:RichHtmlField FieldName="CFDetails" runat="server" />                                           
				   </div>
				</td>
				<td class="right-column" style="width:250px">
					<div class="summary-links-short contacts">
						<table class="s4-wpTopTable" cellpadding="0" style="width:100%";>
						 <tbody>
							<tr class="ms-WPHeader">
								<td title="Contacts" class="ms-WPHeaderTd">
									 <h3 style="text-align:justify;" class="ms-standardheader ms-WPTitle"><span>Contacts</span></h3>
								</td>
							</tr>
						 </tbody>
						</table>
						<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit">
							<SharePoint:UserField runat="server" FieldName="CFContacts" />
						</PublishingWebControls:EditModePanel>
						<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
                           <SharePoint:UserField ID="UserField1" runat="server" FieldName="CFContacts" />
						</PublishingWebControls:EditModePanel>
					</div>
					<div class="summary-links-short countries">
						<table class="s4-wpTopTable" cellpadding="0" style="width:100%";>
						 <tbody>
							<tr class="ms-WPHeader">
								<td title="Contacts" class="ms-WPHeaderTd">
									 <h3 style="text-align:justify;" class="ms-standardheader ms-WPTitle"><span>Countries</span></h3>
								</td>
							</tr>
						 </tbody>
						</table>
						<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit">
							<Taxonomy:TaxonomyFieldControl runat="server" FieldName="CFCountries" />
						</PublishingWebControls:EditModePanel>
						<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
                            <Taxonomy:TaxonomyFieldControl ID="TaxonomyFieldControl1" runat="server" FieldName="CFCountries" />
						</PublishingWebControls:EditModePanel>
					</div>
				</td>
			</tr>
		</table>
	 </td>
	</tr>
	<script language="javascript">if(typeof (MSOLayout_MakeInvisibleIfEmpty) == "function") { MSOLayout_MakeInvisibleIfEmpty(); }</script>
  </table>
 </div>
 </div>
</asp:Content>

<asp:Content ID="PageTitle" ContentPlaceHolderID="PlaceHolderPageTitle" runat="server">
  <SharePoint:ListItemProperty property="Title" runat="server"/>
</asp:Content>

<asp:Content ID="PageTitleInTitleArea" ContentPlaceHolderID="PlaceHolderPageTitleInTitleArea" runat="server" >
<SharePoint:ListItemProperty property="Title" runat="server"/>
</asp:Content>

From reading through the page above you will see that I have enclosed the Contact and Countries section with;

<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Edit">
<Taxonomy:TaxonomyFieldControl runat="server" FieldName="CFCountries" />
</PublishingWebControls:EditModePanel>
<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
             <Taxonomy:TaxonomyFieldControl ID="TaxonomyFieldControl1" runat="server" FieldName="CFCountries" />
</PublishingWebControls:EditModePanel>

This is because later, I will be replacing the Display sections with our custom controls. Currently the above will just give you the out of the box experience for a Person Picker and Managed Metadata control. Also if you have never created a page layout before, notice that the FieldName points to the internal name of our field within the CFProduct content type.

Create the Elements file for the Page Layout.

Here we are just updating the elements file for the page layout. This will tell SharePoint when the feature is activated to place the page layout in the masterpage library, and we will configure the properties to associate the page layout with our custom content type.

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="PageLayouts" Url="_catalogs/masterpage">
<File Path="PageLayouts\ProductPage.aspx" Url="ProductPage.aspx" Type="GhostableInLibrary" IgnoreIfAlreadyExists="true">
<Property Name="Title" Value="Cannonfodder Product Page"/>
<Property Name="MasterPageDescription" Value="A content page containing fields used by the product page"/>
<Property Name="ContentType" Value="CFProduct Page" />
<Property Name="PublishingAssociatedContentType" Value=";#CFProduct Page;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900ff1b27e63a604e3d9c0cf419fcd6f42e;#"/>
<Property Name="PublishingPreviewImage" Value="~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleBodyOnly.png, ~SiteCollection/_catalogs/masterpage/$Resources:core,Culture;/Preview Images/ArticleBodyOnly.png" />
</File>
</Module>
</Elements>

Create a web feature to add the content type to the list.

We could do this step manually, but I like to try and automated everything. Create a web feature and give it an Event Receiver. When we activate this feature, it will grab the Pages list, grab our new content type CFProducts and add this Content Type to the list. It also hides the 3 content types automatically added by SharePoint. (Article Page, Welcome Page, and Page) My using statement CFSP.CustomPageLayoutControls.Configuration is just pointing to a class that is holding all my Constants for this project. (E.g ContentTypes names and IDs, field InternalNames and ID.)


using System;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using CFSP.CustomPageLayoutControls.Configuration;

namespace CFSP.CustomPageLayoutControls.Features.Web___CPLC_CTToList
{
[Guid("7d5df801-4ccc-46c5-a963-3380a50e7981")]
public class Web___CPLC_CTToListEventReceiver : SPFeatureReceiver
{
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWeb web = properties.Feature.Parent as SPWeb;
AddContentTypeToPageLibrary(web);
}

private void AddContentTypeToPageLibrary(SPWeb web)
{
try
{
SPList list = web.Lists.TryGetList("Pages");
if (list != null)
{
var cfProductCT = web.ContentTypes[new SPContentTypeId(Constants.ContentTypes.CFPRODUCTCTID)];
var articlePageCT = web.ContentTypes["Article Page"];
var welcomePageCT = web.ContentTypes["Welcome Page"];
var pageCT = web.ContentTypes["Page"];
web.AllowUnsafeUpdates = true;
list.ContentTypes.Add(cfProductCT);
list.SetDefaultContentType(cfProductCT);

//Hidden, but do not delete incase some of these pages still exist, prevent more from being created

list.HideContentType(articlePageCT);
list.HideContentType(welcomePageCT);
list.HideContentType(pageCT);
}
}
catch (Exception ex)
{
//Throw Error
}
finally
{
web.AllowUnsafeUpdates = false;
}
}
}
}

At this point we have just created a typical SharePoint Page Layout using the out the box controls. If you deploy your solution right now, you should be able to create a product page, add content to your page and save it. The Contact and Countries will look like my first picture at the top of the post as we haven’t created custom controls yet.

Note: If you have problems with your people picker, I did too. Simple fix for this demo is to put the page in IE8 mode. There is lots of google posts online about this bug. Permanent fix is out there somewhere, some people force the page to open in IE8 mode, but I personally think that is a workaround not a fix.

Building the custom controls

Create a C# class called CustomTaxonomyControl. This control will inherit the base class of System.Web.UI.Control. We will need to ensure we have the following using statements.

using Microsoft.SharePoint.Taxonomy;
using Microsoft.SharePoint;
using System.Web;
using System.Web.UI;

In this class we are going to use a LiteralControl to render the control and override the CreateChildControls() and OnPreRender() methods.

public class CustomTaxonomyControl : System.Web.UI.Control
    {
        protected LiteralControl litHtml;
        public string FieldName { get; set; }
        protected override void CreateChildControls()
        {
            litHtml = new LiteralControl();
            this.Controls.Add(litHtml);
            base.CreateChildControls();
        }

        protected override void OnPreRender(EventArgs e)
        {
           //Use a stringbuilder to collate the control data into a list
            StringBuilder result = new StringBuilder();
            result.Append("<div class='cfTaxonomyControl'>");
            result.Append("<ul>");

            if(SPContext.Current.ListItem != null)
            {
                if(SPContext.Current.ListItem.Fields.ContainsField(FieldName))
                {
                    List<TaxonomyInfo> termsLabel = GetTermsLabel(SPContext.Current.ListItem, FieldName);

                    foreach (TaxonomyInfo term in termsLabel)
                    {
                        result.Append(String.Format("<li>{0}</li>", HttpContext.Current.Server.HtmlEncode(term.Name)));
                    }
                }
            }

            result.Append("</ul>");
            result.Append("</div>");
            litHtml.Text = result.ToString();
            base.OnPreRender(e);
        }

        private List<TaxonomyInfo> GetTermsLabel(SPListItem listItem, string fieldName)
        {
         //Get the value(s) of the field.
            var taxonomyField = listItem.Fields.GetFieldByInternalName(fieldName) as TaxonomyField;
           //Multiple values are grabbed using TaxonomyFieldValueCollection
            if (taxonomyField.AllowMultipleValues)
            {
                var fieldValuesCollection = listItem[taxonomyField.Title] as TaxonomyFieldValueCollection;

                var query = from x in fieldValuesCollection
                    select new TaxonomyInfo() {Name = x.Label, ValueWssId = x.WssId};

                return query.ToList();
            }
            else
            {
              //Single managed metadata column value are grabbed using the TaonomyFieldValue.
                var fieldValue = listItem[taxonomyField.Title] as TaxonomyFieldValue;
                return new List<TaxonomyInfo>()
                {
                    new TaxonomyInfo() {Name = fieldValue.Label, ValueWssId = fieldValue.WssId}
                };
            }
        }

    }

//Grabbing both the Name and the WssId as this is useful, especially if you want to extend this control to search for other pages with same managed metadata.
    class TaxonomyInfo
    {
        public string Name { get; set; }
        public int ValueWssId { get; set; }
    }

Create another C# class for the people picker. Again add the using statements and base the class on System.Web.UI.Control

private string mySiteUrl = string.Empty;
        protected LiteralControl litHtml;
        public string FieldName { get; set; }

        protected override void CreateChildControls()
        {
            litHtml = new LiteralControl();
            this.Controls.Add(litHtml);
            base.CreateChildControls();
        }

        protected override void OnPreRender(EventArgs e)
        {
            StringBuilder result = new StringBuilder();
            result.Append("<div class='cfPeopleControl'><ul>");
            if (SPContext.Current.ListItem != null)
            {
                if (SPContext.Current.List.Fields.ContainsField(FieldName))
                {
                    List<string> peopleList = GetPeopleNames(SPContext.Current.ListItem, FieldName);

                    foreach (string p in peopleList)
                    {
                        string[] person = p.Split(';');
                        result.Append(string.Format("<li><a href='{0}'>{1}</a></li>",
                                                        GetPersonLink(person[0]),
                                                        HttpContext.Current.Server.HtmlEncode(person[1])
                                                   )
                                      );
                    }
                }
            }
            string html = result.ToString().TrimEnd(' ');
            litHtml.Text = html + "</ul></div>";
            base.OnPreRender(e);
        }

        private string GetPersonLink(string accountName)
        {
            string result = string.Empty;
            result = "http://my.cannonfodder.local/person.aspx?accountname=" + HttpContext.Current.Server.UrlEncode(accountName);
            return result;
        }

        private List<String> GetPeopleNames(SPListItem listItem, string fieldName)
        {
            var peopleField = listItem.Fields.GetFieldByInternalName(fieldName) as SPFieldUser;

            if (peopleField.AllowMultipleValues)
            {
                var peopleValueCollection = listItem[peopleField.Title] as SPFieldUserValueCollection;
                if (peopleValueCollection == null)
                    return new List<String>();
                else
                    return peopleValueCollection.Select(x => x.User.LoginName + ";" + x.User.Name).ToList();

            }
            else
            {
                var peopleValue = listItem[peopleField.Title] as SPFieldUserValue;

                if (peopleValue == null)
                    return new List<string>();
                else
                {
                    return new List<string>() { peopleValue.User.LoginName + ";" + peopleValue.User.Name};
                }
            }
        }

Updating our Page Layout to include out custom controls.

If you open up our ProductPage.aspx, we will now need to Register our new controls. This can be done by adding the following register tag. Ensure your Namespace matches your Namespace for your controls.

<%@ Register TagPrefix="CF" Namespace="CFSP.CustomPageLayoutControls.Controls" Assembly="$SharePoint.Project.AssemblyFullName$" %>

Now within the PublishingWebControls:EditModePanel sections where they are set to display, replace the out the box controls with ours.

<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
<CF:CustomPeoplePickerControl runat="server" FieldName="CFContacts" />
                <!--<SharePoint:UserField ID="UserField1" runat="server" FieldName="CFContacts" />-->;
</PublishingWebControls:EditModePanel>

And

<PublishingWebControls:EditModePanel runat="server" PageDisplayMode="Display">
<CF:CustomTaxonomyControl runat="server" FieldName="CFCountries" />
<!--<Taxonomy:TaxonomyFieldControl ID="TaxonomyFieldControl1" runat="server" FieldName="CFCountries" />-->
</PublishingWebControls:EditModePanel&gt;

That should be it. Upgrade your solution if you deployed earlier, and then refresh your pages that you created based on the CFProduct template, and you should now see that the Contacts and Countries display your new control, like the second screen shot at the start of this post. When you go into edit mode, the original out the box edit control is displayed to allow your publishers to edit the page still.

You can download the full project from my skydrive http://sdrv.ms/1kLsnJY

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, SharePoint | Tagged Managed Metadata, Page Layouts, SharePoint 2010

Accessing Taxonomy Term Store with JSOM

Posted on April 9, 2013 by cann0nf0dder

This blog will show you how to access the Taxonomy Term Store, how to iterate through all the items. How to add a Group, TermSet and Term, and lastly how to delete them. The Taxonomy Term store can be accessed in a SharePoint App, with permission scope of Taxonomy. If you wish to make changes to the Taxonomy Term Store then you will need to ensure to give Write permission. To be able to use the JSOM API for Taxonomy you need to reference SP.Taxonomy.js found in _layouts/15/ the best way to reference this would be in the document ready.


$(document).ready(function () {

var scriptbase = _spPageContextInfo.webServerRelativeUrl + "/_layouts/15/";

$.getScript(scriptbase + "SP.Runtime.js",

function () {

$.getScript(scriptbase + "SP.js", function () {

$.getScript(scriptbase + "SP.Taxonomy.js", function () {

context = SP.ClientContext.get_current();

//Call your code here.

getTermStores();

});

});

});

});

Reading from Taxonomy

Obtaining All the Term Stores.

Typically there is only one term store per web application, but there is always a chance there is more than one. You can get the term store by name if you know it.


function getTermStores() {

    session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);

    termStores = session.get_termStores();

    context.load(session);

    context.load(termStores);

    context.executeQueryAsync(

function(){

termStoresEnum = termStores.getEnumerator();

      var termStores = "Term Stores: /n";

      while (termStoresEnum.moveNext()) {

        var currentTermStore = termStoresEnum.get_current();

        var termStoreID = currentTermStore.get_id();

        var termStoreName = currentTermStore.get_name();

termStores += "Name: " + termStoreName + " ID:" + termStoreID;

}


}, function(){

//failure loading termstores.

});

}

Get the default TermStore

There is also a method to .getDefaultSiteCollectionTermStore(). See my post getDefaultSiteCollectionTermStore() – JavaScript runtime error: Unable to get property ‘toString’ of undefined or null reference if you get a JavaScript error if you try to access any properties after a successful callback.


function defaultTermStore() {

session = SP.Taxonomy.TaxonomySession.getTaxonomySession(context);

termStore = session.getDefaultSiteCollectionTermStore();

context.load(session);

context.load(termStore);

context.executeQueryAsync(successDefaultTermStore, failedListTaxonomySession);

}

Obtaining all the groups for a TermStore

To retrieve all the groups from within a Term Store, you need to have the Term Store ID. You can get any Group by knowing the ID using termStore.getGroup().


function showGroups(termStoreId) {

var termStoresEnum = termStores.getEnumerator();

//Loop through termstores.

while (termStoresEnum.moveNext()) {

var currentTermStore = termStoresEnum.get_current();

//If match load the TermStore.

if (currentTermStore.get_id().toString() == termStoreId.toString()) {

context.load(currentTermStore);

context.executeQueryAsync(

function () {

//Get Groups and load them.

groups = currentTermStore.get_groups();

context.load(groups);

context.executeQueryAsync(

function () {

var groupList = "Groups: \n";

var groupsEnum = groups.getEnumerator();

//Loop through each group.

while (groupsEnum.moveNext()) {

var currentGroup = groupsEnum.get_current();

var groupName = currentGroup.get_name();

var groupId = currentGroup.get_id();

groupList += groupName + ": " + groupId + "\n";

}

alert(groupList);

}, function () {

//failure loading groups

});

}, function () {

//failure loading termStore

});

break;

}

}

}

 

Obtaining all the TermSets for a Group

To retrieve all the term sets from within a Group, you need to have the Group ID. You can get any TermSet by knowing the ID using termStore.getTermSet(<GUID>).


function showTermSets(groupId) {

//we need to load and populate the matching group first, or the term sets that it contains will be inaccessible to our code.

var groupEnum = groups.getEnumerator();

while (groupEnum.moveNext()) {

var currentGroup = groupEnum.get_current();

if (currentGroup.get_id() == groupId) {

context.load(currentGroup);

context.executeQueryAsync(

function(){

//Get Term Sets and load them.

var termSets = currentGroup.get_termSets();

context.load(termSets);

context.executeQueryAsync(

function(){

var termSetEnum = termSets.getEnumerator();

var termSetList = "Term Sets: \n"

while(termSetEnum.moveNext()){

var currentTermSet = termSetEnum.get_current();

var termSetName = currentTermSet.get_name();

var termSetId = currentTermSet.get_id();

termSetList += termSetName + ": " + termSetId + "\n";

}

alert(termSetList);

},

function(){

//Failure loading Term Sets

});

},

function () {

//Failure loading Group.

});

break;

}

}

}

Obtaining all the Terms for a group.

Terms are not as easy to grab, due to the fact that each Term can have a child term, and them terms have children themselves. Therefore in the following code we need a recursive method to ensure we loop through. You can get any Term (from any level) of a Term Set by knowing the ID using termSet.getTerm(<GUID>).


var termsList = "Terms: \n"

function showTerms(termSetId) {

//We need to load and populat the matching Term Set first.

var termSetEnum = termSets.getEnumerator();

while (termSetEnum.moveNext()) {

var currentTermSet = termSetEnum.get_current();

if (currentTermSet.get_id() == termSetId) {

//If termSet Matches, then get all terms.

context.load(currentTermSet);

context.executeQueryAsync(

function () {

//Load terms

var terms = currentTermSet.get_terms();

context.load(terms);

context.executeQueryAsync(

function () {

var termsEnum = terms.getEnumerator();

while (termsEnum.moveNext()) {

var currentTerm = termsEnum.get_current();

var termName = currentTerm.get_name();

var termId = currentTerm.get_id();

termsList += termName + ": " + termId;


//Check if term has child terms

if (currentTerm.get_termsCount() > 0) {

//Term has sub terms.

recursiveTerms(currentTerm, 1);

}

alert(termList);

}

},

function () {

//failure to load terms.

});

},

function () {

//failure to load current term set

});

break;

}

}

}

function recursiveTerms(currentTerm, nestedLoop) {

//Loop count for formatting purpose.

var loop = nestedLoop + 1;

//Get Term child terms

var terms = currentTerm.get_terms();

context.load(terms);

context.executeQueryAsync(

function () {

var termsEnum = terms.getEnumerator();

while (termsEnum.moveNext()) {

var newCurrentTerm = termsEnum.get_current();

var termName = newCurrentTerm.get_name();

termId = newCurrentTerm.get_id();


//Tab Out format.

for (var i = 0; i < loop; i++) {

termsList += "\t";

}

termsList += termName + ": " + termId;

//Check if term has child terms.

if (currentTerm.get_termsCount() > 0) {

//Term has sub terms.

recursiveTerms(newCurrentTerm, loop);

}

}

},

function () {

//failure to load terms

});

}

 

Adding to Taxonomy

Adding a Group to the Term Store.

As long as you have the Term Store ID, this is very simple piece of code.


function addGroup(termStoreId) {

var groupName = "My New Group"

var newGuid = new SP.Guid.newGuid();


var termStore = termStores.getById(termStoreId);

var newGroup = termStore.createGroup(groupName, newGuid.toString());

context.load(newGroup);

context.executeQueryAsync(function () {

//success

},

function () {

//failed

});

}

 

Adding a Term Set to a Group.

You will need the ID to both the Term Store and the Group to perform this function.


function addTermSet(termStoreId, groupId) {

var termSetName = "New TermSet Name"

var newGuid = new SP.Guid.newGuid();


var termStore = termStores.getById(termStoreId);

var group = termStore.getGroup(groupId);

var newTermSet = group.createTermSet(termSetName, newGuid.toString(), 1033);


context.load(newTermSet);

context.executeQueryAsync(function () {

//success

},

function () {

//failed

});

}

Adding a Term to a Term Set.

You will need the ID to both the Term Store and the Term Set to perform this function.


function addTerm(termStoreId, termSetId) {

var termName = "My New Term"

var newGuid = new SP.Guid.newGuid();


var termStore = termStores.getById(termStoreId);

var termSet = termStore.getTermSet(termSetId);

var newTerm = termSet.createTerm(termName, 1033, newGuid.toString());


context.load(newTerm);

context.executeQueryAsync(function () {

//Success

},

function () {

//failed

});

}

 

Adding a Child Term to a Term.

To add a child term, you need to be aware of the Parent Term ID, as well as the Term Store ID and Term Set it belongs to.


function addSubTerm(termStoreId, termSetId, parentTermId) {

var termName = "My Child Term"

var newGuid = new SP.Guid.newGuid();


var termStore = termStores.getById(termStoreId);

var termSet = termStore.getTermSet(termSetId);

var parentTerm = termSet.getTerm(parentTermId);


var newTerm = parentTerm.createTerm(termName, 1033, newGuid.toString());


context.load(newTerm);

context.executeQueryAsync(function () {

//Success

},

function () {

//failed

});

}

Deleting from Taxonomy

Removing Group from Term Store.

With the ID of the Term Store and Group you can successfully delete a group from a Term Store. However, you can only delete a group if there are no term sets within it. Trying so will cause the error “The group has one or more term sets and cannot be deleted. Move or delete all term sets from the group before deleting the Group.” In my example code below I haven’t tested for this for simplicity.


function removeGroup(termStoreId, groupId) {

var termStore = termStores.getById(termStoreId);

var group = termStore.getGroup(groupId);

group.deleteObject();

context.executeQueryAsync(function () {

//success

},

function () {

//failed.

});

}

Removing Term Set from Group.

Needing 3 IDs. Term Store, Group and Term Set you can delete Term Set from the group. If there are terms these will also be deleted, and if they were used within your site they will become orphaned. It might be a good idea to present this warning to your users before allowing the deletion. In my example below I haven’t given this warning for simplicity.


function removeTermSet(termStoreId, groupId, termSetId) {

var termStore = termStores.getById(termStoreId);

var termSet = termStore.getTermSet(termSetId);


termSet.deleteObject();

context.executeQueryAsync(function () {

//success

},

function () {

//failed

});

}

 

Removing Term from Term Set or Parent Term

This method will take care of deleting a Term from any level. Mainly because when you call getTerm(<GUID>) you do so from the Term Set. It doesn’t matter where it is in the term stack, it can be found. Requiring just Term Store, Term Set and Term IDs.


function removeTerm(termStoreId, termSetId, termId) {

var termStore = termStores.getById(termStoreId);

var termSet = termStore.getTermSet(termSetId);

var term = termSet.getTerm(termId);


term.deleteObject();

context.executeQueryAsync(function () {

//success

},

function () {

//failed.

});

}

Hopefully the above will give you enough to be able to manipulate/obtain information from the Term Store through a SharePoint App, or just by using JavaScript.

Share this:

  • Tweet
  • Email
  • WhatsApp
  • More
  • Pocket
  • Print
  • Share on Tumblr

Like this:

Like Loading...
Posted in Development, SharePoint | Tagged JSOM, Managed Metadata, SharePoint 2013, SharePoint Apps | 16 Comments

Top Posts & Pages

  • Connecting to Azure Devops with a Service Principal
  • Viewing, Restoring and Removing Items from the SharePoint Recycle Bin - The attempted operation is prohibited because it exceeds the list view threshold enforced by the administrator.
  • Getting all MS Teams User Policies using PowerShell
  • SPFX obtaining the URL Query Parameters
  • Unable to see public Teams within ‘Join or create a team’ section of MS Teams
  • Finding the related Site from Teams Private Channel Site
  • Using HTML template and JQuery.clone()
  • Externally Sharing – GetSharingInformation REST API

Archives

Categories

  • Development (158)
  • General (7)
  • Hyper V (19)
  • MS Teams (7)
  • Office (6)
  • PowerShell (55)
  • SharePoint (134)
  • SQL (12)
  • Virtual Machines (3)
  • VM Workstation (2)
  • Windows (13)

Meta

  • Register
  • Log in
  • Entries feed
  • Comments feed
  • WordPress.com

Follow me on Twitter

Tags

Azure Blogging Business Conectivity Services Certificates Content Types CrossDomain Development Dev Machine DevOps Display Templates Extensions Externally Sharing Hyper V JavaScript JQuery Kerberos Managed Metadata Managed Metadata Service Manage Services MS Teams Nintex O365 Groups Office Office 365 PnP PowerShell PowerShell Script REST Ribbon Search SharePoint SharePoint 2010 SharePoint 2013 SharePoint 2016 SharePoint Apps SharePoint Designer SharePoint Online SQL SQL 2012 SQL 2016 Term Store Upgrade Upgrading Visual Studio VMWare Windows 8 Windows 10 Windows Server 2012 Windows Server 2012 R2 Workflows

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 433 other subscribers

SharePoint and other Geeky Stuff

  • RSS - Posts
  • RSS - Comments

Blog Stats

  • 1,305,236 hits
Create a free website or blog at WordPress.com.
Privacy & Cookies: This site uses cookies. By continuing to use this website, you agree to their use.
To find out more, including how to control cookies, see here: Cookie Policy
  • Follow Following
    • SharePoint and other geeky stuff
    • Join 96 other followers
    • Already have a WordPress.com account? Log in now.
    • SharePoint and other geeky stuff
    • Customize
    • Follow Following
    • Sign up
    • Log in
    • Report this content
    • View site in Reader
    • Manage subscriptions
    • Collapse this bar
 

Loading Comments...
 

    %d bloggers like this: