Advanced Paging with SharePoint Content Search Web Part Display Templates

In this post, I cover the topic of exposing paging controls within the display templates used by SharePoint 2013’s Content Search Web Part (CSWP). In addition to looking at the underlying paging mechanisms used by the CSWP, I make available a couple of display templates that I created which include advanced paging support.

Click here to download associated ZIP file with display templatesPages in a BookI’ve had an opportunity to play with SharePoint 2013’s Content Search Web Part (CSWP) on a number of occasions in the last couple of years, and I have to say that I like it a lot. The CSWP can be employed to address a whole host of different use cases; in fact, in many situations I’ve found that it can be used to solve problems that were previously addressable only through custom code.

Search-driven content in SharePoint isn’t anything new, of course, but the display templates that are used to format search results in SharePoint 2013 are a large part of the CSWP’s “special sauce.” Through the use of a Control display template and an Item display template, it is possible to select, arrange, and style the search results that are returned and shown to users in a highly customizable fashion. With some knowledge of HTML and the help of SharePoint 2013’s Design Manager, it’s possible to produce some pretty impressive looking content. And if you know JavaScript, well … the sky is the limit on what you can produce.

Control And Item Display Templates

DisplayAndPropertyMappingsBefore I dive into paging and how I’ve tried to stretch what can be done with the CSWP, I want to briefly examine some display template basics. Although this post isn’t intended to be a primer on the CSWP and its associated display templates, there are a few items worth reviewing.

On the right is a snippet of a ToolPart containing some configuration data for a particular CSWP displaying some very basic data. The examples that follow (i.e., the two images shown below) use the configuration shown in the ToolPart, so refer to it if needed.

The first example below is how a CSWP showing three results might appear to an end user when the List with Paging Control template and Two lines Item template are applied. The second example (to the right of the first) differentiates which content regions on the CSWP are being driven by the List with Paging Control template (shown with green highlighting) and which are being driven by the Two lines Item template (shown in red).

Content Search Web Part Example as Seen by End User Content Search Web Part with Template Regions Highlighted

Through these images, I’m trying to convey a very simple point: the Control template dictates how the search results’ “container” appears, and the Item template determines how each individual search result within the container is displayed. The contents of any CSWP are rendered by the output of a single Control display template and zero or more Item display templates (again, one for each search result/item shown).

Nothing To See Here

As you might have guessed from the title of this post, one functional area that seems relatively unexplored and underdeveloped (in my experience) is that of paging and how paging controls are made available to end users of the CSWP. This is unfortunate, because whenever more results are returned than can be displayed at once – a very common scenario – some form of paging is needed.

Out of the box (OOTB), SharePoint 2013 comes with only a handful of Control and Item display templates that can be used with the CSWP to format your search results. Of these display templates, only one contains paging controls: the List with Paging Control template. If all you require is the basic forward/backward paging offered by the two buttons it contains, then the List with Paging display template may be adequate for your needs.

Personally, I think the List with Paging template is bland and kind of … ugly. It works, sure, but it doesn’t display several pieces of information that I find important; for example, the total number of search results and the result page that the user is currently on. Worse is the fact that it doesn’t provide any sort of mechanism to jump to a specific page. The best that an end-user can do is page forward or backward one page at a time.

Better Paging Support

CSWP With Advanced Paging SupportOne of the talks I’ve been giving recently at various SharePoint events and conferences is titled SharePoint’s New Swiss Army Knife: The Content Search Web Part. During that talk, I demonstrate a set of CSWP display templates that I put together to generate something decidedly “non-search” in appearance – like a directory of files to which the current user has access within the current site collection. An image of that CSWP example appears on the left.

This file directory CSWP instance was generated with three simple files: two custom display templates (one for the Control, and one for each Item shown) and a cascading style sheet. The actual result data isn’t particularly remarkable, but the manner in which the paging is implemented is what tends to catch people’s attention. With the Control template I created, end-users can:

  • See the total number of documents (i.e., search results) to which they have access
  • Clearly see which page of search results they’re on through the boxed page number and the “Page xx of yy” label
  • Identify which items/results they’re viewing through the “Items xxx to yyy of zzz” label
  • Jump directly to a page of results by clicking the specified page number

Finding all of this information and displaying it through the CSWP Control template took some research and tinkering. Some of the information was available directly within the search results that were passed to the CSWP, but some of it wasn’t. In the case where some desired information wasn’t available, it was computed with some basic math.

Overview Of The CSWP HTML

By default, the CSWP implements a client-side processing and paging model. Search results that are displayed are controlled by JavaScript functions contained within the Control and Item display templates that have been assigned to the CSWP. When a set of search results is being processed for display within the client browser, the Control template (which is the results container) gets called first to render the HTML that will frame or house the search items/results. For each search result or item that is passed to the CSWP, the Item display template then gets called to create a snippet of HTML for the result/item that can be inserted into the “frame” (commonly a <div>) created by Control display template.

An example of the rendered HTML for the search results shown in the previous “Better Paging Support” section appears below.

