Currently there are no direct methods to enable Audience Targeting on a List or Library via script or code.

This is currently done through the List or Library Settings screen under Audience targetting settings option

When you enable audience targeting through these settings, a field is created in the corresponding List or Library called Target Audiences

Using the Get-PnpField PnP PowerShell command to get the SchemaXml property we can see how the field is constructed.

1
(Get-PnPField -Identity "Target Audiences" -List Documents).SchemaXml

This is the XML returned

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

<Field
ID="{61cbb965-1e04-4273-b658-eedaa662f48d}"
Type="TargetTo"
Name="Target_x0020_Audiences"
DisplayName="Target Audiences"
Required="FALSE"
SourceID="{803a362c-54fb-47c7-83c8-e7a83a9512f8}"
StaticName="Target_x0020_Audiences"
ColName="ntext2"
RowOrdinal="0" Version="2">
<Customization>
<ArrayOfProperty>
<Property>
<Name>AllowGlobalAudience</Name>
<Value xmlns:q1="http://www.w3.org/2001/XMLSchema" p4:type="q1:bo
olean" xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">true</Value>
</Property>
<Property>
<Name>AllowDL</Name>
<Value xmlns:q2="http://www.w3.org/2001/XMLSchema" p4:type="q2:boolean" xmlns:p4="http://www.w
3.org/2001/XMLSchema-instance">true</Value>
</Property>
<Property>
<Name>AllowSPGroup</Name>
<Value xmlns:q3="http://www.w3.org/2001/XMLSchema" p4:type="q3:boolean" xmlns:p4="http://www.w3.org/2001/XMLSchema-ins
tance">true</Value>
</Property>
</ArrayOfProperty>
</Customization>
</Field>

After some experimenting, I found that the only two columns you need to set are the ID and Type

ID needs to have 61cbb965-1e04-4273-b658-eedaa662f48d set as it’s value

Type needs to have TargetTo set as it’s value

You still need to provide values for Name, DisplayName, StaticName, but they can be whatever you want.

Don’t like the name Target Audiences then you can change it to something else. The great thing is, it can still be turned off through the List or Library settings page.

To enable Audinence targeting using PnP PowerShell it’s as simple as running this script.

1
2
3
4
5
6

$credentials = (Get-Credential)

Connect-PnPOnline -Url https://contoso.sharepoint.com/sites/somesite -Credentials $credentials

Add-PnPFieldFromXml '<Field ID="{61cbb965-1e04-4273-b658-eedaa662f48d}" Type="TargetTo" Name="Target_x0020_Audiences" StaticName="Target_x0020_Audiences" DisplayName="Target Audiences" />' -List Documents

And to do the same thing in C# CSOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

// from SharePointPnPCoreOnline Nuget package
var manager = new OfficeDevPnP.Core.AuthenticationManager();

string siteUrl = "https://contoso.sharepoint.com/sites/somesite";
string appId = "your-app-id";
string appSecret = "your-app-secret";

using (var context = manager.GetAppOnlyAuthenticatedContext(siteUrl, appId, appSecret))
{

var list = context.Web.Lists.GetByTitle("Documents");

var fieldSchemaXml = @"
<Field
ID='{61cbb965-1e04-4273-b658-eedaa662f48d}'
Type='TargetTo'
Name='Target_x0020_Audiences'
StaticName='Target_x0020_Audiences'
DisplayName='Target Audiences'
/>
";

var field = list.Fields.AddFieldAsXml(fieldSchemaXml, false, AddFieldOptions.AddFieldInternalNameHint);
context.ExecuteQueryRetry();
}

To turn off Audience Targeting all you need to do is remove the field from the list.

Comment and share

I’ve often had times, where I’ve migrated a whole load of documents from a File Server or some other ECMS. These exported documents are normally in major published state and need to be in the same state in SharePoint (i.e. Published!).

Sweet and simple, but very effective.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$sites = "https://contoso.sharepoint.com/sites/site1", "https://contoso.sharepoint.com/sites/site1"

foreach ($site in $sites)
{
Connect-PnPOnline -Url $site -UseWebLogin

$list = Get-PnPList -Identity "Documents"

$items = (Get-PnPListItem -List $list -Fields "Title","GUID","FileRef").FieldValues

foreach ($item in $items)
{
$file = $item.FileRef

Set-PnPFileCheckedIn -Url $file -CheckinType MajorCheckIn -Approve
}
}

