Jason Lee's Blog

Tuesday, 6 December 2016

A quick post today on a fairly common problem - you're given a site collection on Office 365 that has grown organically with hundreds of nested sites, four or five levels deep in places. The various site owners have used many different versions of your company logo, and you want to reintroduce a degree of consistency by applying the same logo to every site in a site collection.

The most efficient way to do this is to run some client-side code from PowerShell. Basically you need to iterate over the subsites of a given site and set the Web.SiteLogoUrl property on every site. The easiest way to do this is to:

# Connect to the site collection, then call updateSiteLogo on
the root web

Connect-PnPOnline-Url$rootSiteUrl-Credentials$credentialManagerLabel

$rootweb=Get-PnPWeb

Write-Output"Setting
site logos..."

updateSiteLogo($rootweb.ServerRelativeUrl)

Write-Output"Done."

The Get-PnPWeb cmdlet actually has its own Recurse option, and you could use that if you want to rather than doing your own recursion. I steered away from it because it gets all the subsites in one hit (the right approach in most circumstances), which can cause the script to hang for a while if you've got hundreds of nested sites. This approach instead walks the site structure one level at a time.

Tuesday, 5 July 2016

Using the JavaScript Object Model to look up the manager of the current user (from the User Profile Service) seems to be a fairly common requirement - I've had to do it at least three times in the last couple of months.

First of all, you need to load the profile properties for the current user:

Incidentally, the trickiest part of all this can be getting the SharePoint script files to load in the right order. You can't run your code until sp.userprofiles.js is loaded, and you can't load sp.userprofiles.js until sp.js is loaded. The sp.userprofiles.js library seems to be particularly awkward to load. I usually use the following pattern, cobbled together from various helpful forum posts:

A couple of years ago, I posted on how to create SharePoint site columns using JavaScript. More recently, we needed to add a site column to a list in SharePoint Online as part of a scripted provisioning exercise. There were a few aspects of this that took a bit of trial and error, such as:

Retrieving the site column from the root site.

Getting the field to show up on the default list view.

Hiding the field from various forms.

So I figured it's probably worth sharing the end-to-end code (sanitised and simplified). First of all, let's create a site column named JasonNotes on the root site in the site collection:

The next stage is to add the site column to our list. At this point, we need to ensure that the field also gets added to the default list view. This is also a good opportunity to set any properties you require on the list field, such as whether you want it to appear on forms:

listTarget = web.get_lists().getByTitle(listTitle); // Add the field to the field collection for the list var listFields = listTarget.get_fields();var listFldJasonNotes =
listFields.add(fldJasonNotes); // Show the field only on the New formlistFldJasonNotes.setShowInDisplayForm(false);listFldJasonNotes.setShowInEditForm(false);listFldJasonNotes.setShowInNewForm(true);listFldJasonNotes.update();// Add the JasonNotes field to the
default view var defaultView = listTarget.get_defaultView(); var defaultViewFields =
defaultView.get_viewFields(); defaultViewFields.add("JasonNotes"); defaultView.update();listTarget.update(); context.executeQueryAsync(stage3, onQueryFail);};

There are a few noteworthy points in the code. First of all, when we've added the site column (fldJasonNotes) to the list, note that we need to grab a reference to the resulting list column (listFldJasonNotes) if we want to set list-specific properties.

Second, note how we approach adding the new column to the default list view:

Call get_defaultView to get the default list view from the list instance.

Call get_viewFields to get the field collection from the default list view.

Wednesday, 15 April 2015

There's long been some debate as to whether or not SharePoint requires you to install the SQL Server Full-Text Search feature on the database server. I've worked on the premise that it doesn't - SharePoint has its own search engine, etc.

However, the Access Services service application DOES require the Full-Text Search feature. If this feature is not installed, you'll get an error when you attempt to provision an Access Services service application that states "The Application Database Server does not have the Full-Text Search feature installed".

Access Services actually places a whole bunch of extra demands on the database server in addition to the Full-Text Search requirement. For example:

The database server must be running at least SQL Server 2012 (SharePoint Server 2013 will run happily on SQL Server 2008 R2 SP1)

The SQL Server instance must support mixed mode authentication.

The SQL Server instance must support Named Pipes in addition to TCP/IP.