<div id="sas-lib-listing">
	<div>Documents To Which I Have Access</div>
	<ul class="sas-table">
		<li class="sas-tablerow">
			<span class="sas-tableheader">Title of Document</span>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_dataContainer">
					<a title="3. Optimus Prime Part - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr1_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=4">3. Optimus Prime Part - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_dataContainer">
					<a title="4. Weather Right Now (Not Really) - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr2_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=5">4. Weather Right Now (Not Really) - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_dataContainer">
					<a title="5. Weather Right Now Page - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr3_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=6">5. Weather Right Now Page - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_dataContainer">
					<a title="6. Weather Right Now Provisioned - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr4_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=8">6. Weather Right Now Provisioned - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_dataContainer">
					<a title="8. Donuts And Drinks Pub - Button" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr5_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SiteAssets/Forms/DispForm.aspx?ID=9">8. Donuts And Drinks Pub - Button</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_dataContainer">
					<a title="WeatherRightNowScraper" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr6_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/WeatherRightNowScraper.aspx">WeatherRightNowScraper</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_dataContainer">
					<a title="DonutsAndDrinksPub" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr7_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/Pages/DonutsAndDrinksPub.aspx">DonutsAndDrinksPub</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_dataContainer">
					<a title="DonutsAndDrinks" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr8_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/DonutsAndDrinks.aspx">DonutsAndDrinks</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_dataContainer">
					<a title="MakingPiTakesEffort" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr9_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/MakingPiTakesEffort.aspx">MakingPiTakesEffort</a>
				</div>
			</div>
		</li>
		<li class="sas-tablerow">
			<div class="sas-tablecell" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_container" data-displaytemplate="SAS_Item_Template">
				<div id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_dataContainer">
					<a title="OptimusPrimePart" id="ctl00_ctl39_g_85d5f2f9_fbe4_4e9b_9d51_647e179b87f9_csr10_SAS_Item_Template_displayTextLink"
					href="http://sp2013-dev:18480/SitePages/OptimusPrimePart.aspx">OptimusPrimePart</a>
				</div>
			</div>
		</li>
	</ul>
	<div class="sas-paging-table">
		<div class="sas-paging-tablerow">
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(1);return Srch.U.cancelEvent(event);" href="#">1</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(11);return Srch.U.cancelEvent(event);" href="#">2</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(21);return Srch.U.cancelEvent(event);" href="#">3</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(31);return Srch.U.cancelEvent(event);" href="#">4</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(41);return Srch.U.cancelEvent(event);" href="#">5</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(51);return Srch.U.cancelEvent(event);" href="#">6</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(61);return Srch.U.cancelEvent(event);" href="#">7</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(71);return Srch.U.cancelEvent(event);" href="#">8</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(81);return Srch.U.cancelEvent(event);" href="#">9</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(91);return Srch.U.cancelEvent(event);" href="#">10</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(101);return Srch.U.cancelEvent(event);" href="#">11</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(111);return Srch.U.cancelEvent(event);" href="#">12</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(121);return Srch.U.cancelEvent(event);" href="#">13</a>
			</div>
			<div class="sas-paging-tablecell sas-paging-currentpage">14</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(141);return Srch.U.cancelEvent(event);" href="#">15</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(151);return Srch.U.cancelEvent(event);" href="#">16</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(161);return Srch.U.cancelEvent(event);" href="#">17</a>
			</div>
			<div class="sas-paging-tablecell">
				<a onclick="$getClientControl(this).page(171);return Srch.U.cancelEvent(event);" href="#">18</a>
			</div>
			<div class="sas-paging-tablecell sas-paging-summary">Page
				<span>14</span>of
				<span>18</span>, items
				<span>131</span>to
				<span>140</span>of
				<span>179</span>.</div>
		</div>
	</div>
</div>

The Control template (SwissArmy_Control_Template.js) creates the top level <div> and a child <ul> element (with a CSS class of “sas-table”) for the overall HTML structure, as well as the individual <li> elements used to contain each search/result item. For each search result/item, the Item template (SwissArmy_Item_Template.js) creates a <div> block that is inserted as a child within an <li> element. Each of the individual item <div> blocks created by the Item template has a CSS class of “sas-tablecell” for easier styling.

Below the <ul> block housing the search results is another <div> with a CSS class of “sas-paging-table.” As suggested by the class name, the <div> is used to house the clickable page numbers and additional paging information seen at the bottom of the CSWP control. And like the rest of the container information, this HTML is generated within the Control template.

Fetching Search Results

Each time a set of search results is needed, either on initial page rendering or when the user moves to a new page, the CSWP calls back to its SharePoint site collection for the data it needs. By default, this call occurs asynchronously; however, the CSWP can be configured to make such calls synchronously within the normal page processing sequence.

For example, if I had a page containing a CSWP at the following URL

http://sp2013-dev:18580/DisplayTemplateStyling

… the CSWP on that page would request search results for display by posting a request to the following endpoint:

http://sp2013-dev:18580/_vti_bin/client.svc/ProcessQuery

As part of its request, the CSWP passes an XML structure to the client.svc web service that looks something like the XML that appears below. This structure contains all of the information the ProcessQuery method needs to determine which search results should be sent back:

<Request xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009" SchemaVersion="15.0.0.0" LibraryVersion="15.0.0.0" ApplicationName="Javascript Library">
	<Actions>
		<ObjectPath Id="1" ObjectPathId="0" />
		<SetProperty Id="2" ObjectPathId="0" Name="QueryTemplate">
			<Parameter Type="String">(contentclass:STS_ListItem OR IsDocument:True)</Parameter>
		</SetProperty>
		<ObjectPath Id="4" ObjectPathId="3" />
		<Method Name="Add" Id="5" ObjectPathId="3">
			<Parameters>
				<Parameter Type="String">LastModifiedTime</Parameter>
				<Parameter Type="Number">1</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="6" ObjectPathId="0" Name="StartRow">
			<Parameter Type="Number">130</Parameter>
		</SetProperty>
		<SetProperty Id="7" ObjectPathId="0" Name="RowsPerPage">
			<Parameter Type="Number">10</Parameter>
		</SetProperty>
		<SetProperty Id="8" ObjectPathId="0" Name="RowLimit">
			<Parameter Type="Number">10</Parameter>
		</SetProperty>
		<SetProperty Id="9" ObjectPathId="0" Name="TotalRowsExactMinimum">
			<Parameter Type="Number">141</Parameter>
		</SetProperty>
		<SetProperty Id="10" ObjectPathId="0" Name="SourceId">
			<Parameter Type="Guid">{8413cd39-2156-4e00-b54d-11efd9abdb89}</Parameter>
		</SetProperty>
		<ObjectPath Id="12" ObjectPathId="11" />
		<Method Name="SetQueryPropertyValue" Id="13" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">SourceName</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">Local SharePoint Results</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="14" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">SourceLevel</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">Ssa</Property>
				</Parameter>
			</Parameters>
		</Method>
		<ObjectPath Id="16" ObjectPathId="15" />
		<Method Name="Add" Id="17" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">Path</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="18" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">Title</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="19" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">
 FileExtension</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="20" ObjectPathId="15">
			<Parameters>
				<Parameter Type="String">
 SecondaryFileExtension
