Simple Custom DropDown Search Refiner

Tuesday, April 29, 2014

0

Once an arcane Art whose brooding mysteries eluded the most learned and powerful gurus, custom SharePoint search is vastly more powerful and simple in SP 2013. Through display templates, we can render arbitrary HTML / JS for both search refiners and results. The possibilities are endless. Pie-chart refiners? Hands-free refining via neural matrix uplink? Refinement with actual pies?  Someone’s probably already blogged it.

Still, creating my first custom refiner was a daunting task—especially with the profusion of sophisticated examples.  Here I’ll go over how to make a very simple drop-down refiner that acts more like a plain ol' filter.

The goal is to let users filter on Approval Status (the ows__ModerationStatus field).
There are a bunch of statuses, but most users only care about “published” vs “non-published”. So let’s build a refiner with just  those options.

Active will filter for Approved status, and Inactive will be everything else (Draft, Pending, Scheduled, etc).  Note that the value in Moderation Status is actually an integer which corresponds to these statuses.

Assumptions

Though ows__ModerationStatus field is an OOB field, it is not by default a search Managed Property. I’m assuming in this example that it’s already been mapped to a Managed Property called ModerationStatus.

I'm also assuming Search is configured and working properly, and you have a page with Search Results and Search Refinement webparts.

Custom Display Template

Let's start simple by building a refiner with a static dropdown box.  The OOB refiner display templates live in the Master Page Gallery, under /_catalogs/masterpage/Display Templates/Filters


Note that there is a HTML and JS file for each template.  The JS is a SharePoint generated file.  We will only ever work with the HTML.  Download a copy of Filter_Default.html.

Crack that open in a text editor and remove everything between the body tags.  You will end up with just this:

<html xmlns:mso="urn:schemas-microsoft-com:office:office"
      xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Refinement Item</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>

</body>
</html>

Update the title.  Inside the body, we'll add a couple DIVs and a static drop down box:

<html xmlns:mso="urn:schemas-microsoft-com:office:office"
      xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
   <div id="ModerationStatusRefinement">
      <div id="Container">
         <select id="statusDdl">
            <option value="0">ALL</option>
            <option value="1">Active</option>
            <option value="2">Inactive</option>
         </select>

      </div>
   </div>

</body>
</html>

Test Drive

Now, let's see it in action!  Save the file as Filter_ModerationStatus.html, and upload it to the Master Page gallery.

Edit your Refinement webpart, and hit Choose Refiners.  Add the ModerationStatus managed property as a refiner, and change its Display Template.  Note that this was the title we set in the HTML.


Hit OK and save the page.  If you see a drop down in the refiners panel, we're in business.  Sort of.  Of course, it doesn't do anything yet...

Adding Refinement

To make the refiner, you know... refine stuff, we have to update the active refiners when the dropdown value changes.  This is done through javascript by invoking methods on the refiner control.

First, wire up the dropdown box's onchange event.  We will create an applyRefiner(val, refinerCtrl) method to apply the selected refiner.  The javascript code for this method needs to go into an external JS file.

Note: this file can not have the same name as the display template.  That is reserved for the SharePoint generated JS file.  I named the example Filter_ModerationStatus_functions.js.

Add a script block beneath the body to reference this external file.

<html xmlns:mso="urn:schemas-microsoft-com:office:office"
      xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
   <script>
      $includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
   </script>

   <div id="ModerationStatusRefinement">
     <div id="Container">
       <select id="statusDdl"
             onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
         <option value="0">ALL</option>
         <option value="1">Active</option>
         <option value="2">Inactive</option>
       </select>
     </div>
   </div> 
</body>
</html>

Create Filter_ModerationStatus_functions.js, and add the code below.  Recall that val is the option value that's just been selected.

First, we clear any active ModerationStatus refiners by setting it to NULL.  Then, for Active, add a refinement for ModerationStatus = 0, which stands for Approved.  Inactive actually corresponds to a set of ModerationStatuses, so we use addRefinementFiltersWithOp with an OR operation to tell SharePoint to match on any of the statuses in the array.

function applyRefiner(val, refinementCtrl) {
   // Clear refiners
   refinementCtrl.updateRefiners( { 'ModerationStatus' : null } );

   if (val == 1) // Active --> Approved
      refinementCtrl.addRefinementFilter('ModerationStatus', '0');
   else if (val == 2) // Inactive --> Rejected, Pending, Draft, Scheduled
      refinementCtrl.addRefinementFiltersWithOp(
          { 'ModerationStatus' : ['1','2','3','4'] },
          'or'
      );
}

There's scant documentation on MSDN (and by scant I mean none) for the refiner control, but check out Elio Struyf's blog post for other refinement methods and more examples.

Upload both the HTML and JS files, and reload the search result page.  Now, when you change the dropdown, the search results should be getting refined!

Finishing Touches

You probably noticed the Status selection doesn't stay selected and gets reset (though the search results are being refined).  This is because we didn't take into account which option is already selected when rendering the dropdown.