For more information, take a look at the SharePoint 2013: Access Services wiki page on TechNet. Of course, there's nothing to stop you using a separate SQL Server installation to host your Access Services databases. This probably isn't a bad idea if you're planning to use Access Services heavily.

Wednesday, 18 February 2015

Recently I needed to get a grip on how to work with the eDiscovery tools in SharePoint 2013 from the server-side object model. There's not much information out there on how to do this (and some of the information out there is plain wrong), so I built a proof-of-concept console app to work through the key features, including:

Programmatically retrieving an eDiscovery case.

Creating a new source.

Creating a custodian.

Creating a new source.

Creating a new eDiscovery set.

Using queries and exports.

Applying in-place holds to eDiscovery sets.

I'll keep the explanation to a minimum, as I'm hoping the code largely speaks for itself. I'll assume you know the basic concepts of eDiscovery in SharePoint 2013, including how to work with cases, sources, sets, queries, and exports through the UI. To use the code, you'll need assembly references to Microsoft.SharePoint.dll and Microsoft.Office.Policy.dll. All the eDiscovery classes you need are in the Microsoft.Office.Server.Discovery namespace.

Our first task is to retrieve a case. An eDiscovery case is an individual site (SPWeb) within your eDiscovery Center site collection. Individual eDiscovery cases are represented by the Case class. The Case class provides the entry point for all eDiscovery operations in code. To retrieve a case, you need to call the Case constructor and pass the corresponding SPWeb instance as a parameter:

using
(SPSite site = new SPSite("http://sp2013/sites/ediscovery/"))

using
(SPWeb webCase = site.OpenWeb("case1"))