Comment and share

This post is about creating a ListView CommandSet extension which uses the the onInit method to make “service” calls.

If you want to dive straight in, the source code for this post is available from my-command-set GitHub repository.

Full details can be found on creating an Extension solution can be found on the SharePoint Framework Extension Documentation Site, but I’ve given a whistle stop tour below.

Creating an Extension project

This is done using the Yeoman SharePoint Generator in a terminal of your choice.

1
2
3
md my-command-set
cd app-extension
yo @microsoft/sharepoint

Enter the following options

1
2
3
4
5
6
7
8
9
10
? What is your solution name? my-command-set
? Which baseline packages... ? SharePoint Online only (latest)
? Where do you ... files? Use the current folder
? Do you want ... tenant admin ... in sites? No
? Will the components ... APIs ... tenant? No
? Which type of client-side component to create? Extension
? Which type of client-side extension to create? ListView Command Set
Add new Command Set to solution my-command-set.
? What is your Command Set name? SecurableCommandSet
? What is your Command Set description? SecurableCommandSet description

Wait for Yeoman to do it’s thing…

And then wait a little longer…

I use Visual Studio Code so if you’re joining me on this journey…

1
code .

Hopefully you’ll see these files in your repo

First of all lets setup the CommandSet in the manifest.

  • Navigate to src\extensions\securableCommandSet folder
  • Open the SecurableCommandSetCommandSet.manifest.json file

Updating the project files

Remove the two commands that were created with the project

1
2
3
4
5
6
7
8
9
10
11
12
"items": {
"COMMAND_1": {
"title": { "default": "Command One" },
"iconImageUrl": "icons/request.png",
"type": "command"
},
"COMMAND_2": {
"title": { "default": "Command Two" },
"iconImageUrl": "icons/cancel.png",
"type": "command"
}
}

Replace them with the command that’s going to be secured

1
2
3
4
5
6
7
"items": {
"CMD_SECURE": {
"title": { "default": "Secret Command" },
"iconImageUrl": "data:image...",
"type": "command"
}
}

Full base64-encoded image is included with source code

Next open up SecurableCommandSetCommandSet.ts

Add a private field to the classe to store the visibility of the command.

1
private isInOwnersGroup: boolean = false;

We want to make sure the command is only visible to people who are in the Owners group of the site we are in.

This is done in the onListViewUpdated method of the SecurableCommandSetCommandSet class.

Below is the code added when the project is created.

1
2
3
4
5
6
7
8
@override
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
const compareOneCommand: Command = this.tryGetCommand('COMMAND_1');
if (compareOneCommand) {
// This command should be hidden unless exactly one row is selected.
compareOneCommand.visible = event.selectedRows.length === 1;
}
}

Replace it with following

1
2
3
4
5
6
7
8
9
@override
public onListViewUpdated(event: IListViewCommandSetListViewUpdatedParameters): void {
const compareSecureCommand: Command = this.tryGetCommand('CMD_SECURE');
if (compareSecureCommand) {

compareSecureCommand.visible = this.isInOwnersGroup;
}

}

Install the PnP client side libraries as we’re going to need some of their magic in this solution

1
npm i @pnp/sp @pnp/common @pnp/logging @pnp/odata --save

SharePoint Groups and their members aren’t available in the BaseListViewCommandSet.context property, so we’re going to need to load them.

The problem is that this will have to be done using Promises and onListViewUpdated doesn’t return a promise.

Luckily we have the onInit method for this (returns Promise<void>). This method gets called when you component is initialised (Basically when the list view is loaded up in the page). Anything in the onInit method will run before the commands are rendered, similar to the actions you’d run in the componentWillMount method of a react component.

To use the pnpjs library it needs to be initialised first and this needs to be done in the onInit method.

Add the import statement

1
import { sp } from "@pnp/sp";

Replace the onInit method with the following code, this sets up the sp helper with context of the Extension and then to call into the SharePoint Groups in the site, we’re going to have to await away in the onInit method again to call into the site and set the isInOwnersGroup field.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@override
public async onInit(): Promise<void> {

await super.onInit();

await sp.setup({ spfxContext: this.context });

const email: string = this.context.pageContext.user.email;
const ownerGroup: SiteGroup = sp.web.associatedOwnerGroup;
const users: SPUser[] = await ownerGroup.users.get();

this.isInOwnersGroup = users.some((user: any) => user.Email === email);

return Promise.resolve<void>();
}