To fix this, we have to add some javascript to the display template.  This is different from the javascript in the external file.  Javascript inside the display template controls what gets rendered.  The script itself is not emitted on to the page.  Therefore, javascript that's invoked by the controls on the page must live in an external file.

Within the template, all javascript statements must be enclosed in special tags: <!--#_   _#-->.  Javascript variables can also be emitted on to the page when wrapped in these tags: _#=   =#_.

We'll make a simple method to render the dropdown option based on it's selected state:

function addRefiner(val, text, selected) {
   if (selected) {
      <option selected="selected" value="val">text</option>
   } else {
      <option value="val">text</option>
   }
}

The javascript lines and variables must be wrapped before being added to the template:

<!--#_
   function addRefiner(val, text, selected) {
      if (selected) {
_#-->
         <option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_
      } else {
_#-->
         <option value="_#= val =#_">_#= text =#_</option>
<!--#_
      }
   }
_#-->

Finally, we replace the hard-coded options in the dropdown with calls to addRefiner.  For each option, we ask the refinement control if the corresponding filters are active by calling hasAllRefinementFilters.

For completion, we can also add a title at the top.

<html xmlns:mso="urn:schemas-microsoft-com:office:office"
      xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>

   <script>
      $includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
   </script>

   <div id="ModerationStatusRefinement">
       <div id="Container">
         <div style="font-size:13pt; margin-bottom: 10px">
            _#= Srch.Refinement.getRefinementTitle(ctx.RefinementControl) =#_
         </div>

         <select id="statusDdl"
               onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
<!--#_
addRefiner(0, 'All', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['']));
addRefiner(1, 'Active', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['0']));
addRefiner(2, 'Inactive', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['1', '2','3','4']));
_#-->

          </select>
       </div>
<!--#_
   function addRefiner(val, text, selected) {
      if (selected) {
_#-->
         <option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_ 
      } else {
_#--> 
         <option value="_#= val =#_">_#= text =#_</option>
<!--#_
      }
   }
_#-->

   </div> 
</body>
</html>


Setting Managed Metadata Column Default Value

Monday, April 28, 2014

2

In this post I'll go over programmatically setting the default value on a taxonomy column.  This turns out to be a bit trickier than one would expect.

TaxonomyField.DefaultValue expects a string in the format <WssId>;#<TermLabel>|<TermGuid>. TermLabel and TermGuild are easy to find, but what about WssId?

If you already know about the TaxonomyHiddenList and WssId, go ahead and skip to the next paragraph. Every site that works with Managed Metadata has a hidden list called, aptly, TaxonomyHiddenList. This serves as a local cache, and all term references on the site actually point to items in this hidden list. WssID refers to a term’s ID in this list. It is initially empty, and terms are added the first time they’re actually used on the site.

In most situations, we do not have to worry about the WssID. When adding a list item with a taxonomy field, setting WssID to -1 tells SharePoint to resolve the WssID if the term exists in TaxonomyHiddenList, or otherwise add it.

Unfortunately, this does not work when setting a field’s default value. The -1 WssId will simply be saved as the default value. Note that setting DefaultValue with an invalid WssID will not cause an exception, and the default value still shows up in the column settings UI. However, when you actually create a new list item, the default value will not be set.

We can try to retrieve WssId from TaxonomyHiddenList, but if the term hasn’t been used on the site before, there will be no WssID. SharePoint does not expose any method to explicitly add a term to the hidden list.

The solution is to create a dummy list item, and call SetFieldValue with the term. You do NOT have to actually save the dummy item by calling Update() on it. Simply calling SetFieldValue triggers a SharePoint internal “TouchAllTaxonomyColumns” method on the field, which populates the hidden list if needed.

Finally, GetValidatedString returns the complete string that DefaultValue expects.

$web = Get-SPWeb -Identity http://sharepointificate.com

# Get Taxonomy column

$list = $web.Lists["Posts"]
$taxField = $list.Fields["Label"] -as 
              [Microsoft.SharePoint.Taxonomy.TaxonomyField];

# Get term

$termStoreId = taxField.SspId;
$termSession = Get-SPTaxonomySession -Site $list.ParentWeb.Site;
$termStore =  $termSession.TermStores[$termStoreId];
$termSet = $termStore.GetTermSet( $taxField.TermSetId );
$terms = $termSet.GetTerms( "SharePoint", $false );
$term = $terms[0];

# Use the term in a dummy item to ensure that is added to the site's
# HiddenTaxonomyList and has a WssId. Do NOT call Update() on
# the item-- we don't actually want to add it.  Simply calling SetFieldValue
# will force SP to initialize the hidden list entry.

$newItem = $list.Items.Add();
$taxField.SetFieldValue( $newItem, $term );

# Retrieve term string from dummy item, 
# in form of <wssid>;#<termlabel>|<termguid>

$value = $taxField.GetValidatedString( $newItem[$taxField.Id] );

# Set default value and save

$taxField.DefaultValue = $value;
$taxField.Update();