Quickly determine unique values in a SharePoint field

Report on unique values in a SharePoint field

Ever need to know which values are in use for a multi-select field? Here’s a quick and easy way to do it:

$web= Get-SPWeb "http ://SharePoint/SPWeb/"
$FieldName="Activity" # use your own field name
$list = $web.Lists["ListName"]
$activities = @()
foreach($doc in $list.Items) { $activities += $doc[$FieldName] }
$activities | Select-Object -Unique | Sort-Object

Accessing an SPItem via full URL

Accessing an SPItem via full encoded URL

One of the most common tasks is to access a document; heck isn’t that half of what SharePoint is about? Well in PowerShell it’s actually not obvious how to do it. I’ve used embarrassingly iterative hacks in the past, but my friend Brett Parker figured it out. Here’s the solution:

$docurl = "http ://SharePoint.com/sites/mySite/myWeb/myList/SpecificFileName.xlsx"
$site = New-Object Microsoft.SharePoint.SPSite($docurl)
$web = $site.OpenWeb()
$item = $web.GetListItem($docurl)
$list = $item.ParentList

$item now has the SPListItem, and you’re good to go.

Note that the SPDocumentLibrary is derived from the parentList property of the item.

Interestingly, the $site.openWeb() doesn’t open the RootWeb of the SPSite, but the actual web of the URL, which is great.

 

Customized scheduled SharePoint Search Alerts in HTML

Customized scheduled SharePoint Search Alerts in HTML

Occasionally I get requests for customized notification about documents within a SharePoint farm. Regular Alerts and SharePoint Search Alerts work great out of the box, but sometimes users want something more such as:
– Complex criteria
– Custom sort sequence
– Custom metadata
– Customized notification frequency
– Custom message text, or subject line
– Refined layout

The solution I’ve used is to script a search query in powerShell, and load it into a formatted HTML table, and schedule it to the desired frequency. I’ll outline the framework below that is easily adapted and extended. Note I wrote this for FAST, but using SharePoint Search Query classes you can achieve similar results in regular SharePoint search.

First, let’s establish some basics about search and notification. For this solution, I only want to return documents that are up to two or three days old, so I grab the date, take two off it, and put it into a format we can use later for the query:

 

 $Mydate=get-date
$MyDate = $Mydate.AddDays(-2)
$MyDateStr = $Mydate.Year.ToString("0000") + "-" + $Mydate.Month.ToString("00")  + "-" + $Mydate.day.ToString("00")  #formatted YYYY-MM-DD

Let’s now set up for the search querty. I chose to use FQL (FAST Query Language), but you can use Keyword Search. Note SQL Search is deprecated. I chose 50 results, but you can choose whatever amount you prefer:

 

  $site = New-Object Microsoft.SharePoint.SPSite $webappurl