For the observant people out there, you may notice that I’ve declared user as any. For some reason, the users collection returned has UpperCamelCase properties and the TypeScript reference is using lowerCamelCase, which was causing a TypeScript compile error. Hence the user.Email === email rather than user.email === email in the some function call.

In this snippet of code, we’re getting the login of the current user, the associated owner group of the site and then getting the users in the group.

The some function determines if the user is in the group and it’s result sets the isInOwnersGroup.

Finally an update is needed on the onListViewUpdated method to show / hide the command.

1
2
3
4
5
6
const compareSecureCommand: Command = this.tryGetCommand('CMD_SECURE');

if (compareSecureCommand) {

compareSecureCommand.visible = this.isInOwnersGroup;
}

Add the new command to the onExecute method to make sure it gets picked up in the switch statement

1
2
3
4
5
6
7
switch (event.itemId) {
case 'CMD_SECURE':
Dialog.alert("Shhhhhh! It's a secret...");
break;
default:
throw new Error('Unknown command');
}

Now we’re ready to gulp serve

Open up serve.json in the config directory

Change the two pageUrl properties to a list in your tenant

1
"pageUrl": "https://contoso.sharepoint.com/sites/mySite/SitePages/myPage.aspx"

Currently there is a bug in tenants that are on First Release that stops gulp serve working correctly. If you can’t see your command then switch to Standard (not always instant!), if it still doesn’t work then try deploying without the --ship paramter. See all the details on the sp-dev-docs GitHub repository

Make sure you account is in the Owners group (It won’t be if you created a “groupified” team site).

If all is good your new CommandSet should appear on the top menu and will show the alert message when clicked.

Summary

Getting the SharePoint group users is just an example of how you can use the onInit method to call into other services, like custom web apis, MS Graph, etc.

Remember that this could effect the load time of your command, which may effect the user experience. The context menu may not be on screen for long, so your menu may have not loaded before it’s gone.

Comment and share

Finally got round to migrating from the old WordPress site at statto1974.wordpress.com to an all new shiny site using Hexo hosted as a static site in Azure Storage from wutek.co.uk

It was a “fairly” painless process.

Setting up the site with Hexo

Make sure you’ve got Node.js and Git installed.

Install Hexo with the following command

1
$ npm install -g hexo-cli

Run the following commands

1
2
3
$ hexo init <folder>
$ cd <folder>
$ npm install

Migrate posts and media

  • On the WordPress site dashboard, go to Tools > Export
  • Click on Export All, download the ZIP file and extract to a folder

Install the hexo-migrator-wordpress plugin

1
$ npm install hexo-migrator-wordpress --save

Run the following command

1
$ hexo migrate wordpress <source>

Where source is the file path or URL to the WordPress export file.

You’ll need to do some tidying up of the posts and linking images

Publishing to Azure

Once you’re happy with the posts, you need to generate the static site in Hexo

1
$ hexo generate

Publish the public folder to Azure Storage in Visual Studio Code (You’ll need to have the Azure Extension installed)

  • Right click the public folder
  • Select the Deploy to Static Website…
  • Select a storage account or create a new one
  • Enable static website if requested
  • Wait a while for files to upload
  • When finished you’ll be shown the site link

Further resources

Comment and share

When loading this page in Central Administration, I was being presented with a Excel Services error and a correlation ID.

Looking at the ULS logs I noticed the following error.

Insufficient SQL database permissions for user 'Name: <DOMAIN\USERNAME> SID: <SID-CODE> ImpersonationLevel: None' in database '<DATABASE-NAME>' on SQL Server instance '<SQL-INSTANCE>'. Additional error information from SQL Server is included below. The EXECUTE permission was denied on the object 'proc_ReturnWebFeatures', database '<DATABASE-NAME>', schema 'dbo'. 1d0d479c-5a36-c0dc-912b-5bc267b09a0a

The database name was referring to the Content Admin database of Central Administration and the user name was referring to the service application account that the PowerPivot service application had been configured to run under.

The proc_ReturnWebFeatures stored procedure required the user to be a member of the SPDataAccess role to allow EXECUTE permissions. The service application user was only in the WSS_Content_Application_Pools role.

