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

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

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

You may have the need to run some code in a SPServiceContext of a specific user. e.g. when dealing with user profiles or activities.

This can be achieved by using a combination of the SPUser, WindowsIdentity, GenericPrincipal and the GetContext method of SPServiceContext.

From these we can set the HttpContext.Current.User property to the specified user.

Don’t forget to set the context back after the code has executed!

Below is a helper method that makes this very easy.

I haven’t done any solid testing on performance etc. so the usual caveats apply.

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
97
98
99

// Example calling code

RunAs("http://asharepointsite", "THEDOMAIN\THEUSER", c =>
{
// The code to execute. e.g. get a the UserProfileManager as the passed in user
var upm = new UserProfileManager(c, true);

// Do some stuff to the UPM as the passed in user
});

// Helper method

private static void RunAs(string siteUrl, string userName, Action<SPServiceContext> contextToUse)
{
try
{
var currentSetting = false;
var currentHttpContext = HttpContext.Current;
SPUser currentUser = null;

if (SPContext.Current != null)
{
if (SPContext.Current.Web != null)
{
currentUser = SPContext.Current.Web.CurrentUser;
}
}

SPSecurity.RunWithElevatedPrivileges(delegate
{
using (var site = new SPSite(siteUrl))
{
using (var web = site.OpenWeb())
{
try
{
var user = web.EnsureUser(userName);

currentSetting = web.AllowUnsafeUpdates;
web.AllowUnsafeUpdates = true;

var request = new HttpRequest("", web.Url, "");

HttpContext.Current = new HttpContext(request,
new HttpResponse(
new StringWriter(CultureInfo.CurrentCulture)));

HttpContext.Current.Items["HttpHandlerSPWeb"] = web;

var wi = WindowsIdentity.GetCurrent();

var newfield = typeof (WindowsIdentity).GetField("m_name",
BindingFlags.NonPublic |
BindingFlags.Instance);

if (newfield != null) newfield.SetValue(wi, user.LoginName);

if (wi != null) HttpContext.Current.User = new GenericPrincipal(wi, new string[0]);

var elevatedContext = SPServiceContext.GetContext(HttpContext.Current);

contextToUse(elevatedContext);

//Set the HTTPContext back to "normal"
HttpContext.Current = currentHttpContext;

if (currentUser != null)
{
var winId = WindowsIdentity.GetCurrent();

var oldField = typeof (WindowsIdentity).GetField("m_name",
BindingFlags.NonPublic |
BindingFlags.Instance);

if (oldField != null) oldField.SetValue(winId, currentUser.LoginName);

if (winId != null)
HttpContext.Current.User = new GenericPrincipal(winId, new string[0]);
}
}
catch (Exception ex)
{
// Log or whatever
}
finally
{
web.AllowUnsafeUpdates = currentSetting;
}
}
}
});

}
catch (Exception exO)
{
// Log or whatever
}
}

Comment and share

If anyone knows a better way then let me know.

1
2
3
4
5
6
7
8
9
10
11
string smtp = string.Empty;

SPFarm.Local.Servers.ForEach(s => s.ServiceInstances.ForEach(i =>
{
if(i is SPOutboundMailServiceInstance)
{
smtp = s.Address;
}
}));

return smtp;

I use a ForEach extension which I’ve included below

1
2
3
4
5
6
7
public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
{
foreach(T item in enumerable)
{
action(item);
}
}

Comment and share

To match the ScopeId in the EventData property of the SPAuditEntry to a SPListItem you need to use the hidden column ScopeId and not the Id property of SPListItem.

Here is what I mean.

The following XML is returned in the EventData proprty for a SPAuditEventType of SecRoleBindUpdate. To match the Scope returned in the XML to SPSite, SPWeb and SPList object you would use the Id property of this object, but not for the SPListItem.

1
2
3
<roleid>1073741826</roleid>
<principalid>11</principalid>
<scope>72EEC412-B14B-4EFB-AB95-EA821A3A4C63</scope>

You need to use the ScopeId column of the SPListItem

So why can’t I just use the SPAuditQuery.RestrictToListItem method to return the audit entries for that SPListItem.

It doesn’t work, that’s why and this has been confirmed by MS Support.

Here is a workaround that will link the ScopeId to the SPListItem that triggered the audit entry

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
97
98
99
100
101
102
103
104
105
106
107
108
109
using System; 
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

namespace ConsoleApplication1
{  
class Program  
{  
static void Main(string[] args)  
{
string siteUrl = "http://url";  
string listName = "TheList";  
string scopeIdFromItem = string.Empty;  
string scopeIdFromChange = string.Empty;

try  
{  
using (SPSite site = new SPSite(siteUrl))  
{  
using (SPWeb web = site.OpenWeb())  
{
SPList list = web.Lists[listName];
SPRegionalSettings regionalSettings = web.RegionalSettings;  
SPTimeZone timeZone = regionalSettings.TimeZone;
SPAuditQuery query = new SPAuditQuery(site);  
SPAuditEntryCollection auditEntries = site.Audit.GetEntries(query);

foreach (SPAuditEntry auditEntry in auditEntries)  
{  
if (IsSecurityEvent(auditEntry))  
{  
try  
{  
if (auditEntry.Event.ToString().ToUpper() == "SECROLEBINDUPDATE")  
{
// Should really do a proper XML query here, but for demo just do some string stuff
scopeIdFromChange = auditEntry.EventData.ToString().Substring((auditEntry.EventData.ToString().Length - 44), 36);
foreach (SPListItem item in list.Items)  
{ // Square brackets round the scope id need to be removed
scopeIdFromItem = item["ScopeId"].ToString().Substring(1, 36);

if (scopeIdFromChange == scopeIdFromItem)  
{  
Console.Write("Event Data : ");
Console.WriteLine(auditEntry.EventData.ToString());  
Console.Write("Occurred : ");
Console.WriteLine(timeZone.UTCToLocalTime(auditEntry.Occurred));

if (list.BaseType == SPBaseType.DocumentLibrary)  
{  
Console.WriteLine("Security changed on file: " + item.File.ToString());  
}  

if (list.BaseType == SPBaseType.GenericList)  
{  
Console.WriteLine("Security changed on item: " + item.Name.ToString());  
}  
}  
else  
{  
Console.WriteLine("No item found in list " + siteUrl + "/" + listName + " for " + auditEntry.EventData.ToString());  
}  
}  
}  
}  
catch (Exception ex)  
{  
Console.WriteLine(ex.Message.ToString());  
}
}  
}  
}  
}  
}  
catch (Exception ex)
{  
Console.WriteLine(ex.Message);  
}  
}

private static bool IsSecurityEvent(SPAuditEntry entryToTest)  
{  
SPAuditEventType[] SECURITY_EVENTS = new SPAuditEventType[]  
{  
SPAuditEventType.SecGroupCreate,  
SPAuditEventType.SecGroupDelete,  
SPAuditEventType.SecGroupMemberAdd,  
SPAuditEventType.SecGroupMemberDel,  
SPAuditEventType.SecRoleBindBreakInherit,  
SPAuditEventType.SecRoleBindInherit,  
SPAuditEventType.SecRoleBindUpdate,  
SPAuditEventType.SecRoleDefBreakInherit,  
SPAuditEventType.SecRoleDefCreate,  
SPAuditEventType.SecRoleDefDelete,  
SPAuditEventType.SecRoleDefModify  
};

foreach (SPAuditEventType eventType in SECURITY_EVENTS)  
{  
if (eventType == entryToTest.Event)
return true;  
}
return false;
}
}
}
}
}

Comment and share

  • page 1 of 1
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