$vc =New-Object Microsoft.Office.Server.Search.Query.KeywordQuery $site
$vc.ResultsProvider = [Microsoft.Office.Server.Search.Query.SearchProvider]::FASTSearch
$vc.ResultTypes = [Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults
#In my case I enabled the FQL syntax and set some other parameters:
$vc.EnableFQL = $true # enable FQL
$vc.RowLimit = 50 # sets the limit of results
$vc.StartRow = 0 # 0 is the default

Now let’s make sure the query returns the fields you want. These must be Managed Properties configured followed by a Full Crawl.

  $vc.SelectProperties.Add("Company Name")
$vc.SelectProperties.Add("URL")
$vc.SelectProperties.Add("Title")
$vc.SelectProperties.Add("Filename")
$vc.SelectProperties.Add("Company ClaimNumber")
$vc.SelectProperties.Add("Company PolicyNumber")
$vc.SelectProperties.Add("Company Modified")
$vc.SelectProperties.Add("Company EffectiveYear")

Now let’s piece together the XML of the FQL. Note two strings ($q1 and $q2) are used to construct the query and put into $BigQ, with the date we formatted earlier. We’re looking for documents newer than two days ago, where a particular field CompanyClaimDocumentType equals a specific value (“Specific Value”). Then we execute the FQL:

$q1='and(filter(Company modified:range(datetime("'
      $q2='"), max, from="GT")), filter(CompanyClaimDocumentType:equals("Specific Value")))'
      $BigQ=$q1+$MyDateStr+$q2
 
$vc.QueryText = $BigQ
$results = $vc.Execute()

Now let’s convert the search results into a DataTable to make it easy to shape into an HTML table for the outbound email alert notification, we’ll define the columns, and load the values. One nice thing is to shape the link column to allow a hyperlink embedded in the table for easy user access to the documents. I also structure a special link using the DMF:// protocol supported by MacroView DMF:

$resultsTable = $results.Item([Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults)
$resultsDataTable = $resultsTable.Table
$rows = $resultsDataTable.Rows
 
 
$table = New-Object system.Data.DataTable “SearchReport”
 
$col1 = New-Object system.Data.DataColumn Title,([string])
$col2 = New-Object system.Data.DataColumn CompanyName,([string])
$col3 = New-Object system.Data.DataColumn ClaimNumber,([string])
$col4 = New-Object system.Data.DataColumn Link,([string])
$col5 = New-Object system.Data.DataColumn PolicyNumber,([string])
$col6 = New-Object system.Data.DataColumn Modified,([string])
$col7 = New-Object system.Data.DataColumn EffectiveYear,([string])
$col8 = New-Object system.Data.DataColumn FileName,([string])
$col9 = New-Object system.Data.DataColumn DMF,([string])
 
$table.columns.add($col1)
$table.columns.add($col2)
$table.columns.add($col3)
$table.columns.add($col4)
$table.columns.add($col5)
$table.columns.add($col6)
$table.columns.add($col7)
$table.columns.add($col8)
$table.columns.add($col9)
 
if ($rows.count -gt 0)
    {
    for ($i=0; $i -lt $rows.Count; $i++)
    {
    $row = $table.NewRow()
    $row.Link = "<a>'+ " Link " + "</a>";
    #$row.DMF = ($row.Link.Replace("http://","DMF://")).replace(" File Link", "SP Explorer")  Took out, doesn't appear to work quite right
    $row.Title = $rows[$i].Title;
    $row.InsuredName= $rows[$i].CompanyName;
    $row.ClaimNumber= $rows[$i].CompanyClaimNumber;
    $row.PolicyNumber= $rows[$i].CompanyPolicyNumber;
    $row.EffectiveYear= $rows[$i].CompanyEffectiveYear;
    $row.FileName= $rows[$i].FileName;
    $row.Modified= $rows[$i].Company Modified.substring(0,10);
 
    $table.Rows.Add($row)
    #("<a>" + $_.Portname.SubString(3) + "</a>"}}
    }

Now, we want to shape this into an outbound table in the email. To do that we’ll use the handy CovertTo-HTML CmdLet. Just pass in the column names you want to appear. Above I map more than we use below. I shape the borders and colors for a really professional look. However we have a sticky problem where we don’t want it to mess with the embedded HTML such as brackets around the a href for links. To solve that, I pipe the stream into a routine called Convert-HTMLEscape that I will outline shortly. Note the nice H1 header, and we’ll nicely handle single/plural number of rows in the subject line and top of the email:

$a = ""
$a = $a + "TABLE{border-width: 1px;border-style: solid;border-color:black;}"
$a = $a + "Table{background-color:#EFFFFF;border-collapse: collapse;}"
$a = $a + "TH{border-width:1px;padding:5px;border-style:solid;border-color:black;background-color:#DDDDDD}"
$a = $a + "TD{border-width:1px;padding-left:5px;padding-right:3px;border-style:solid;border-color:black;}"
$a = $a + ""
 
#$MyOutput = $rows | ConvertTo-Html Title, Author, URL, Link -body "<H1>Recent Custom Reports</H1>" -PostContent "Goodbye and thanks for all the fish"
 
if ($rows.count -eq 1)
{
    $Plural=$null;
}
else
{
    $Plural="s";
}
 
$MyOutput = $table| ConvertTo-Html Title, Link, InsuredName, ClaimNumber, PolicyNumber, EffectiveYear, Modified -head $a -body "<H1>$($rows.count) Recent Property Adjuster Report$($Plural)</H1>"  | Convert-HTMLEscape

Here’s the Convert-HTMLEscape function. It’s a little dense, but works within the pipeline and does what we need, by converting the XML equivalent of the two common XML characters right back to the correct characters, basically undoing the bit of a mess made by ConvertTo-HTML:

Function Convert-HTMLEscape {
 
# convert < and > to <> It is assumed that these will be in pairs
 
[cmdletbinding()]
 
Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)
 
Process {
foreach ($item in $text) {
    if ($item -match "<") {
        <#
          replace codes with actual symbols. This line is a shortcut to do two replacements with one line of code. The code in the first
          set of parentheses revised text with ".
        #>
        (($item.Replace("<","")).Replace(""",'"')
     }
     else {
        #otherwise just write the line to the pipeline
        $item
     }
 }
} #close process
 
} #close function

Now the easy part, let’s generate the email. Note the format sets isHTML to $true, and uses SMTP:

 

   #email setup, can move to top
#param( 
        [string] $From = "SharePoint Support@MyDomain.com"
        [string] $To = $null; #$Recipients #"joelplaut@MyDomain.com"
        [string] $Title = "Daily Report of updated custom Reports"
        #[string] $Body = "body"
    #)
    $Body = $rows | ConvertTo-Html
    $SmtpClient = New-Object System.Net.Mail.SmtpClient
    $SmtpServer = "mail.Company limited.com"
    $SmtpClient.host = $SmtpServer
 
#    $SmtpClient.Send($From,$To,$Title,$Body)
$MailMessage = New-Object system.net.mail.mailmessage
 
$mailmessage.from = $From;
foreach ($recip in $ToRecipientsArray)
{
    $mailmessage.To.add($recip)
}
foreach ($recip in $CCRecipientsArray)
{
    $mailmessage.CC.add($recip)
}
$mailmessage.Subject = $Title
$mailmessage.Body = $myoutput #"Body"
$MailMessage.set_IsBodyHtml($true)
$smtpclient.Send($mailmessage)

Now let’s put it all together. Note I always set an $env for the environment, to make it easy to test in Dev, before deploying in Production.

# SharePoint Search Alerts
Clear-Host
Write-Host "Start LC Alerts" -ForegroundColor darkblue
Add-PsSnapin Microsoft.SharePoint.PowerShell -erroraction silentlycontinue
 
$ToRecipients = "MyADGroup@MyDomain.com"
$CCRecipients = "joelplaut@MyDomain.com,Bozo@MyDomain.com"
$ToRecipientsArray = $ToRecipients.Split(",");
$CCRecipientsArray = $CCRecipients.Split(",");
 
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Administration")
 
$env="Prod"
if ($env -eq "Dev")
{
    $webappurl = "http ://devFarm/" # Replace with URL of web application that you wish to warm up
    $filterpath = "http ://devFarm/insureds"
}
else
{
    $webappurl = "http ://SharePoint/" # Replace with URL of web application that you wish to warm up
    $filterpath = "http ://SharePoint/insureds" #path for exercising previews
}
 
Function Convert-HTMLEscape {
 
<#
convert < and > to 
It is assumed that these will be in pairs
#>
 
[cmdletbinding()]
 
Param (
[Parameter(Position=0,ValueFromPipeline=$True)]
[string[]]$Text
)
 
Process {
foreach ($item in $text) {
    if ($item -match "<") {
        <#
          replace codes with actual symbols
          This line is a shortcut to do two replacements
          with one line of code. The code in the first
          set of parentheses revised text with ".
        #>
        (($item.Replace("<","")).Replace(""",'"')
     }
     else {
        #otherwise just write the line to the pipeline
        $item
     }
 }
} #close process
 
} #close function
 
 
 
$Mydate=get-date
$MyDate = $Mydate.AddDays(-2)
$MyDateStr = $Mydate.Year.ToString("0000") + "-" + $Mydate.Month.ToString("00")  + "-" + $Mydate.day.ToString("00")  #formatted YYYY-MM-DD
 
$site = New-Object Microsoft.SharePoint.SPSite $webappurl
$vc =New-Object Microsoft.Office.Server.Search.Query.KeywordQuery $site
$vc.ResultsProvider = [Microsoft.Office.Server.Search.Query.SearchProvider]::FASTSearch
$vc.ResultTypes = [Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults
#In my case I enabled the FQL syntax and set some other parameters:
$vc.EnableFQL = $true # enable FQL
$vc.RowLimit = 50 # sets the limit of results
$vc.StartRow = 0 # 0 is the default
 
$vc.SelectProperties.Add("Company Name")
$vc.SelectProperties.Add("URL")
$vc.SelectProperties.Add("Title")
$vc.SelectProperties.Add("Filename")
$vc.SelectProperties.Add("Company ClaimNumber")
$vc.SelectProperties.Add("Company PolicyNumber")
$vc.SelectProperties.Add("Company Modified")
$vc.SelectProperties.Add("Company EffectiveYear")
 
 
#Query / Result
  
      $q1='and(filter(Company modified:range(datetime("'
      $q2='"), max, from="GT")), filter(Company claimdocumenttype:equals("Property Adjuster Reports")))'
      $BigQ=$q1+$MyDateStr+$q2
 
 
 
$vc.QueryText = $BigQ
$results = $vc.Execute()
#$results
 
$resultsTable = $results.Item([Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults)
$resultsDataTable = $resultsTable.Table
$rows = $resultsDataTable.Rows
 
 
$table = New-Object system.Data.DataTable “SearchReport”
 
$col1 = New-Object system.Data.DataColumn Title,([string])
$col2 = New-Object system.Data.DataColumn InsuredName,([string])
$col3 = New-Object system.Data.DataColumn ClaimNumber,([string])
$col4 = New-Object system.Data.DataColumn Link,([string])
$col5 = New-Object system.Data.DataColumn PolicyNumber,([string])
$col6 = New-Object system.Data.DataColumn Modified,([string])
$col7 = New-Object system.Data.DataColumn EffectiveYear,([string])
$col8 = New-Object system.Data.DataColumn FileName,([string])
$col9 = New-Object system.Data.DataColumn DMF,([string])
 
$table.columns.add($col1)
$table.columns.add($col2)
$table.columns.add($col3)
$table.columns.add($col4)
$table.columns.add($col5)
$table.columns.add($col6)
$table.columns.add($col7)
$table.columns.add($col8)
$table.columns.add($col9)
 
if ($rows.count -gt 0)
    {
    for ($i=0; $i -lt $rows.Count; $i++)
    {
    $row = $table.NewRow()
    $row.Link = "<a>'+ " Link " + "</a>";
    #$row.DMF = ($row.Link.Replace("http://","DMF://")).replace(" File Link", "SP Explorer")  Took out, doesn't appear to work quite right
    $row.Title = $rows[$i].Title;
    $row.InsuredName= $rows[$i].CompanyName;
    $row.ClaimNumber= $rows[$i].CompanyClaimNumber;
    $row.PolicyNumber= $rows[$i].CompanyPolicyNumber;
    $row.EffectiveYear= $rows[$i].CompanyEffectiveYear;
    $row.FileName= $rows[$i].FileName;
    $row.Modified= $rows[$i].Company Modified.substring(0,10);
 
    $table.Rows.Add($row)
    #("<a>" + $_.Portname.SubString(3) + "</a>"}}
    }
 
    $a = ""
    $a = $a + "TABLE{border-width: 1px;border-style: solid;border-color:black;}"
    $a = $a + "Table{background-color:#EFFFFF;border-collapse: collapse;}"
    $a = $a + "TH{border-width:1px;padding:5px;border-style:solid;border-color:black;background-color:#DDDDDD}"
    $a = $a + "TD{border-width:1px;padding-left:5px;padding-right:3px;border-style:solid;border-color:black;}"
    $a = $a + ""
 
    #Filename removed at My's suggestion
 
    #$MyOutput = $rows | ConvertTo-Html Title, Author, URL, Link -body "<H1>Recent Custom Reports</H1>" -PostContent "Goodbye and thanks for all the fish"
     
    if ($rows.count -eq 1)
    {
        $Plural=$null;
    }
    else
    {
        $Plural="s";
    }
 
    $MyOutput = $table| ConvertTo-Html Title, Link, InsuredName, ClaimNumber, PolicyNumber, EffectiveYear, Modified -head $a -body "<H1>$($rows.count) Recent Custom Report$($Plural)</H1>"  | Convert-HTMLEscape
    #$MyOutput > C:A.html #debug technique
    #email setup, can move to top
    #param( 
            [string] $From = "SharePoint Support@Company limited.com"
            [string] $To = $null; #$Recipients #"joelplaut@Company limited.com"
            [string] $Title = "Daily Report of updated Property Adjuster Reports"
            #[string] $Body = "body"
        #)
        $Body = $rows | ConvertTo-Html
        $SmtpClient = New-Object System.Net.Mail.SmtpClient
        $SmtpServer = "mail.Company limited.com"
        $SmtpClient.host = $SmtpServer
 
    #    $SmtpClient.Send($From,$To,$Title,$Body)
    $MailMessage = New-Object system.net.mail.mailmessage
 
    $mailmessage.from = $From;
    foreach ($recip in $ToRecipientsArray)
    {
        $mailmessage.To.add($recip)
    }
    foreach ($recip in $CCRecipientsArray)
    {
        $mailmessage.CC.add($recip)
    }
    $mailmessage.Subject = $Title
    $mailmessage.Body = $myoutput #"Body"
    $MailMessage.set_IsBodyHtml($true)
    $smtpclient.Send($mailmessage)
 
} #don't bother, no new hits

SharePoint Monitoring Metrics

SharePoint Monitoring Metrics

I pulled together some useful metrics for monitoring SharePoint environments:

Storage

  1. Server drive(s) disk space availability (15% free)
  2. Content DB size, and monthly growth rate
  3. Site Collection size and monthly growth rate (same as above, for where we have one site collection per content DB)
  4. Storage by file type (are people uploading home pictures or MP3s)
  5. MySites site size

Security

  1. Unique permission scopes per library

Usage

  1. Items per library, and monthly growth rate
  2. Most active site / least active site
  3. Libraries/Sites with no activity in 90 days
  4. Documents with over 200 Versions (considering version trimming of middle 75% of versions, if > 100 versions of a document)
  5. Duplicate Documents
  6. Users who do not create documents in 30 days (perhaps they are storing locally or on other media)
  7. Folders with over 2000 items in the root
  8. Any newly created library or site

Search

  1. Search terms resulting in no-hits
  2. Very frequent searches

System

  • Reboots
  • IIS Pool recycle frequency
  • Daily report of Unexpected events in ULS log (could be large, and not actionable)
  • RAM usage > 95%
  • Full Crawl exceeding 20 hours
  • Incremental crawl not running hourly
  • Failed Timer jobs
  • CPU Usage > 90% average over any five minute period
  • Disk queue length on SharePoint server
  • Web Page load time (can have a program pull a page every minute)

How to delete a hidden library field in SharePoint

Deleting a hidden library field in SharePoint

Hidden fields do not appear in the user interface when viewing Library settings. One reason to delete a hidden field is after a field is removed from a Content Type in a syndication hub, the field remains in the libraries associated with the original Content Type, although it is now “orphaned” and unrelated to the Content Type. The solution is to remove the field itself from each and every library. The script commands below can easily action all libraries based on an iteration through all Web Apps, Sites, Webs and Libs.

A hidden field can be deleted via PowerShell, but one should note a few obstacles to work around. First, the field needs to be unhidden, albeit temporarily. Then the field object needs to be updated. Only then can the field be deleted. Here’s how:

First, let’s get the Web, the list within it, and the target field:

 

 $JPWeb = Get-SPWeb "http ://SharePoint/div/Path/Site"
$lists = $JPWeb.lists
$list = $lists["MyList"]
$fields=$list.fields

Let’s have a peek at the friendly name and internal name of each field in the list, by piping to a select:

   $fields | select title, staticname

Let’s grab the target field, and start by unhiding it, updating the field, then deleting it:

   $field = $fields["FieldToDelete"]
#$field.delete() #can't delete a hidden column
$field.set_Hidden($false)
$field.Update()
$field.delete()

The above code has a bit of a risk. External field names can change. Oddly a number of internal field names have the same external name. These are known as “Title” values in the object model. The best approach when deleting a field is to use the Static name. Here’s how:

 $field = $list.Fields.GetFieldByInternalName("InternalFieldName")

Best practice is to use a try/catch, as the above function will throw an error if the field is not found.

That’s it. No further updates are necessary to the List or Web objects. If you have content types referencing this field, the local copy of the content type within the List is customized via the removal of this field.

 

Setting the Minimum BLOB Storage Size Correctly

How to set the Minimum BLOB Storage Size

Setting the minimum storage size for BLOBs is a bit tricky.  It turns out that setting the property generally does not work, and the SPRemoteBlobStorageSettings object does not have an associated update() method.  Trying to update the container object (Content DB) doesn’t work either.  The trick is to use the associated method to set the value:

$rbs.set_MinimumBlobStorageSize(number)

Here’s a script that iterates through a set of content DBs, and sets the min blob size.  I prefer to work with lists of DBs:

 

 $DBList="Content_1,Content_12,Content_3"
$DBs=$DBList.split(",")
for ($i=0; $i -lt $DBs.count; $i++)
{
# Provide your content DB name using the name of the Content DB, or if there's only one Content DB for a web app, you can get it by identifying the web app
$cdb=Get-SPContentDatabase -identity $DBs[$i]
 
$rbs = $cdb.RemoteBlobStorageSettings
#here's what not to do. This does not work; min blob size is lost; save conflict
#$rbs.MinimumBlobStorageSize =25kb # this is the minimum size of a file to be sent to the Blob Store; anything smaller is kept in the Content DB.

#a much better approach is to use the method to set the value, this does not require an update() method. Note the SPRemoteBlobStorageSettings ($rbs here is that type) does not have an associated update() method
$rbs.set_MinimumBlobStorageSize(25*1024)
}

Here’s a script to ensure that RBS is configured correctly, and the min blob size is what you expect it to be:

 $DBList="Content_1,Content_12,Content_3"
$DBs=$DBList.split(",")

for ($i=0; $i -lt $DBs.count; $i++)
{
$cdb=Get-SPContentDatabase -identity $DBs[$i]
$rbs = $cdb.RemoteBlobStorageSettings

write-host “RBS installation for $($cdb.name) is $($rbs.Installed())”

write-host “MinBlobStorageSize for $($cdb.Name) is $($rbs.MinimumBlobStorageSize)”
}


	

Book Review: SharePoint 2010 Development with Visual Studio 2010

Cookbook is a good term for this book.  It is an excellent overview of SharePoint programming, with a task focus on the core programming tasks and deliverables a typical programmer is faced with.  This means it’s a handy shelf reference when faced with an assignment.

I particularly enjoyed the common approach of “How it works” and “There’s more” to continue exploration.  This is a well structured and approachable programming reference book I would recommend.

http://www.amazon.com/gp/product/1849684588/ref=cm_cr_mts_prod_img

Add a Taxonomy Field into Every Site Collection and to a Content Type

Add a Taxonomy Field into Every Site Collection

I recently had to add a Taxonomy Site Column into a range of Site Collections, and then add the new Site Column into a Content Type.  Lastly, I needed to update all libraries in the site collection to reflect the updated Content Type.  How might we do this?

Step by step

Let’s get a web application, and its sites.  Note that this is not the most efficient approach, as a collection of SPSites can be large.  A more efficient way could be to pass the Sites through a pipeline.

  $webApp=Get-Spwebapplication "http ://SharePoint" #your web app url
$sites=$webApp.sites;

Let’s loop through the SPSites, and get the SPWeb to work with.  The SPSite actually has no content, nor does it have Site Columns or Content Types.  The root web is where Site Columns and Content Types are managed.

  for ($i=0; $i -lt $sites.count; $i++)
{
$site= $sites[$i];
$JPweb=$site.rootweb;
}

To do anything with Managed Metadata requires opening a Taxonomy session.   This can be done directly through the Service Application.  For simplicity, I open the session at the Site level.  This code assumes only one Managed Metadata Service association (index offset of zero).  If you have more than one MMS service application, query the collection of termstores to ensure you are grabbing the correct one.  Note below there is a “group name” and “Term Set Name” required.  Adjust these to match your environment.  Lastly there’s a check below to ensure you grabbed a termset, and aren’t holding a null reference:

$taxSession = new-object Microsoft.SharePoint.Taxonomy.TaxonomySession($site, $true);
$termStore = $taxSession.TermStores[0];
$TermSet = $termStore.Groups["Your Term Group"].TermSets["TermSetName"]
 
if ($TermSet -eq $null)
{
Write-Host -ForegroundColor DarkRed "Termset does not exist"
continue;
}

Now let’s create a new field. Note we’ll need to know the display name for the field, as well as define a static (internal) name.  As with all Site Columns, I highly recommend using a static name that avoids spaces and other characters that lead to control escape sequences in the code (%20 for space, etc).  Note the termset that is “latched” to this field refers to the specific term store (it could point to any of your Managed Metadata Service Applications) and to the specific termset.  These are GUIDs.  Note that each Managed Metadata Service Application is associated with a dedicated database.  That’s where your termsets are stored.  You’ll want to select a “group” for where the site column will be displayed.  We’ll add the field, and update the SPWeb object.

 $taxonomyField = $JPweb.Fields.CreateNewField("TaxonomyFieldType", $FieldToAddDisplayName)
$taxonomyField.SspId = $termSet.TermStore.Id
$taxonomyField.TermSetId = $termSet.Id
$taxonomyField.AllowMultipleValues = $false
$taxonomyField.Group = "Site Column Group"
$taxonomyField.StaticName = $FieldToAddStaticName
$taxonomyField.ShowInEditForm = $true
$taxonomyField.ShowInNewForm = $true
$taxonomyField.Hidden = $false
$taxonomyField.Required = $false
 
$JPweb.Fields.Add($taxonomyField);
$JPweb.Update();

Now let’s grab the set of Content Types, find our target Content Type and field, and update the Content Type.  We’ll turn off ReadOnly for the Content Type, and do the following special object update to force propagation of the Content Type into all the libraries in the site collection: $ct.UpdateIncludingSealedAndReadOnly()

$cts=$JPWeb.ContentTypes
$ct=$cts["Specific Content Type"] # replace with your content type
 
$ct.set_ReadOnly($false)
$fields=$ct.fields
 
$fs=$JPWeb.Fields
$favField=$fs.get_Item($FieldToAddDisplayName)
 
if ($favField -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot find $($FieldToAdd) in web $($JPWeb.url)"
continue;
}
 
$link = new-object Microsoft.SharePoint.SPFieldLink $favField
$ct.FieldLinks.Add($link)
$ct.UpdateIncludingSealedAndReadOnly($true)
$ct.set_ReadOnly($true)

Let’s now put it all together into one neat script that includes some extra error handling:

 $FieldToAddStaticName = "InternalFieldName"
$FieldToAddDisplayName = "Field Display Name"
$webApp=Get-Spwebapplication "http ://SharePoint" #your web app url
$sites=$webApp.sites;
 
for ($i=0; $i -lt $sites.count; $i++)
{
$site= $sites[$i];
$JPweb=$site.rootweb;
 
if ($site.url -notlike "http ://SharePoint/MyPreferredPath/*")
{
continue;
}
 
$taxSession = new-object Microsoft.SharePoint.Taxonomy.TaxonomySession($site, $true);
$termStore = $taxSession.TermStores[0];
 $TermSet = $termStore.Groups["Your Term Group"].TermSets["TermSetName"]
 
if ($TermSet -eq $null)
{
Write-Host -ForegroundColor DarkRed "Termset does not exist"
continue;
}
 
$taxonomyField = $JPweb.Fields.CreateNewField("TaxonomyFieldType", $FieldToAddDisplayName)
 
$taxonomyField.SspId = $termSet.TermStore.Id
 $taxonomyField.TermSetId = $termSet.Id
 $taxonomyField.AllowMultipleValues = $false
 $taxonomyField.Group = "Site Column Group"
 $taxonomyField.StaticName = $FieldToAddStaticName
 $taxonomyField.ShowInEditForm = $true
 $taxonomyField.ShowInNewForm = $true
 $taxonomyField.Hidden = $false
 $taxonomyField.Required = $false
 
$JPweb.Fields.Add($taxonomyField);
 
$JPweb.Update();
$cts=$JPWeb.ContentTypes
$ct=$cts["Specific Content Type"] # replace with your content type
if ($ct -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot add field to Content Type in web $($JPWeb.url)"
continue;
}
 
$ct.set_ReadOnly($false)
$fields=$ct.fields
 
$fs=$JPWeb.Fields
$favField=$fs.get_Item($FieldToAddDisplayName)
 
if ($favField -eq $null)
{
Write-Host -ForegroundColor DarkRed "Cannot find $($FieldToAdd) in web $($JPWeb.url)"
continue;
}
 
$link = new-object Microsoft.SharePoint.SPFieldLink $favField
 
$ct.FieldLinks.Add($link)
 
$ct.UpdateIncludingSealedAndReadOnly($true)
 
$ct.set_ReadOnly($true)
}

Creating a View in each Document Library in a Web Application

Crate a View in each Library in a Web App

My friend Rob Holmes raised an interesting challenge; deploying a new view to all libraries. Here’s the script to apply it to all libraries with Content Types enabled that are Document Libraries, across all libraries in all webs in all site collections of a web application.

Sorting/filtering entries in a view requires the use of CAML, for which I provide a few examples commented in the script. To write CAML, one needs to know the available fields. If you have a $Lib, you can dump the static or internal names for every field in the library with this command:

1
$Lib.Fields | select internalname

Here’s some CAML examples.

'<OrderBy><FieldRef Name="mvReceived_x0020_Time" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvReceived_x0020_Time" /></IsNotNull></Where>'
'<OrderBy><FieldRef Name="mvSentOn" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvReceived_x0020_Time" /></IsNotNull></Where>'
'<OrderBy><FieldRef Name="mvSentOn" Ascending="FALSE" /></OrderBy><Where><IsNotNull><FieldRef Name="mvSentOn" /></IsNotNull></Where>'

There’s a few good CAML builder tools. You can also just navigate an existing View and grab the XML from the View in SharePoint.

Here’s the script:

  Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue
$wa=get-spwebapplication http ://SharePoint
 
Write-Host "script starting $(get-date)"
 
$wa=get-spwebapplication http ://SharePoint
 
foreach ($Site in $wa.sites)
{
    if ($site.url -like $MatchStr)
    {
        $webs=$Site.AllWebs
        $webcount = $Site.AllWebs.Count
 
        for ($i=0; $i -lt $webcount; $i++)
        {
            $web=$webs[$i]
 
            Write-Host "working in $($web.url)"
 
            $lists=$web.lists;
 
              for ($k=0; $k -lt $lists.count; $k++)
              {
                    $JPLib = $lists[$k];
                         #don't bother adding a view to a hidden library
                    if ($JPLib.Hidden)
                    {
                        continue;
                    }
                         #only for libraries, not for GenericList and other types
                    if ($JPLib.BaseType -ne "DocumentLibrary")
                    {
                        continue;
                    }
                         #choose your own lib filter; this acts on every content type enabled library
                    if ($JPLib.ContentTypesEnabled)
                    {
                    write-host -f green "The Library $($JPLib.title) exists in the site $($web.url), about to tune the view"
                    try
                    {
                    $x=$JPLib.Views.get_Item("Email")
                    if ($x.id -ne $null) #prevents duplicate entries
                    {
                        $JPLib.Views.Delete($x.ID.ToString())
                    }
                    }
                    catch
                    {}
                    if ($JPLib.Views["Email"] -eq $null) #prevents duplicate entries
                       {
 #$viewQuery = ''
                     #$viewQuery = ''
                     $viewQuery = ''
 
                     $viewFields = New-Object System.Collections.Specialized.StringCollection
                     $viewFields.Add("DocIcon") &gt; $null
                     $viewFields.Add("LinkFilename") &gt;  $null
                     $viewFields.Add("Title") &gt;  $null
                     $viewFields.Add("mvTo") &gt;  $null
                     $viewFields.Add("mvFrom") &gt;  $null
                     $viewFields.Add("mvSubject") &gt;  $null
                     $viewFields.Add("mvSentOn") &gt;  $null
                     $viewFields.Add("mvAttach_x0020_Count") &gt;  $null
                #    $viewFields.Add("DocType") &gt;  $null # this is accounting specific
 
                     #RowLimit property
                     $viewRowLimit = 50
                     #Paged property
                     $viewPaged = $true
                     #DefaultView property
                     $viewDefaultView = $false
                     #$ViewQuery=$null;
                     $viewTitle="Email"
                     $newview = $JPLib.Views.Add($viewTitle, $viewFields, $viewQuery, $viewRowLimit, $viewPaged, $viewDefaultView)
                       }
    }
    }
   }
#} #foreach site
} #if $true/Siteurl is not null, if environment setup is valid
}<del datetime="2024-03-06T19:12:48+00:00">  </del>

Configuring Blob Cache correctly

Blob Cache

For snappy SharePoint performance, one great option to enable is Blob Caching. Make sure to first back up the web-config for your target web app, which you can navigate to via IIS.

I prefer to increase the max-age=”600″ to a full day max-age=”86400″. This parameter forces remote browser caching which really helps with remote users over relatively poor connections.

Set an appropriate location; I prefer to segregate by web app name. Microsoft suggests a 10GB maxSize, with 20% extra space. You can lower it, it rarely increases to that level.

Best is configuring this on a dedicated drive. Unless you have an enterprise-class SAN, dedicated spindles are the way to go.

For every cache, you’ll want to know how to flush it:

$webApp = Get-SPWebApplication "http://SharePoint"
[Microsoft.SharePoint.Publishing.PublishingCache]::FlushBlobCache($webApp)
Write-Host "Flushed the BLOB cache for:" $webApp
It’s totally safe to flush the cache at any time without disruption, and best of all, wastes no water 🙂