If you run the following script, this will grant the correct rights in the database

1
2
3
$url = "[http://centraladminsite:2013](http://centraladminsite:2013)"
$webApp = Get-SPWebApplication -Identity $url
$webApp.GrantAccessToProcessIdentity("DOMAIN\serviceapplicationuser")

Now when you load up the PowerPivot Management Dashboard, you report should be displayed.

Comment and share

This turned out to be harder than I thought.

Metadata navigation and per-location views are an little known, but powerful way of making lists more useful.

It allows you to assign a default view and others views to a folder, content type or field. My need was to allow users to navigate using a Taxonomy field. Dependent on the selected field, I would like to show different fields using a view.

The first part is to add the Metadata navigation.

This is done by creating a hierarchy using MetadataNavigationHierarchy

1
2
3
4
5
6
7
8
9
var list = SPContext.Current.Web.Lists.TryGetList("MyList");
var field = list .Fields.TryGetFieldByStaticName("MyField");
var settings = MetadataNavigationSettings.GetMetadataNavigationSettings(list); var hierarchy = settings.FindConfiguredHierarchy(field.Id);

if (hierarchy == null) {
hierarchy = new MetadataNavigationHierarchy(field); settings.AddConfiguredHierarchy(hierarchy);
}

MetadataNavigationSettings.SetMetadataNavigationSettings(list, settings);

The hard part is adding the settings for the per location views.

This can only be done by injecting XML into the settings XML. MetadataNavigationSettings is a wrapper class around an XML snippet that is stored in a hidden property of the root folder of the list.

Have a look at SPList.RootFolder.Properties["client_MOSS_MetadataNavigationSettings"]

The XML Schema is as follows. Haven’t found anything documenting this schema on MSDN yet, so this is just taken from my configuration of my list, so may differ on yours

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<MetadataNavigationSettings SchemaVersion="1" IsEnabled="True" AutoIndex="True"> 
<NavigationHierarchies>
<FolderHierarchy HideFoldersNode="True" />
<MetadataField FieldID="a96cea49-ef78-4bfa-8a69-2c49071155fb" FieldType="TaxonomyFieldType" CachedName="Legal_x0020_Document_x0020_Category" CachedDisplayName="Legal Document Category">
<ViewSettings UniqueNodeId="eadcb112-33af-4700-ad11-8e6afbd800e6">
<View ViewId="4f4d05f4-046f-4bfa-a37c-170847fd4e34" CachedName="My Custom View" Index="0" CachedUrl="Store/Forms/My Custom View.aspx" />
<View ViewId="a1ab958f-40d5-4e4a-ac3f-a10bc3cd22d2" CachedName="My Other Custom View" Index="1" CachedUrl="Store/Forms/AllItems.aspx" />
<View ViewId="5c9447a3-ff59-419a-92b1-c7f0191d6f82" CachedName="Not visisible view" Index="-1" CachedUrl="Store/Forms/Not visisible view.aspx" />
</ViewSettings>
</MetadataField>
</NavigationHierarchies>
<KeyFilters />
<ManagedIndices>
<ManagedIndex IndexID="a96cea49-ef78-4bfa-8a69-2c49071155fb" IndexFieldName="Legal_x0020_Document_x0020_Category" IndexFieldID="a96cea49-ef78-4bfa-8a69-2c49071155fb" />
</ManagedIndices>
<ViewSettings UniqueNodeId="">
<View ViewId="a1ab958f-40d5-4e4a-ac3f-a10bc3cd22d2" CachedName="All Documents" Index="0" CachedUrl="Store/Forms/AllItems.aspx" />
</ViewSettings>
</MetadataNavigationSettings>

The part I’m interested in here is the ViewSettings and View tag. The UniqueId attribute relates to the GUID of the selected Term GUID. So this will show the views defined using the View tag when the Term is selected in the Metadata navigation.

If a View tag is added with 0 index this will be used as the default view when the term is selected, all other positive numbers will be shown in the order defined as other available views for that Term. Any negatives will not be available (You don’t need to add them)

I used the following code to add these nodes programmaticallty using XLinq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var list = SPContext.Current.Web.Lists.TryGetList("MyList"); 
var view = list.Views.Cast().SingleOrDefault(v => v.Title == "MyView");
var session = new TaxonomySession(SPContext.Current.Site);
var field = list.Fields.TryGetFieldByStaticName("MyField");
var term = session.GetTerm("a96cea49-ef78-4bfa-8a69-2c49071155fb");
var settings = MetadataNavigationSettings.GetMetadataNavigationSettings(list); var doc = XDocument.Parse(settings.SettingsXml);