{

Case myCase = new Case(webCase);

Next, let's use the Case instance to create a new custodian. Essentially all this does is create a new list item in the Custodians list in the case web. Later, you'll see how we can assign custodians to sources to indicate who's responsible for the source:

//Create a custodian

Custodian newCustodian = myCase.CreateCustodian();

newCustodian.Name = "Legal Eagle";

newCustodian.LoginName = "JASON\\bob"; // i.e. domain username

newCustodian.Update();

Next let's create a new source. Essentially, SharePoint 2013 eDiscovery sources are either SharePoint webs or Exchange mailboxes. In both cases, the source is represented by the Source class. To create a new source from a SharePoint web, you call the Case.CreateLocation method. To create a new source from an Exchange mailbox, you call the Case.CreateMailbox method. I'm going to focus on creating a source from a SharePoint web. Once you've created the new Source instance, if you want it to work properly, you need to set three key properties:

Set the Source.WebId property to the ID of the SPWeb for which you want to define a source.

Set the Source.SiteId property to the ID of the SPSite containing the web for which you want to define a source.

Set the Source.FederationId property to the ID of the search result source (e.g. "Local SharePoint Results") from which you want to retrieve content.

Getting the ID of webs and sites is straightforward. Getting the ID of the search result source is a little more complex (credit). The code looks like this:

Now that we've created a source, we can use it in eDiscovery sets or queries. In the object model, eDiscovery sets are represented by the SourceGroup class. Let's create a new one and add our source to it:

// Create a new eDiscovery Set

SourceGroup newSet = myCase.CreateSourceGroup();

newSet.Name = "Team Site Leaflets";

newSet.AddSource(newSource);

newSet.DateStartFilter = new DateTime(2015, 1, 1);

newSet.Query = "Leaflet";

newSet.Update();

If you want to apply an in-place hold to the set, you simply set the SourceGroup.Preserve property to true and then call the Update method:

// Put the set on hold

newSet.Preserve = true;

newSet.Update();

Note that this requests an in-place hold. The hold won't actually take effect until the eDiscovery In-Place Hold Processing timer job runs.

Next, let's look at how to create a query. In the object model, queries are represented by the SavedSearch class:

// Create a new query based on the discovery set we created earlier

SavedSearch
newQuery = myCase.CreateSavedSearch();

newQuery.Name
= "Coded Query";

newQuery.SourceManagement
= SourceManagementType.SourceGroup;

newQuery.SourceGroupIds.Add(1)
// SPListItem ID of the discovery set

newQuery.Deduplication = true;

newQuery.SPFilters = "Created<=2/15/2015";

newQuery.Update();

When you create a query through the UI, you can choose whether to include eDiscovery sets (each consisting of one or more sources with filter criteria) or sources (no filter criteria). In the object model, you specify query scope by setting the SavedSearch.SourceManagement property to one of the following SourceManagementType enumeration values:

SourceManagementType.AllContent. This corresponds to the All case content option in the UI. The query contains all the sources defined in the case, with any eDiscovery set filters applied.

SourceManagementType.SourceGroup. This corresponds to the Select eDiscovery sets option in the UI. The query contains the sources defined in the discovery sets you select, with any discovery set filters applied.

SourceManagementType.Source. This corresponds to the Select sources option in the UI. The query contains the sources you select, and no discovery set filters are applied.

Once you've configured this property, you can either add sources or discovery sets to the query by adding integer IDs to the SourceIds or SourceGroupIds collections respectively. In this example, I've specified the SourceManagementType.SourceGroup option and then added the integer ID of my discovery set to the SourceGroupIds collection.

The SavedSearch class also allows you to specify string filters and refiners for the query. You can use the SPFilters and SPRefinements properties to specify filters and refiners for SharePoint sources, and you can use the EXFilters and EXRefinements properties to specify filters and refiners for Exchange mailboxes. A word of warning, however - the syntax required for the refinement properties is not user friendly. For example, if you create a query with refiners that match a file extension of "txt" and an author of "Administrator", the SPRefinements property ends up looking like this:

So it's probably fair to say that our ability to create query refiners in code is limited.

Finally, let's take a look at how to create an export in code. When you create an export, you're essentially just adding an item to the Exports list in the case web. However, the Export class does expose a ResultLink property that enables you to get the exported and packaged discovery content.

Sunday, 4 January 2015

In this post I'll show you how to build a custom workflow activity in Visual Studio that can update managed metadata field values in a SharePoint 2013 list or library. This is the final part of a three-part series on working with managed metadata fields in workflows:

Custom Workflow Activity for Setting Managed Metadata Field Values (this post). In this post, I'll walk you through how to build a workflow activity that sets managed metadata field values on a list item.

I've said it before, but it's worth repeating - you can use these custom workflow activities in any SharePoint Designer list workflows, including on Office 365 sites - custom workflow activities in SharePoint 2013 are entirely declarative, so deploying to SharePoint Online is not a problem.

This series of posts tackles the scenario where you want to extract a managed metadata field value from an item in one list (previous post), and then apply that value to a managed metadata field in another list (this post). The main constraint is that the managed metadata fields in the source list and the destination list must both use the same term set.

Arguments and Variables

If you've been following the series of posts you'll be familiar with the scenario and the concepts, so let's assume we've created a brand new custom workflow activity named Set MMS Field Value in Visual Studio and jump straight into defining arguments and variables. First the arguments:

I want to be able to use this activity update a managed metadata field on any SharePoint list or library, so the first piece of information we need is an identifier for the target list or library (selectedList). Next, we need to know which list item to update. List items are commonly identified using either a GUID identifier (listItemGuid) or an integer identifier (listItemIdIn) - I've defined arguments for both so the workflow designer can use either approach to identify the target list item. Next, we need to know the name of the managed metadata field in the target list item (mmsFieldName). Finally, we need the two property values that uniquely identify our managed metadata term (termGuid and termLabelInteger).

Now the variables:

We'll use listItemId to store the integer identifier for the target list item. The emptyGuid variable is just an empty GUID that we'll use for comparison purposes, and the remaining variables (metadataDV, propertiesDV and fieldValueDV) are DynamicValue properties that we'll use to progressively build the correct JSON structure to update a managed metadata field.

Activity Design

The workflow activity consists of seven child activities that correspond to three main tasks:

Get the integer identifier of the target list item. (If the workflow designer has provided an integer identifier, use it directly. Alternatively, if the workflow designer has provided a GUID identifier, use the GUID to look up the integer identifier.)

Build up the JSON payload we must provide in order to update the specified managed metadata field.

Update the specified managed metadata field on the specified list item.

Let's take a closer look at these three high-level tasks.

Task 1 - Get an integer identifier for the target list item

Our first task is to get an integer identifier for the target list item. Remember that we're giving the workflow designer two options: he or she can provide either a GUID identifier or an integer identifier to specify the target list item. To cater for both scenarios, we use an If activity. If the list item GUID is equal to an empty GUID, we can assume the workflow designer has used an integer identifier to specify the target list item. In this case, we use an Assign activity to set the the listItemId variable to the value of the listItemIdIn argument. If not, we use a LookupSPListItemId activity to look up the integer identifier using the specified GUID and then set the listItemId variable accordingly.

Task 2 - Build a JSON payload for the target managed metadata field

Our next task is to build a JSON payload for the target managed metadata field. The payload must take the following format, where <Field name> is the name of the target managed metadata field, <Term label integer> is the term label integer of the managed metadata term, and <Term GUID> is the term GUID of the managed metadata term:

To take advantage of the built-in child activities in Visual Studio, we need to create our JSON payload using DynamicValue structures. Because of the nested nature of this payload, we need to build the structure progressively from the inside out. First we use a BuildDynamicValue activity to build the contents of the innermost braces (the value of the __metadata property):

Next, we use a BuildDynamicValue activity to build the value of the middle set of braces (the value of the <Field name> property):

Notice how we set the __metadata key to the metadataDV value we created in the previous step, thereby creating a nested DynamicValue instance.

Finally, we use a CreateDynamicValue activity to build the value of the outer set of braces:

Note: We use a CreateDynamicValue activity rather than a BuildDynamicValue activity in this task because it allows us to set the dictionary key (PropertyName) to a variable value (mmsFieldName in this case). The BuildDynamicValue activity only allows you to type static string text for the dictionary key (Path). That wouldn't work in this scenario as we don't know the name of the target managed metadata field at compile time.

Task 3 - Update the target list item

Now that we've identified our target list item and build our JSON payload, all that remains is to perform the update operation on the list item. We can use the built-in UpdateListItem activity to do this:

The Actions File

The next stage is to build the actions (.actions4) file for the workflow activity, to specify how our activity should behave when we add it in SharePoint Designer. My actions file looks like this:

I won't go into more detail on the structure of the actions file right now, as there's nothing out of the ordinary in it and I don't want to get too repetitive. When you deploy the activity and use it in SharePoint Designer, it looks like this:

In this case, I'm using my Get MMS Field Value activity to get the term GUID and the term label integer from a managed metadata field named Customer in the current list item. I'm then using the Set MMS Field Value activity to set the value of the Client field on a list item in the Clients list to the same term. Because the source Customer field and the destination Client field both use the same term set, the workflow is able to copy the value across as desired.

In this post I'll show you how to build a custom workflow activity in Visual Studio that gets managed metadata field values from a SharePoint 2013 list or library. You can use the workflow activity in any SharePoint Designer list workflows, including on Office 365 sites - custom workflow activities in SharePoint 2013 are entirely declarative, so deploying to SharePoint Online is not a problem.

This is the second of a three-part series on working with managed metadata fields in workflows:

Custom Workflow Activity for Getting Managed Metadata Field Values (this post). In this post, I'll walk you through how to build a custom workflow activity in Visual Studio that gets managed metadata field values from a list item.

I'll assume you know the basics of how to build and deploy custom workflow activities - if you need a bit more detail on the deployment side of things, have a read through my first post on custom workflow activities. For now, let's just say I've created a new custom workflow activity named Get MMS Field Value in Visual Studio.

Arguments and Variables

I'll start with a quick run through of the arguments and variables I've used in the activity. First the arguments:

The activity will get the value of a managed metadata field from the current list item, so we only need the caller to provide one argument value - the name of the field (mmsFieldName). We want to return two values to the caller: the term GUID (termGuidOut) and the term label integer (labelOut).

Now the variables:

We'll use listItemFields to store a JSON representation of the list item. The DynamicValue type is perfect for storing and manipulating JSON data. We'll use fieldPathTermGuid and fieldPathLabel to build the XPath expressions we need in order to isolate and extract the term GUID and the term label from the list item JSON.

Activity Design

The workflow activity consists of seven child activities:

Get the current list item.

Build an XPath expression to find the TermGuid property of the specified managed metadata field value.

Build an XPath expression to find the Label property of the specified managed metadata field value.

Add a try-catch block so we can catch any errors when we parse the list item.

Within the try block, use the XPath expression we created earlier to get the TermGuid property from the managed metadata field value.

Within the try block, use the XPath expression we created earlier to get the Label property from the managed metadata field value.

Within the catch block, catch invalid operation exceptions and log the details to the workflow history list. (Invalid operation exceptions occur if the specified field does not exist or is not a managed metadata field.)

Let's walk through each of these in turn.

Step 1 - Get the current list item

The first task is to retrieve the list item from which you want to extract the managed metadata field:

I've used a LookupSPListItem activity to do this. The activity returns the JSON representation of the list item as a DynamicValue instance, which I've assigned to the listItemFields variable.

Step 2 - Build an XPath expression to find the TermGuid property

Now we've got the JSON representation of the list item, we need to figure out how to extract the properties we need. The only effective way to do this is to use the Visual Studio debugger (or a web debugger such as Fiddler) to take a look at the raw JSON data. In my development environment, it looks something like this:

I've highlighted the bits we're interested in. In this case, we can figure out that the XPath expression to get to the TermGuid property is as follows:

d/results(0)/Customer/TermGuid

Note that the results property actually contains an array of one result, so we use results(0) to get the first object in the array.

We can use an Assign activity to create our XPath expression and assign it to the fieldPathTermGuid variable. If we replace Customer (the MMS field name in this example) with a placeholder for our mmsFieldName variable, the activity looks like this:

Remember that we're not using the XPath expression at this stage - we're just building an XPath expression from the workflow variables to use in a later task.

Step 3 - Build an XPath expression to find the Label property

We can use the same approach to build an XPath expression that retrieves the Label property - only the last node of the XPath expression is different. In this case we assign the XPath expression to our fieldPathLabel variable:

Step 4 - Add a try-catch block
When we come to actually parse the list item, there's quite a lot that could go wrong. If the list item doesn't contain the specified field name, or the specified field is not a managed metadata field, the XPath expressions will fail and the workflow will throw an InvalidOperationException. As such, we want to build the parsing logic within a TryCatch activity:

Here you can see that we attempt to get the salient managed metadata field properties within a Try block, and we look for an InvalidOperationException in the Catch block. We'll look more closely at the activities within the Try and Catch blocks in the next step. For now, notice that the activities within the Try and Catch blocks are wrapped in Sequence activities. In a production scenario you will probably want to implement a more comprehensive error handling strategy, but for this proof-of-concept scenario I'm mainly interested in catching the common invalid operation exceptions.

Steps 5 and 6 - Get the TermGuid and Label property values

Within our Try block, we can use generic GetDynamicValueProperty<T> activities to get the TermGuid and Label property values:

In each case:

The Source property is the JSON representation of our list item.

The PropertyName property is the XPath expression that finds our TermGuid or Label property.

The Result property is the output argument that exposes each property to SharePoint Designer workflows.

Step 7 - Catch invalid operation exceptions

Within our InvalidOperationException catch block, all I'm doing at this stage is writing a (hopefully) helpful message to the workflow history list:

The Actions File

The next stage is to build the actions (.actions4) file for the workflow activity. The actions file defines the sentence that appears in SharePoint Designer when you add the custom activity to a workflow, together with the arguments (inputs and outputs) for the custom activity. I'll assume you know the basics of actions files - if not, check out my first post on custom workflow activities. In my case, the actions file looks like this:

As you can see from the Sentence attribute in the RuleDesigner element, we require the workflow designer to specify the name of the managed metadata field, and we return the term GUID and the term label integer value. Note that in the parameter definition for the mmsFieldName argument, we specify a DesignerType of FieldNames. This enables the workflow designer to select the name of the managed metadata field from a list of all the fields in the current list item.

In the SharePoint Designer workflow designer, the custom activity looks like this:

In this case, I've created a really simple SharePoint Designer workflow to test my custom activity. I use the custom activity to get the term GUID and term label integer properties from a managed metadata field named Customer, and I write the values to the workflow history list.

In the next post, I'll look at the other half of the problem - using these managed metadata field properties to update a managed metadata field in another list.