</Parameter>
			</Parameters>
		</Method>
		<ObjectPath Id="22" ObjectPathId="21" />
		<Method Name="Add" Id="23" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Title</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="24" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Path</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="25" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">Author</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="26" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">SectionNames</Parameter>
			</Parameters>
		</Method>
		<Method Name="Add" Id="27" ObjectPathId="21">
			<Parameters>
				<Parameter Type="String">SiteDescription</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="28" ObjectPathId="0" Name="TrimDuplicates">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<Method Name="SetQueryPropertyValue" Id="29" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TryCache</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="30" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">Scope</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">{Site.URL}</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="31" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">UpdateLinksForCatalogItems</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="32" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">EnableStacking</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">true</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">3</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="33" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">ListId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">fb9e18d3-1d80-4ab1-8e76-bc36b0a8e22d</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="34" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">ListItemId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">6</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">2</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="Null" />
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="35" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">0bfc89ed-1741-4925-9d25-828eaf74b2c8</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="36" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermSetId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">0f14334e-b929-42bf-ad10-ed6a93d16a4f</Property>
				</Parameter>
			</Parameters>
		</Method>
		<Method Name="SetQueryPropertyValue" Id="37" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">TermStoreId</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">c4c58328-0e88-4937-91e9-cf7526b9b0db</Property>
				</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="38" ObjectPathId="0" Name="ResultsUrl">
			<Parameter Type="String">http://sp2013-dev:18580/DisplayTemplateStyling#k=#s=131</Parameter>
		</SetProperty>
		<SetProperty Id="39" ObjectPathId="0" Name="BypassResultTypes">
			<Parameter Type="Boolean">true</Parameter>
		</SetProperty>
		<SetProperty Id="40" ObjectPathId="0" Name="ClientType">
			<Parameter Type="String">ContentSearchRegular</Parameter>
		</SetProperty>
		<SetProperty Id="41" ObjectPathId="0" Name="EnableInterleaving">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<SetProperty Id="42" ObjectPathId="0" Name="ProcessBestBets">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<Method Name="SetQueryPropertyValue" Id="43" ObjectPathId="11">
			<Parameters>
				<Parameter Type="String">QuerySession</Parameter>
				<Parameter TypeId="{b25ba502-71d7-4ae4-a701-4ca2fb1223be}">
					<Property Name="BoolVal" Type="Boolean">false</Property>
					<Property Name="IntVal" Type="Number">0</Property>
					<Property Name="QueryPropertyValueTypeIndex" Type="Number">1</Property>
					<Property Name="StrArray" Type="Null" />
					<Property Name="StrVal" Type="String">a4e0e9b5-6dd9-4aea-b38b-26f325c67fdf</Property>
				</Parameter>
			</Parameters>
		</Method>
		<SetProperty Id="44" ObjectPathId="0" Name="ProcessPersonalFavorites">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<SetProperty Id="45" ObjectPathId="0" Name="SafeQueryPropertiesTemplateUrl">
			<Parameter Type="String">querygroup://webroot/Pages/DisplayTemplateStyling.aspx?groupname=Default</Parameter>
		</SetProperty>
		<SetProperty Id="46" ObjectPathId="0" Name="IgnoreSafeQueryPropertiesTemplateUrl">
			<Parameter Type="Boolean">false</Parameter>
		</SetProperty>
		<ObjectPath Id="48" ObjectPathId="47" />
		<ExceptionHandlingScope Id="49">
			<TryScope Id="51">
				<Method Name="ExecuteQueries" Id="53" ObjectPathId="47">
					<Parameters>
						<Parameter Type="Array">
							<Object Type="String">26103082-0e16-4ab0-a12c-bded636a7fb8Default</Object>
						</Parameter>
						<Parameter Type="Array">
							<Object ObjectPathId="0" />
						</Parameter>
						<Parameter Type="Boolean">true</Parameter>
					</Parameters>
				</Method>
			</TryScope>
			<CatchScope Id="55" />
		</ExceptionHandlingScope>
	</Actions>
	<ObjectPaths>
		<Constructor Id="0" TypeId="{80173281-fffd-47b6-9a49-312e06ff8428}" />
		<Property Id="3" ParentId="0" Name="SortList" />
		<Property Id="11" ParentId="0" Name="Properties" />
		<Property Id="15" ParentId="0" Name="SelectProperties" />
		<Property Id="21" ParentId="0" Name="HitHighlightedProperties" />
		<Constructor Id="47" TypeId="{8d2ac302-db2f-46fe-9015-872b35f15098}" />
	</ObjectPaths>
</Request>				 				 			 			 		 	 	 		 		 		 		 		 		 	  

Once the server finds the desired search results and processes them, it packages them up as a JSON object and returns that object to the browser for further action. The following is an example of a JSON structure (containing ten results) that was returned for the Swiss Army Knife CSWP as it was shown earlier in the “Better Paging Support” section:

[  
   {  
      "SchemaVersion":"15.0.0.0",
      "LibraryVersion":"15.0.4569.1501",
      "ErrorInfo":null,
      "TraceCorrelationId":"36d5029d-dcee-709e-a995-81ad013126e0"
   },
   119,
   {  
      "IsNull":false
   },
   122,
   {  
      "IsNull":false
   },
   130,
   {  
      "IsNull":false
   },
   134,
   {  
      "IsNull":false
   },
   140,
   {  
      "IsNull":false
   },
   166,
   {  
      "IsNull":false
   },
   171,
   {  
      "418e02d2-9bc5-46fd-ada2-83762ed79256Default":{  
         "_ObjectType_":"Microsoft.SharePoint.Client.Search.Query.ResultTableCollection",
         "ElapsedTime":31,
         "Properties":{  
            "RowLimit":10,
            "SourceId":"\/Guid(8413cd39-2156-4e00-b54d-11efd9abdb89)\/",
            "EnableStacking":true,
            "SerializedQuery":"<Query Culture=\"en-US\" EnableStemming=\"True\" EnablePhonetic=\"False\" EnableNicknames=\"False\" IgnoreAllNoiseQuery=\"True\" SummaryLength=\"180\" MaxSnippetLength=\"180\" DesiredSnippetLength=\"90\" KeywordInclusion=\"0\" QueryText=\"\" QueryTemplate=\"(contentclass:STS_ListItem OR IsDocument:True)\" TrimDuplicates=\"False\" Site=\"e964e9f4-d726-4c46-bda6-00367a81bb29\" Web=\"1b739834-de53-4968-b4cc-04a559306668\" KeywordType=\"True\" HiddenConstraints=\"\" \u002f>"
         },
         "QueryErrors":null,
         "QueryId":"e12d2e7f-598c-4db7-ae6e-fbf021d0b230",
         "SpellingSuggestion":"",
         "TriggeredRules":[  

         ],
         "ResultTables":[  
            {  
               "_ObjectType_":"Microsoft.SharePoint.Client.Search.Query.ResultTable",
               "GroupTemplateId":null,
               "ItemTemplateId":null,
               "Properties":{  
                  "GenerationId":9223372036854775806,
                  "ExecutionTimeMs":16,
                  "QueryModification":"(contentclass:STS_ListItem OR IsDocument:True) -ContentClass=urn:content-class:SPSPeople",
                  "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fGroup_Default.js",
                  "StartRecord":0
               },
               "QueryId":"e12d2e7f-598c-4db7-ae6e-fbf021d0b230",
               "QueryRuleId":"00000000-0000-0000-0000-000000000000",
               "ResultRows":[  
                  {  
                     "Rank":0,
                     "DocId":114,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=4",
                     "Title":"3. Optimus Prime Part - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=4",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":115,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=5",
                     "Title":"4. Weather Right Now (Not Really) - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=5",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":116,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=6",
                     "Title":"5. Weather Right Now Page - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=6",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":117,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=8",
                     "Title":"6. Weather Right Now Provisioned - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=8",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":119,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=9",
                     "Title":"8. Donuts And Drinks Pub - Button",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSiteAssets\u002fForms\u002fDispForm.aspx?ID=9",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":688,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fWeatherRightNowScraper.aspx",
                     "Title":"WeatherRightNowScraper",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fWeatherRightNowScraper.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":683,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fPages\u002fDonutsAndDrinksPub.aspx",
                     "Title":"DonutsAndDrinksPub",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fPages\u002fDonutsAndDrinksPub.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":685,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fDonutsAndDrinks.aspx",
                     "Title":"DonutsAndDrinks",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fDonutsAndDrinks.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":686,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fMakingPiTakesEffort.aspx",
                     "Title":"MakingPiTakesEffort",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fMakingPiTakesEffort.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  },
                  {  
                     "Rank":0,
                     "DocId":139,
                     "Path":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fOptimusPrimePart.aspx",
                     "Title":"OptimusPrimePart",
                     "OriginalPath":"http:\u002f\u002fsp2013-dev:18480\u002fSitePages\u002fOptimusPrimePart.aspx",
                     "PartitionId":"\/Guid(0c37852b-34d0-418e-91c6-2ac25af4be5b)\/",
                     "UrlZone":0,
                     "AAMEnabledManagedProperties":"AttachmentURI;deeplinks;DefaultEncodingURL;ExternalMediaURL;HierarchyUrl;OrgParentUrls;OrgUrls;OriginalPath;ParentLink;Path;PictureThumbnailURL;PictureURL;PublishingImage;recommendedfor;ServerRedirectedEmbedURL;ServerRedirectedPreviewURL;ServerRedirectedURL;SiteLogo;SitePath;SPSiteURL;UserEncodingURL",
                     "RenderTemplateId":"~sitecollection\u002f_catalogs\u002fmasterpage\u002fDisplay Templates\u002fSearch\u002fItem_Default.js",
                     "QueryRuleId":"\/Guid(00000000-0000-0000-0000-000000000000)\/"
                  }
               ],
               "ResultTitle":null,
               "ResultTitleUrl":null,
               "RowCount":10,
               "TableType":"RelevantResults",
               "TotalRows":179,
               "TotalRowsIncludingDuplicates":179
            }
         ]
      }
   },
   167,
   {  
      "HasException":false,
      "ErrorInfo":null
   }
]

This JSON structure contains a lot of information – certainly more information than is being displayed by the CSWP. The trick, of course, is in figuring out exactly “what” is “where” for purposes of building a paging system.

Page-Related Processing Within The Control Template

Thankfully, the CSWP provides us with a relatively easy mechanism for getting at the search result data we care about within the JSON object that is returned from the call to client.svc. When a Control display template is invoked by the CSWP, it is passed a context object as follows:

function DisplayTemplate_2aa45a743fd94e75a3e53c940628e2ec(ctx) { ... } 

ctx Content ObjectThe ctx context object contains a number of useful methods, properties, and subordinate objects we can leverage in our attempts to manipulate search results and calculate paging information. We can use ctx to interact directly with the CSWP (which is accessible through the ClientControl property), as well as obtain the search results themselves (and information about them) through the ListData property.

Note: Even though Microsoft’s List with Paging Control template doesn’t provide anything more than relative paging forward and backward, the CSWP does appear to support some form of more advanced paging scheme through its get_pagingInfo() method. When this method is called, it returns an object that contains expanded paging information that includes information about the current page, as well as a subset of pages before and after the current page. The object does not, however, contain all of the information needed to determine the total number of items in the search result set, how to access those pages, etc.

Fortunately for us, though, the ctx.ListData object contains everything we need to implement an end-to-end paging system. Here’s how each of the critical paging information pieces is located or computed within the Control template so that the portion of the control seen below can be rendered:

Paging Line

  • ctx.ListData InspectionTotal number of all search results available. The totalRowCount variable is used within the Control display template scripting to store this value. And the value itself is readily available through the ctx.ListData.ResultTables[0].TotalRows property.
  • Maximum number of items to display per page. This value (represented within the Control display template scripting as rowsPerPageCount) is easily obtained through the ctx.ListData.Properties.RowLimit property. It identifies the maximum number of rows that may be returned in the search results table (through ctx.ListData.ResultTables[0]), and it is the value which should be used to determine the number of individual search results shown in the CSWP.
  • Number of items to display on the current page. In most cases, this value (defined through the rowsOnCurrentPageCount variable within the Control display template scripting) will be the same as the maximum number of items per page. On the last page of results, though, it isn’t uncommon for there to be fewer results available than the maximum number possible. To determine how many items should be shown on the current page of search results, the script simply determines how many rows are present in the current search results table (ctx.ListData.ResultTables[0].RowCount).
  • First page number for the result set. As my kids would say, “Easy peasy lemon squeezy.” This variable (firstPageNumber) always has the intuitive value of 1. This variable should not be confused with the firstPage variable that commonly appears in the OOTB display template scripting. The reference which is assigned to the firstPage variable is a PagingLink object that is meaningful within the context of the pagingInfo object returned from the call to the ctx.ClientControl.get_pagingInfo() – not the page calculations in the display templates associated with this post.
  • Last page number for the result set. This value (represented by the lastPageNumber variable) isn’t readily available in a way that can be “plucked” out of the search data, so the display template scripting simply does a little math to calculate it: Math.ceil(totalRowCount /rowsPerPageCount). In non-code terms: the last page of the total search results set is the total number of results in the set divided by the number of results displayed per page, rounded-down. As with the firstPageNumber and firstPage variables, don’t confuse lastPageNumber with the OOTB lastPage object reference.

In addition to the values described above, a series of calculations are performed to determine the currentPageStartItem and currentPageEndItem variables that represent the first and last item numbers on the current page. Once these values are known and assigned to their respective variables, it becomes a relatively straightforward exercise to display the paging information as is done within the SwissArmy_Control_Template.js Control template. The (not-so-heavy) lifting is accomplished with the following Javascript:

// Figure out the current page information
for (var i = 0; i< pagingInfo.length; i++)
{
	var pl = pagingInfo[i];
	if (!$isNull(pl))
	{
		if (pl.startItem == -1)
		{
			currentPage = pl;
			currentPageNumber = pl.pageNumber;
			currentPageStartItem = ((currentPageNumber - 1) * rowsPerPageCount) + 1;
			if (currentPageNumber == lastPageNumber) {
				currentPageEndItem = totalRowCount;
			} else {
				currentPageEndItem = currentPageNumber * rowsPerPageCount;
			}
		}
	}
}

// Generate page divs and their links
var getPageNumberDivs = function() {
	var currentDiv, divBlocks, pageStartItem;
	divBlocks = '';
	for (var i = firstPageNumber; i <= lastPageNumber; i++) {
		cellStyle = 'sas-paging-tablecell';
		if (i == currentPageNumber) {
			currentDiv = '<div class="sas-paging-tablecell sas-paging-currentpage">';
			currentDiv += i;
			currentDiv += '</div>';
		} else {
			pageStartItem = ((i - 1) * rowsPerPageCount) + 1;
			currentDiv = '<div class="sas-paging-tablecell">';
			currentDiv += '<a href="#" onclick="$getClientControl(this).page(' + $htmlEncode(pageStartItem) + ');return Srch.U.cancelEvent(event);">';
			currentDiv += i;
			currentDiv += '</a></div>';
		}
		divBlocks += currentDiv;
	}
	return divBlocks;
}

// Generate the block that indicates current page, item counts, etc.
var getPageSummary = function() {
	var currentDiv;
	var pageInfo = 'Page {1} of {2}, items {3} to {4} of {5}.';
	pageInfo = pageInfo.replace('{1}', '<span>' + currentPageNumber + '</span>');
	pageInfo = pageInfo.replace('{2}', '<span>' + lastPageNumber + '</span>');
	pageInfo = pageInfo.replace('{3}', '<span>' + currentPageStartItem + '</span>');
	pageInfo = pageInfo.replace('{4}', '<span>' + currentPageEndItem + '</span>');
	pageInfo = pageInfo.replace('{5}', '<span>' + totalRowCount + '</span>');
	currentDiv = '<div class="sas-paging-tablecell sas-paging-summary">' + '</span>';
	currentDiv += pageInfo + '</div>';
	return currentDiv;
}

ms_outHtml.push(''
,''
,'			</ul>'
,''
,'			<div class="sas-paging-table">'
,'				<div class="sas-paging-tablerow">'
,'					', getPageNumberDivs() ,''
,'					', getPageSummary() ,''
,'				</div>'
,'			</div>'
,'		</div>'
,'		'
);

Creating The Page Links

One area does warrant a little more explanation, though, and that is how the hyperlinks are created for each of the clickable page numbers at the bottom of the CSWP. There really isn’t a whole lot of magic behind the creation of the hyperlinks themselves; the results are achieved on line 101 of the SwissArmy_Control_Template.js file:

 
currentDiv += '<a href="#" onclick="$getClientControl(this).page(' + $htmlEncode(pageStartItem) + ');return Srch.U.cancelEvent(event);">';

The pageStartItem variable value is computed as follows …

 
pageStartItem = ((i - 1) * rowsPerPageCount) + 1; 

… where i is the value of the new result page being clicked by the user. For example, if the user clicks on “17” (to indicate a desire to move to page 17 of the search results), the value of i will be 161 when there are 10 items per page.

http://sp2013-dev:18580/DisplayTemplateStyling#k=#s=161