var metaDataField =
(
from f in doc.Descendants("MetadataField")
let fieldId = f.Attribute("FieldID")
where fieldId != null && fieldId.Value == field.Id.ToString()
select f
).SingleOrDefault();

if (metaDataField != null)
{
var viewSettings =
(
from v in metaDataField.Elements("ViewSettings")
let uniqueNodeId = v.Attribute("UniqueNodeId")
where uniqueNodeId != null && uniqueNodeId.Value == term.Id.ToString() select v
).SingleOrDefault();

if (viewSettings == null)
{
metaDataField.Add(
new XElement("ViewSettings",
new XAttribute("UniqueNodeId", term.Id.ToString()),
new XElement("View",
new XAttribute("ViewId", view.ID.ToString()),
new XAttribute("CachedName", view.Title),
new XAttribute("Index", "0"),
new XAttribute("CachedUrl", view.Url)
)
)
);
}
}

settings = new MetadataNavigationSettings(doc.ToString()); MetadataNavigationSettings.SetMetadataNavigationSettings(list, settings);

Comment and share

THIS IS VERY BAD. WE ALL KNOW THAT MAKING DIRECT UPDATES TO THE DATABASE WILL CREATE SOME SORT OF BLACK HOLE AND KITTENS DIE IN REDMOND ETC, ETC. I HAVEN’T TESTED THE LONG TERM EFFECTS AND ONLY DID THIS ON MY DEVELOPMENT ENVIRONMENT TO GET ME OUT OF A PICKLE.

Disclaimer over.

If you ever get this error when trying to update a field, either through the web UI or programmatic-ally it normally means your Version fields are out of sync. This can occur if your field has been created using the Schema XML from another environment or you have migrated content using some dodgy 3rd party app.

Run this update on your content databases and you all should be good again.

UPDATE [MyContentDatabase].[dbo].[ContentTypes] SET [Version] = CAST([Definition] AS XML).value('(/Field/@Version)[1]', 'int') WHERE CAST([Definition] AS XML).value('(/Field/@Version)[1]', 'int') <> [Version]

Comment and share

I had a need to show the path of terms, starting from a specific term and then showing the path to the final children.

This would involve traversing the structure as the depth is unknown, so I wrote this piece of code to accomplish this using the Client Components.

