Consuming Content Type Hub in custom web templates

Setting up content type hub in SharePoint 2010 is quite straightforward process. There are simple steps to follow and if followed in correct order you can get it working in no time. Content Type Hub relies on managed metadata service. Unless all web applications are sharing the same instance of managed metadata service application, they should be able to consume content types from the content type hub (a site collection marked as a hub and other web application are marked to consume from this location).

However, if you are doing the customization and you have some custom Web Templates that you use to create sites and subsites. And in those sites, you want to consume content types from content type hub, you might get a little bump in the road during the setup.

As I mentioned, Content Type Hub relies on Managed Matadata service, one feature TaxonomyFieldAdded (a hidden) feature is enabled on every site collection by default. This feature is not activated by default when you create web templates (please refer to this blog for further explanation of this: Web Templates and Content Type Publishing). So if you navigate to the Site Settings for any site that is created from custom web template, you will not see any link for Term store management or Content Type Publishing

sitesettings-featuredisabled

So for custom web templates, you are required to activate this feature.  Here is the PowerShell code for enabling this feature on a specific site collection.

$snapin = Get-PSSnapin | Where-Object {$_.Name -eq 'Microsoft.SharePoint.Powershell'}

if ($snapin -eq $null)
{
Write-Host "Loading SharePoint Powershell Snapin"
Add-PSSnapin "Microsoft.SharePoint.Powershell" -EA SilentlyContinue
}

#Enable <strong>TaxonomyFieldAdded </strong>feature on site collection
Enable-SPFeature –Identity 73EF14B1-13A9-416b-A9B5-ECECA2B0604C –url "http://portal.local.domain/sites/subsite1/"

Once feature is activated, you will see the links in Site Settings appear !
sitesettings-featureenabled

Now, you should be able to consume content types from content type hub as expected. You can activate this feature directly from your web template, add an entry for this feature in onet.xml file for your web template, in this way you don’t have to enable this feature manually.

</pre>
<SiteFeatures>
<!-- OOTB: Taxonomy -->
<Feature ID="73EF14B1-13A9-416b-A9B5-ECECA2B0604C" />
</SiteFeatures>
<pre>

Paged Custom Search Query using KeywordQuery

SharePoint Server Object Model provides two classes to perform custom search queries namely FullTextSqlQuery and KeywordQuery. Both of these classes inherit from a generic Query class (in  Microsoft.SharePoint.Search namespace).

You can perform search operations using both of these classes depending on your needs as FullTextSqlQuery provides more sophisticated queries using Sql syntax whereas KeywordQuery class provides simpler syntax to do the search.

However, the scenario I’m explaining in this post is relevant to both of these classes but i will use KeywordQuery class to demonstrate.

Scenario:

I want to write a custom people search web part that takes in the keywords entered by user and performs the search.

I’m using powershell to demonstrate here but this can be done in C# as well in similar fashion.

So, here is the function that performs the basic search


Function GetSearchResults([string]$siteCollectionUrl, [string]$keyword, [int]$sIndex, [int]$rLimit)
{
	#load the site and set the keyword query object
	$searchSite = Get-SPSite $siteCollectionUrl

	$keywordQuery = New-Object Microsoft.Office.Server.Search.Query.KeywordQuery $searchSite

	$keywordQuery.ResultTypes = [Microsoft.Office.Server.Search.Query.ResultType]::RelevantResults

  	#Write-Host "Start Index " $sIndex
	#Write-Host "Row Limit " $rLimit
	#Write-Host "Keyword " $keyword

	$keywordQuery.StartRow = $sIndex
	$keywordQuery.RowLimit = $rLimit
	$keywordQuery.HiddenConstraints = "scope:People"

	#Pass the query text - SharePoint
	$keywordQuery.QueryText = $keyword

	#query execution
	$results = $keywordQuery.Execute()
	return $results
}

KeywordQuery class has two properties (RowLimit and StartRow) that are of our interest here.
RowLimit specifies the number of items that you want the search to return when you execute the query
and StartRow gets or sets the first row of information from the search results. So basically with both of these properties you get the paged search result instead of getting entire data in one chunk !

Now, one of the obvious reasons to do this performance. But there is one more reason as well. RowLimit (default value is 50 if you don’t specify it yourself), even though can accept maximum 32-bit integer value theoretically, is set to some hardcoded limit of 10000. It means that if you set RowLimit to anything greater that 10000 you will get an exception when you execute the query.

The exception is something like this:

"Exception from HRESULT: 0x80040E01"

Here is the StackTrace from ULS log:

Log Query: More Information: Row could not be inserted into the rowset without exceeding provider's maximum number of active rows.

SearchServiceApplication::Execute--Exception: System.Runtime.InteropServices.COMException (0x80040E01): Exception from HRESULT: 0x80040E01
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
at Microsoft.Office.Server.Search.Query.KeywordQueryInternal.Execute()
at Microsoft.Office.Server.Search.Query.QueryInternal.Execute(QueryProperties properties)
at Microsoft.Office.Server.Search.Administration.SearchServiceApplication.Execute(QueryProperties properties)