On the server side, SharePoint translates the request for 161 (which can also be reached directly via re-post by attaching a #s=161 to the query string portion of the URL as shown above) to package-up and send down a search results table containing all of the page 17 result items.

How Do I Use The Sample (Downloadable) Files?

Once you’ve downloaded the SwissArmyTemplates.zip file and opened it up, you’ll find that it contains five files:

  • SwissArmy_Control_Template.html
  • SwissArmy_Control_Template.js
  • SwissArmy_Item_Template.html
  • SwissArmy_Item_Template.js
  • SwissArmy_Styles.css

The two HTML files are each of the display templates in their HTML form. These HTML files can be processed through SharePoint 2013’s Design Manager to generate the corresponding JavaScript files that are actually used by the CSWP. Alternatively, the JS files that are included in the ZIP file can be used as-is and dropped directly into the Master Page Gallery > Display Templates > Content Web Parts folder within a site collection to make them available to the CSWP.

The remaining file is a CSS style sheet, and it provides the look and feel that is employed by the display templates. The style sheet itself is referenced from with the Control template files (SwissArmy_Control_Template.html and SwissArmy_Control_Template.js), and the Control template files assume that the style sheet will be available in the following location within the site collection:

~sitecollection/Style Library/en-us/SwissArmyStyles/SwissArmy_Styles.css

If you place the style sheet somewhere else in the site collection, ensure that the $includeCSS() calls in the SwissArmy_Control_Template.html file (line 25) and the SwissArmy_Control_Template.js file (line 164) are updated accordingly.

Final Request

Thanks for citing appropriatelyAs with all of the resources I make available, please feel free to use the display templates and style sheet I’ve provided within your own projects, either as-is or in a form that you’ve modified to suit your needs.

If you do share or redistribute what I’ve provided in some form, whether or not you’ve made modifications, I simply ask that you reference the original source (i.e., me and/or this blog post) in what you’re sharing. I do believe in citing sources where appropriate.

Thanks, and have fun paging through search results!

References and Resources

  1. SharePoint Interface: Files for the Swiss Army Knife CSWP example
  2. MSDN: Content Search Web Part in SharePoint 2013
  3. MSDN: SharePoint 2013 Design Manager display templates
  4. MSDN: Overview of Design Manager in SharePoint 2013
  5. SharePoint Interface: Presentation for SharePoint’s New Swiss Army Knife: The Content Search Web Part
  6. MSDN: 3.1.4.1 ProcessQuery
  7. JSON.org: Introducing JSON

Finding Duplicate GUIDs in Your SharePoint Site Collection

In this self-described “blog post you should never need,” I talk about finding objects with duplicate GUIDs in a client’s SharePoint site collection. I supply the PowerShell script used to find the duplicate GUIDs and offer some suggestions for how you might remedy such a situation.

This is a bit of an oldie, but I figured it might help one or two random readers.

Let me start by saying something right off the bat: you should never need what I’m about to share.  Of course, how many times have you heard “you shouldn’t ever really need this” when it comes to SharePoint?  I’ve been at it a while, and I can tell you that things that never should happen seem to find a way into reality – and into my crosshairs for troubleshooting.

Disclaimer

The story and situation I’m about to share is true.  I’m going to speak in generalities when it comes to the identities of the parties and software involved, though, to “protect the innocent” and avoid upsetting anyone.

The Predicament

I was part of a team that was working with a client to troubleshoot problems that the client was encountering when they attempted to run some software that targeted SharePoint site collections.  The errors that were returned by the software were somewhat cryptic, but they pointed to a problem handling certain objects in a SharePoint site collection.  The software ran fine when targeting all other site collections, so we naturally suspected that something was wrong with only one specific site collection.

After further examination of logs that were tied to the software, it became clear that we had a real predicament.  Apparently, the site collection in question contained two or more objects with the same identity; that is, the objects had ID properties possessing the same GUID.  This isn’t anything that should ever happen, but it had.  SharePoint continued to run without issue (interestingly enough), but the duplication of object GUIDs made it downright difficult for any software that depended on unique object identities being … well, unique.

Although the software logs told us which GUID was being duplicated, we didn’t know which SharePoint object or objects the GUID was tied to.  We needed a relatively quick and easy way to figure out the name(s) of the object or objects which were being impacted by the duplicate GUIDs.

Tackling the Problem

It is precisely in times like those described that PowerShell comes to mind.

My solution was to whip-up a PowerShell script (FindDuplicateGuids.ps1) that processed each of the lists (SPList) and webs (SPWeb) in a target site collection.  The script simply collected the identities of each list and web and reported back any GUIDs that appeared more than once.

The script created works with both SharePoint 2007 and SharePoint 2010, and it has no specific dependencies beyond SharePoint being installed and available on the server where the script is run.

########################
# FindDuplicateGuids.ps1
# Author: Sean P. McDonough (sean@sharepointinterface.com)
# Blog: http://SharePointInterface.com
# Last Update: August 29, 2013
#
# Usage from prompt: ".\FindDuplicateGuids.ps1 <siteUrl>"
#   where <siteUrl> is site collection root.
########################


#########
# IMPORTS
# Import/load common SharePoint assemblies that house the types we'll need for operations.
#########
Add-Type -AssemblyName "Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"


###########
# FUNCTIONS
# Leveraged throughout the script for one or more calls.
###########
function SpmBuild-WebAndListIdMappings {param ($siteUrl)
	$targetSite = New-Object Microsoft.SharePoint.SPSite($siteUrl)
	$allWebs = $targetSite.AllWebs
	$mappings = New-Object System.Collections.Specialized.NameValueCollection
	foreach ($spWeb in $allWebs)
	{
		$webTitle = "WEB '{0}'" -f $spWeb.Title
		$mappings.Add($spWeb.ID, $webTitle)
		$allListsForWeb = $spWeb.Lists
		foreach ($currentList in $allListsForWeb)
		{
			$listEntry = "LIST '{0}' in Web '{1}'" -f $currentList.Title, $spWeb.Title
			$mappings.Add($currentList.ID, $listEntry)
		}
		$spWeb.Dispose()
	}
	$targetSite.Dispose()
	return ,$mappings
}

function SpmFind-DuplicateMembers {param ([System.Collections.Specialized.NameValueCollection]$nvMappings)
	$duplicateMembers = New-Object System.Collections.ArrayList
	$allkeys = $nvMappings.AllKeys
	foreach ($keyName in $allKeys)
	{
		$valuesForKey = $nvMappings.GetValues($keyName)
		if ($valuesForKey.Length -gt 1)
		{
			[void]$duplicateMembers.Add($keyName)
		}
	}
	return ,$duplicateMembers
}


########
# SCRIPT
# Execution of actual script logic begins here
########
$siteUrl = $Args[0]
if ($siteUrl -eq $null)
{
	$siteUrl = Read-Host "`nYou must supply a site collection URL to execute the script"
}
if ($siteUrl.EndsWith("/") -eq $false)
{
	$siteUrl += "/"
}
Clear-Host
Write-Output ("Examining " + $siteUrl + " ...`n")
$combinedMappings = SpmBuild-WebAndListIdMappings $siteUrl
Write-Output ($combinedMappings.Count.ToString() + " GUIDs processed.")
Write-Output ("Looking for duplicate GUIDs ...`n")
$duplicateGuids = SpmFind-DuplicateMembers $combinedMappings
if ($duplicateGuids.Count -eq 0)
{
	Write-Output ("No duplicate GUIDs found.")
}
else
{
	Write-Output ($duplicateGuids.Count.ToString() + " duplicate GUID(s) found.")
	Write-Output ("Non-unique GUIDs and associated objects appear below.`n")
	foreach ($keyName in $duplicateGuids)
	{
		$siteNames = $combinedMappings[$keyName]
		Write-Output($keyName + ": " + $siteNames)
	}
}
$dumpData = Read-Host "`nDo you want to send the collected data to a file? (Y/N)"
if ($dumpData -match "y")
{
	$fileName = Read-Host "  Output file path and name"
	Write-Output ("Results for " + $siteUrl) | Out-File -FilePath $fileName
	$allKeys = $combinedMappings.AllKeys
	foreach ($currentKey in $allKeys)
	{
		Write-Output ($currentKey + ": " + $combinedMappings[$currentKey]) | Out-File -FilePath $fileName -Append
	}
}
Write-Output ("`n")

Running this script in the client’s environment quickly identified the two lists that contained the same ID GUIDs.  How did they get that way?  I honestly don’t know, nor am I going to hazard a guess …

What Next?

If you’re in the unfortunate position of owning a site collection that contains objects possessing duplicate ID GUIDs, let me start by saying “I feel for you.”

Having said that: the quickest fix seemed to be deleting the objects that possessed the same GUIDs.  Those objects were then rebuilt.  I believe we handled the delete and rebuild manually, but there’s nothing to say that an export and subsequent import (via the Content Deployment API) couldn’t be used to get content out and then back in with new object IDs. 

A word of caution: if you do leverage the Content Deployment API and do so programmatically, simply make sure that object identities aren’t retained on import; that is, make sure that SPImportSettings.RetainObjectIdentity = false – not true.

Additional Reading and References

  1. TechNet: Import and export: STSADM operations
  2. MSDN: SPImportSettings.RetainObjectIdentity

The ApplyApplicationContentToLocalServer Method and Why It Comes Up Short

This post explores the SPWebService’s ApplyApplicationContentToLocalServer method, the constraints one faces when using it, and an alternative to its use when updating application page sitemap files.

Caching capabilities that are available (or exposed) through MOSS are something I spend a fair number of working hours focusing on.  MOSS publishing farms can make use of quite a few caching options, and wise administrators find ways to leverage them all for maximum scalability and performance. While helping a client work through some performance and scalability issues recently, I ran into some annoying problems with disk-based caching – also known as BLOB (Binary Large OBject) caching. These problems inspired me to create the BlobCacheFarmFlush solution that I’ve shared on CodePlex, and it was during the creation of this solution that I wrangled with the ApplyApplicationContentToLocalServer method.

Background

The BlobCacheFarmFlush solution itself has a handful of moving parts, and the element I’m going to focus on in this post is the administration page (BlobCacheFarmFlush.aspx) that gets added to the farm upon Feature activation.  In particular, I want to share some of the lessons I learned while figuring out how to get the page’s navigational (breadcrumb) support operating properly.

Unlike “standard” content pages that one might deploy through a SharePoint Feature or solution package, application pages (also called “layouts pages” because they go into the LAYOUTS folder within SharePoint’s 12 hive) don’t come with wired-up breadcrumb support.  An example of the type of breadcrumb to which I’m referring appears below (circled in red):

Application Page Breadcrumb Example

Unless additional steps are taken during the installation of your application pages (beyond simply placing them in the LAYOUTS folder), breadcrumbs like the one shown above will not appear.  It’s not that application pages (which derive from LayoutsBasePage or UnsecuredLayoutsBasePage) don’t include support for breadcrumbs – they do.  The reason breadcrumbs fail to show is because the newly added application pages themselves are not integrated into the sitemap files that describe the navigational hierarchy of the layouts pages.

Wiring Up Breadcrumb Support

Getting breadcrumbs to appear in your own application pages requires that you update the layouts sitemap files for each of the (IIS) sites serving up content on each of the SharePoint web front-end (WFE) servers in your farm.  The files to which I’m referring are named layouts.sitemap and appear in the _app_bin folder of each IIS site folder on the WFE.  An example of one such file (in its _app_bin folder) appears below.

A SharePoint Site's LAYOUTS SiteMap File

I’m a “best practices” kind of guy, so when I was doing research for my BlobCacheFarmFlush solution, I was naturally interested in trying to make the required sitemap modifications in a way that was both easy and supported.  It didn’t take much searching on the topic before I came across Jan Tielens’ blog post titled “Adding Breadcrumb Navigation To SharePoint Application Pages, The Easy Way.”  In his blog post, Jan basically runs through the scenario I described above (though in much greater detail than I presented), and he mentions that another reader (Brian Staton) turned him onto a very simple and straightforward way of making the required sitemap modifications.  I’ll refer you to Jan’s blog post for the specifics, but the two-step quick summary goes like this:

  1. Create a layouts.sitemap.*.xml file that contains your sitemap navigation additions and deploy it to the LAYOUTS folder within SharePoint’s 12 hive on a server.
  2. Execute code that implements one of the two approaches shown below (typically on Feature activation) :
// Approach #1: Top-down starting at the SPFarm level
SPFarm.Local.Services.GetValue<SPWebService>().ApplyApplicationContentToLocalServer();

// Approach #2: Applying to the sites within an SPWebApplication
myWebApp.WebService.ApplyApplicationContentToLocalServer();

This isn’t much code, and it’s pretty clear that the magic rests with the ApplyApplicationContentToLocalServer method.  This method carries out a few operations, but the one in which we’re interested involves taking the new navigation nodes in the layouts.sitemap.*.xml file and integrating them into the layouts.sitemap file for each IIS site residing under a target SPWebService instance.  With the new nodes (which tie the new application pages into the navigational hierarchy) present within each layouts.sitemap file, breadcrumbs appear at the top of the new application pages when they are rendered.

I took this approach for a spin, and everything looked great!  My sitemap additions were integrated as expected, and my breadcrumb appeared on the BlobCacheFarmFlush.aspx page.  All was well .. until I actually deployed my solution to its first multi-server SharePoint environment.  That’s when I encountered my first problem.

Problem #1: The “Local” Part of the ApplyApplicationContentToLocalServer Method

When I installed and activated the BlobCacheFarmFlush solution in a multi-server environment, the breadcrumbs failed to appear on my application page.  It took a little legwork, but I discovered that the ApplyApplicationContentToLocalServer method has “Local” in its name for a reason: the changes made through the method’s actions only impact the server on which the method is invoked.

This contrasts with the behavior that SharePoint objects commonly exhibit.  The changes that are made through (and to) many SharePoint types impact data that is actually stored in SQL Server, and changes made through any farm member get persisted back to the appropriate database and become available through all servers within the farm.  The ApplyApplicationContentToLocalServer method, on the other hand, carries out its operations directly against the files and folders of the server on which the method is called, and the changes that are made do not “automagically” appear on or through other farm members.

The Central Administration host server for the farm in which I was activating my Feature wasn’t one of the WFEs serving up my application page.  When I activated my Feature from within Central Admin, my navigation additions were incorporated into the affected sites on the local (Central Admin) host … but the WFEs serving up actual site pages (and my application page) were not updated.  Result: no breadcrumb on my application page.

This issue is one of those problems that wouldn’t normally be discovered in a typical development environment.  Most of the SharePoint developers I know do their work within a virtual machine (VM) of some sort, so it’s not until one moves out of such an environment and into a multi-server environment that this type of deployment problem even makes itself known.  This issue only serves to underscore how important it is to test Features and solutions in a typical target deployment environment before releasing them for general use.

Putting my thinking cap back on, I worked to come up with another way to integrate the sitemap changes I needed in a way that was multi-server friendly.  The ApplyApplicationContentToLocalServer method still seemed like a winner given all that it did for a single line of code; perhaps all I needed to do was create and run a one-time custom timer job (that is, schedule a custom SPJobDefinition subclass) on each server within the farm and have that timer job execute the ApplyApplicationContentToLocalServer method locally.

I whipped-up a custom timer job to carry out this action and took it for a spin.  That’s when I ran into my second problem.

Problem #2: Rights Required for ApplyApplicationContentToLocalServer Method Invocation

The documentation for the ApplyApplicationContentToLocalServer method ends with this one line:

Only local administrators can call this method.

Prior to the creation of the custom timer job that I was going to use to update the sitemap files on each of the WFEs, I had basically ignored this point.  The local administrator requirement quickly became a barricade for my custom timer job, though.

Timer jobs, both SharePoint-supplied and custom, are executed within the context of the SharePoint Timer Service (OWSTIMER.EXE).  The Timer Service runs in an elevated security context with regard to the SharePoint farm, but its privileges shouldn’t extend beyond the workings of SharePoint.  Though some SharePoint administrators mistakenly believe that the Timer Service account (also known as the “database access account” or “farm service account”) requires local administrator rights on each server within the SharePoint farm, Microsoft spells out that this is neither required nor recommended.

The ApplyApplicationContentToLocalServer method works during Feature activation when the activating user is a member of the Local Administrators group on the server where activation is taking place – a common scenario.  The process breaks down, however, if the method call occurs within the context of the SharePoint Timer Service account because it isn’t (or shouldn’t be) a member of the Local Administrators group.  Attempts to call the ApplyApplicationContentToLocalServer method from within a timer job fail and result in an “Access Denied” message being written to the Application Event Log.  A quick look at the first section of code inside the method itself (using Reflector) makes this point pretty clearly:

if (!SPAdministrationServiceUtilities.IsCurrentUserMachineAdmin())
{
    throw new SecurityException(SPResource.GetString("AccessDenied", new object[0]));
}

This revelation told me that the ApplyApplicationContentToLocalServer method simply wasn’t going to cut the mustard for my purposes unless I wanted to either (a) require that the Timer Service account be added to the Local Administrators group on each server in the farm, or (b) require that an administrator manually execute an STSADM command or custom command line application to carry out the method call.  Neither of these were acceptable to me.

Method Deconstruction

Since I couldn’t use the ApplyApplicationContentToLocalServer method directly, I wanted to dissect it to the extent that I could in order to build my own process in a manner that replicated the method’s actions as closely as possible.  Performing the dissection (again via Reflector), I discovered that the method was basically iterating through each SPIisWebSite in each SPWebApplication within the SPWebService object being targeted.  As implied by its type name, each SPIisWebSite represents a web site within IIS – so each SPIisWebSite maps to a physical web site folder within the file system at C:\Inetpub\wwwroot\wss\VirtualDirectories (by default if IIS folders haven’t been redirected).

Once each of the web site folder paths is known, it isn’t hard to drill down a bit further to each layouts.sitemap file within the _app_bin folder for a given IIS web site.  With the fully qualified path to each layouts.sitemap file computed, it’s possible to carry out a programmatic XML merge with the new sitemap data from a layouts.sitemap.*.xml file that is deployed with a custom Feature or solution.  The ApplyApplicationContentToLocalServer method carries out such a merge through the private (and obfuscated) MergeAspSiteMapFiles method of the SPAspSiteMapFile internal type, but only after it has created a backup copy of the current layouts.sitemap file using the SPAspSiteMapFile.Copy method.

The Solution

With an understanding of the process that is carried out within the ApplyApplicationContentToLocalServer method, I proceeded to create my own class that effectively executed the same set of steps.  The result was the UpdateLayoutsSitemapTimerJob custom timer job definition that is part of my BlobCacheFarmFlush solution.  This class mimics the enumeration of SPWebApplication and SPIisWebSite objects, the backup of affected layouts.sitemap files, and the subsequent XML sitemap merge of the ApplyApplicationContentToLocalServer method.  The class is without external dependencies (beyond the SharePoint object model), and it is reusable in its current form.  Simply drop the class into a SharePoint project and call its DeployUpdateTimerJobs static method with the proper parameters – typically from the FeatureActivated method of a custom SPFeatureReceiver.  The class then takes care of provisioning a timer job instance that will update the layouts.sitemap navigational hierarchy for affected sites on each of the servers within the farm.

As an aside: while putting together the UpdateLayoutsSitemapTimerJob, there were times when I thought I had to be missing something.  On a handful of occasions, I found myself thinking, “Certainly there had to be a multi-server friendly version of the ApplyApplicationContentToLocalServer method.”  When I didn’t find one (after much searching), I had the good fortune of stumbling upon Vincent Rothwell’s “Configuring the breadcrumb for pages in _layouts” blog post.  Vincent’s post predates my own by a hefty two and a half years, but in it he describes a process that is very similar to the one I eventually ended up implementing in my custom timer job.  Seeing his post helped me realize I wasn’t losing my mind and that I was on the right track.  Thank you, Vincent.

Conclusion

I can sum up the contents of this post pretty simply: when developing application pages that entail sitemap updates, avoid using the ApplyApplicationContentToLocalServer method unless you’re (a) certain that your Feature will be installed into single server environments only, or (b) willing to direct those doing the installation and activation to carry out some follow-up administration on each WFE in the SharePoint farm.

Why does the ApplyApplicationContentToLocalServer method exist?  I did some thinking, and my guess is that it is leveraged primarily when service packs, hotfixes, and other additions are configured via the SharePoint Products and Technologies Configuration Wizard.  Anytime a SharePoint farm is updated with a patch or hotfix, the wizard is run on each server by a local administrator.

An examination of the LAYOUTS folder on one of my farm members provided some indirect support for this notion.  In my LAYOUTS folder, I found the layouts.sitemap.search.xml file, and it was dated 3/25/2008.  I believe (I’m not positive) that this file was deployed with the SharePoint Infrastructure Updates in the middle of 2008, and those updates introduced a number of new search admin pages for MOSS.  Since the contents of the layouts.sitemap.search.xml file include quite a few new search-related navigation nodes, my guess is that the ApplyApplicationContentToLocalServer method was leveraged to merge the navigation nodes for the new search pages when the configuration wizard was run.

In the meantime, if you happen to find a way to use this method in a multi-server deployment scenario that doesn’t involve the configuration wizard, I’d love to hear about it!  The caveat, of course, is that it has to be a best-practices approach – no security changes, no extra manual work/steps for farm administrators, etc.

Additional Reading and References

  1. MSDN: Caching In Office SharePoint 2007
  2. CodePlex: MOSS 2007 Farm-Wide BLOB Cache Flushing Solution
  3. Jan Tielens: Adding Breadcrumb Navigation To SharePoint Application Pages, The Easy Way
  4. MSDN: SPWebService.ApplyApplicationContentToLocalServer Method
  5. TechNet: Plan for administrative and service accounts (Office SharePoint Server)
  6. Red Gate Software: .NET Reflector
  7. CodePlex: UpdateLayoutsSitemapTimerJob class
  8. Vincent Rothwell: Configuring the breadcrumb for pages in _layouts