Please feel free to steal and improve (Only if you give back the improved code though!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private IEnumerable<Path> Paths(bool includeRoot, Guid termId, string siteUrl)
{
var paths = new List<Path>();

Action<ClientContext, Term, String> traverse = null;

traverse = (context, term, path) =>
{
context.Load(term, t => t.Id, t => t.Name, t => t.Terms);
context.ExecuteQuery();

if(!(term.Id == termId && !includeRoot))
path += path == string.Empty ? term.Name : " > {0}".ToFormat(term.Name);

if (term.Terms.Count == 0)
{
paths.Add(new Path
{
TermId = term.Id,
PathText = path
});
}
else
{
foreach(var st in term.Terms)
{
traverse(context, st, path);
}
}
};

using(var context = new ClientContext(siteUrl))
{
var rootTerm = TaxonomySession.GetTaxonomySession(context).GetTerm(termId);

traverse(context, rootTerm, string.Empty);
}

return paths;
}

public class Path
{
public Guid TermId
{
get;
set;
}

public string PathText
{
get;
set;
}
}

Comment and share

If you have a field called “Folder” it will not be available in the returned ListItem object

e.g. listItem["Folder"].ToString()

The inner workings of the ListItem object uses an ExpandoObject to store the properties. It seems it mixes this up in the FieldValues Collection with all your custom fields. The Folder property then takes the value from the FieldValues collection to make it available to the Folder property. Thus making your own Folder field “disappear”

Here is how you can replicate it.

  • Create a list based on the Custom List template
  • Add a column called “Folder” and make it a text field
  • Add a column called “DisplayName” and make it a text field
  • Add a column called “MyField” and make it a text field

Add a couple of dummy rows of data

Create a console application in VS that references Microsoft.SharePoint.Client and Microsoft.SharePoint.Client.Runtime

Add the following code to the Main method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using (var context = new ClientContext("http://siteurl"))
{
var query = CamlQuery.CreateAllItemsQuery();
var listItems = context.Web.Lists.GetByTitle("Testing").GetItems(query);

context.Load(listItems);
context.ExecuteQuery();

foreach (var listItem in listItems)
{
Console.WriteLine("Title: {0}", listItem["Title"]);
Console.WriteLine("Folder: {0}", listItem["Folder"]);
Console.WriteLine("DisplayName: {0}", listItem["DisplayName"]);
Console.WriteLine("MyField: {0}", listItem["MyField"]);
}
}

Here is the code from .Net Reflector that shows the ListItem object populating the properties. It’s also worth noting that the other properties detailed in this method will have the same problem, but it’s unlikely that you’ll call a field FileSystemObject!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
protected override bool InitOnePropertyFromJson(string peekedName, JsonReader reader)
{
bool flag = base.InitOnePropertyFromJson(peekedName, reader);
if (!flag)
{
switch (peekedName)
{
case "AttachmentFiles":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("AttachmentFiles", this.AttachmentFiles, reader);
this.AttachmentFiles.FromJson(reader);
return flag;

case "ContentType":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("ContentType", this.ContentType, reader);
this.ContentType.FromJson(reader);
return flag;

case "DisplayName":
flag = true;
reader.ReadName();
base.ObjectData.Properties["DisplayName"] = reader.ReadString();
return flag;

case "EffectiveBasePermissions":
flag = true;
reader.ReadName();
base.ObjectData.Properties["EffectiveBasePermissions"] = reader.Read<BasePermissions>();
return flag;

case "EffectiveBasePermissionsForUI":
flag = true;
reader.ReadName();
base.ObjectData.Properties["EffectiveBasePermissionsForUI"] = reader.Read<BasePermissions>();
return flag;

case "FieldValuesAsHtml":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("FieldValuesAsHtml", this.FieldValuesAsHtml, reader);
this.FieldValuesAsHtml.FromJson(reader);
return flag;

case "FieldValuesAsText":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("FieldValuesAsText", this.FieldValuesAsText, reader);
this.FieldValuesAsText.FromJson(reader);
return flag;

case "FieldValuesForEdit":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("FieldValuesForEdit", this.FieldValuesForEdit, reader);
this.FieldValuesForEdit.FromJson(reader);
return flag;

case "File":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("File", this.File, reader);
this.File.FromJson(reader);
return flag;

case "FileSystemObjectType":
flag = true;
reader.ReadName();
base.ObjectData.Properties["FileSystemObjectType"] = reader.ReadEnum<FileSystemObjectType>();
return flag;

case "Folder":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("Folder", this.Folder, reader);
this.Folder.FromJson(reader);
return flag;

case "Id":
flag = true;
reader.ReadName();
base.ObjectData.Properties["Id"] = reader.ReadInt32();
return flag;

case "ParentList":
flag = true;
reader.ReadName();
base.UpdateClientObjectPropertyType("ParentList", this.ParentList, reader);
this.ParentList.FromJson(reader);
return flag;
}
}
return flag;
}

Here’s a link in TechNet Forums to the initial discussion and a link to the reported bug

Comment and share

There are two types of ampersands that you need to be aware of when playing with SharePoint Taxonomy

Our favorite and most loved

& ASCII Number: 38

And the impostor

& ASCII Number: 65286

After reading this article by Nick Hobbs, it became apparent that when you create a term it replaces the 38 ampersand with a 65286 ampersand.

This then becomes a problem if you want to do a comparison with your original source (spreadsheet, database, etc) as they are no longer the same.

As detailed in Nick’s article, you can use the TaxonomyItem.NormalizeName method to create a “Taxonomy” version of your string for comparison.

Below is the code I used in the SharePoint 2013 Client Component which is a little different from the server code.

1
2
3
4
5
6
7
8
9
10
string myString = "This contains &";

using(var context = new ClientContext("http://myurl"))
{
var result = TaxonomyItem.NormalizeName(context, myString);

context.ExecuteQuery();

string normalisedString = result.Value;
}

Comment and share

Author's picture

Toby Statham

Independent Office 365 / SharePoint Specialist and an associate consultant at aiimi.com, an Information Management company.

Independent Office 365 / SharePoint Specialist

Brighton, UK