SearchServiceApplicationProxy::Execute--Error occured: System.ServiceModel.FaultException`1[System.ServiceModel.ExceptionDetail]:
Exception from HRESULT: 0x80040E01 (Fault Detail is equal to An ExceptionDetail, likely created by IncludeExceptionDetailInFaults=true,
whose value is: System.Runtime.InteropServices.COMException: Exception from HRESULT: 0x80040E01
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
at Microsoft.Office.Server.Search.Query.KeywordQueryInternal.Execute()
at Microsoft.Office.Server.Search.Query.QueryInternal.Execute(QueryProperties properties)
at Microsoft.Office.Server.Search.Administration.SearchServiceApplication.Execute(QueryProperties properties)
at SyncInvokeExecute(Object , Object[] , Object[] )
at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs)
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(MessageRpc& rpc) ...).

You can determine this limit by this method in your environment.

# Get the maximum results returned value set for KeywordQuery and FullTextQuery in current environment
Function GetMaxResultsReturned()
{
 # Get reference to Search Service Application
 $ssa = Get-SPEnterpriseSearchServiceApplication
 return $ssa.GetSetting('Config:qp_MaxResultsReturned')
}

This value is configured at Search Service Application level. Now this could be different for your environment but default is 10000. So if you set RowLimit to anything greater than this value (‘Config:qp_MaxResultsReturned’) you will get an exception.
You can modify this value if you want using this function

# Sets the maximum results returned value set for KeywordQuery and FullTextQuery in current environment
Function SetMaxResultsReturned($newLimit)
{
	$ssa = Get-SPEnterpriseSearchServiceApplication
	$ssa.UpdateSetting('Config:qp_MaxResultsReturned', $newLimit)
	$ssa.Update()
}

So, in order to avoid this exception, you should query to retrieve results in chunks and then merge them. In this way you don’t loose on performance and also you will get your results back without facing this exception.

Following, I’m calling the GetSearchResults function (defined above) to perform people search query by retrieving results in pages and merging them in a DataTable.


$startIndex = 0
$rowLimit = 50
$siteCollectionUrl = "<Url of the site collection>"
$searchKey = "s*"

$resultCollection = GetSearchResults $siteCollectionUrl $searchKey $startIndex $rowLimit
$hits = $resultCollection.TotalRows
Write-Host Total hits: $hits
$resultsDataTable = $resultCollection.Table

#Iterate through the rest of the pages to fetch items
while($resultCollection.TotalRows -gt $resultsDataTable.Rows.Count)
{
	$sIndex = $resultsDataTable.Rows.Count
	$resultCollection = GetSearchResults $searchKey $startIndex $rowLimit
        $resultsDataTable.Load($resultCollection, [System.Data.LoadOption]::PreserveChanges)
}

# display the results in table format for better view
$resultsDataTable | Format-Table -AutoSize -Property title , url

Hope it helps!

Update list items to trigger event handler through PowerShell

Scenario:
A SharePoint list has different content types associated with it and every content type has a field of type taxonomy (connected to managed metadata termstore). And that list has an event handler attached to it that is triggered on item add and item update. In that event handler, you push the change to user profile property (for respective user profile) that is also of type managed metadata. So basically when you add or update an item in the list, that value is written back in a user profile property.

Now, under normal circumstances everything should work fine as event handler will kick in and write back the values to user property. However, if you have to delete the user profile or due to different reasons, the values in user profile is wiped off etc, you need to ensure that the data already inserted in the list is also present in the user profile.

Solution:

For such scenarios, what we need to do is trigger the event handler on every list item so that the value is written back to user profile property. PowerShell comes to our rescue in such situation!

Here is the powerShell script for that


Function UpdateListItems($webAppUrl)
 {
  [Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
  {
     #Get Site Collections for respective web applications
     $sitecolls = Get-SPSite -WebApplication $webAppUrl

     Write-Host "Total number of site collections: " $sitecolls.Count

     #Iterate through every site collection to find the list at the root web
     foreach($siteCol in $sitecolls)
     {
        $site = Get-SPSite $siteCol.Url

        Write-Host "Site Collection: " $site.Url
        $rootSite = $site.RootWeb

        Write-Host $rootSite.Name

        #Retrieve the list
        $list = $rootSite.Lists["ListName"]

        if($list -ne $null)
        {

         # Fetch list items only for required content type (If this needs to execute for every content type,
         # just remove the filter from following statement, just write $listItems = $list.Items)
         $listItems = $list.Items | ?{$_.ContentType.Name -eq "ContentTypeName"}

         foreach($item in $listItems)
         {
           Write-Host $item["Field's Dislay Name"]

           #Update the item
           $item.Update()
         }
       }
     }
  })
 }

Now once the function is defined, call it for execution.


UpdateListItems "http://mysite.mydomain.com"

One thing to notice in the script above is that we are using RunWithElevatedPrivileges. My Colleague Matthias Einig has written an amazing post on his blog regarding this, do check it out.

Hope it helps.