Content Search Webpart Dynamic Filtering

Wednesday, August 13, 2014

13

The Content Search webpart (CSWP) introduced in SharePoint 2013 is an extremely powerful and flexible tool for displaying dynamic content.  It can surface any content from the Search index, and display templates allow us complete control over how the results are rendered.

Each CSWP is associated with a search query which drives its content.  This post will focus on scenarios in which the search query itself needs to be dynamic.  The techniques in this post can be applied to the Search Results webpart as well.

There are several approaches to making the webpart's query dynamic:
  1. Use built-in query variable tokens
  2. Extend the CSWP with server side code
  3. Dynamically update the query with Javascript

Query Variables

The CSWP supports a set of predefined variables that can be used to build dynamic queries.  The variable tokens are replaced with actual values when the CSWP loads.  There's a plethora of variables to choose from--some of the more useful ones are Page Field, Site Property, User, URL Token, and Query String.

Say we have a page called Orders.aspx, with a CSWP that displays Orders.  We want to limit its results to orders for a specific Product, to be specified in the query string.  For instance, Orders.aspx?ProductID=3 should display only Orders with a ProductID of 3.

Assuming we have an Orders content type and a search managed property ProductID, this can be achieved by setting the CSWP's query to:

  contentType:Orders ProductID:{QueryString.ProductID}

Refer to this Technet article for the complete list of variable tokens.

Extending the CSWP

While query variables can handle many common scenarios, sometimes we need a little custom logic, or a field not available in the OOB variables.  What if we want to plug the current year into the query?  Or a value from another SP list?  Or from a user control on the page?

One solution is to extend the CSWP with server side code to intercept and update the query.  I am not going to go over the implementation details as there are already several good blog posts on it.

This article is a great tutorial on extending the CSWP to inject SharePoint Variation Labels into the query.

Dynamically Updating the CSWP with Javascript

If we can't use server side code, or need to dynamically change the results after the page loads, it is also possible to update the query using Javascript.  Note that some of the methods used in this approach are undocumented.  Use at your own risk!

Consider this scenario:  we have a CSWP that displays a list of Orders.  We want to query for orders related to a specific Product, determined by a dropdown list on the page.  When the dropdown selection is changed, the CSWP's results should dynamically update (without reloading the page).

First, add a CSWP to the page and set the default query that should run when the page loads.  In this case, I filter for Orders with Product ID 1.


Next, we will create a drop down box with Product options, and Javascript to update the CSWP on change.  I assume that jQuery is available on the page.  For prototyping purposes, we can just drop the markup in a Content Editor webpart.

To create the dropdown:

<select id="Products">
   <option value="1" selected="selected">Plushies</option>
   <option value="2">Computers</option>
   <option value="3">Unspeakable Items</option>
</select>

And now the good stuff... we attach a change handler to kick off some code when the dropdown is updated.  In the handler, we iterate through all the active data providers, looking for the ones associated with CSWPs.  In this example there would only be one.

Then, update the data provider's query to filter on the selected Product ID.  Finally, we call issueQuery() to execute.  The data provider's associated CSWP should automatically pick up the new results and update its display.

<script type="text/javascript">
  $(document).ready(function () {
    // Attach change event handler to Products dropdown
    $('#Products').change(function () {

      // Get selected product ID
      var prd = $(this).val();

      // Loop through Query Groups
      var groups = Srch.ScriptApplicationManager.get_current().queryGroups;

      $.each(groups, function () {
        // Look for query groups associated with a CSWP
        if (this.displays != null && this.displays.length > 0) {
          if (this.displays[0] instanceof Srch.ContentBySearch) {
            // Update and execute query
            var newQuery = 'contentType:Orders productID:' + prd;

            this.dataProvider.set_queryTemplate(newQuery);
            this.dataProvider.issueQuery();
          }
        }
      });
    });
  });
<script type="text/javascript">

 

Addendum: Targeting a Specific CSWP


I've been asked how to change the query of a specific CSWP, if there are more than one on the page?  In the example above where we search through the query groups, it's impossible to tell which is which.

We can leverage the $getClientControl function for this.  This approach requires using a custom display template.  $getClientControl takes as input any HTML element within a CSWP, and returns the containing CSWP object.

Now, we still have no way to grab an HTML element from inside that specific CSWP.  But if we're using a custom display template, we can tag an element with a custom ID or css class.

For example, give a DIV in the display template a special ID:

<div id="MyCSWP">Some content</div>

Now we can grab the CSWP object with $getClientControl( $("#MyCSWP")[0] ):

var ctrl = $getClientControl( $("#MyCSWP")[0] );

ctrl.get_dataProvider().set_queryTemplate(newQuery);
ctrl.get_dataProvider().issueQuery();

Note that $getClientControl expects an HTML element, not a jQuery object. So, passing $("#MyCSWP") will not work.  It has to be $("#MyCSWP")[0].  Alternatively, use document.getElementById.


MCSD SharePoint 2013

Tuesday, May 13, 2014

0

Every year my review forms are dominated by a gapping void that's the "certifications" checkbox.  Time in the IT world flows differently--many moons passed, mountains were worn to pebbles, and those blown to dust.  As MCTS SP07 became MCTS SP10, my manager's hopeful look turned quizzical, then accusatory. What man would so wantonly forsake his yearly goals?  Oathbreaker, they call me.

Now that MCTS has been replaced by MCSD altogether, the time has come to get off my butt. This is the year I shall be certified.  Better years late than never, right?

I will update this post with tips and resources for the four MCSD SharePoint exams as I go through them.


Taxonomy Columns & SharePoint 2013 List REST API

Monday, May 12, 2014

14

I ran into a strange issue today when using the SharePoint 2013 REST API for Lists with Managed Metadata columns.  For a multi-select taxonomy column, the List service returns Label text as expected.  But if you change that same column to single-select, suddenly an ID is returned in the Label field!

For instance, consider an Orders list with a Product multi-select taxonomy field.  I run the following REST query to dump all the list items:

  http://sharepointificate/_api/web/Lists/GetByTitle('Orders')/Items

All is good and the Product label text is returned:

Now if I just change Product to single-select and run that same query, the friendly label is gone!  This has to be a bug.  Yes, YOU, Microsoft.  I'm talking to you!


Luckily, there is a workaround. The Taxonomy label is correctly returned when we use a CAML query to retrieve the list items.  Erik C. Jordan's answer on this MSDN thread reveals a way to use CAML against the REST API.  This guy must be some kind of SP savant, because I can find absolutely no other documentation on this method—but it works

/GetByTitle('Orders')/GetItems(query=@v1)?@v1={'ViewXml':'<View><Query></Query></View>'}

A couple things to note:

  1. GetItems is a POST operation.  The query above will not work when run from your browser's address bar, which performs a GET.  You need a tool like Fiddler to send a POST message to the REST endpoint.  More likely, you'll be doing a jQuery ajax call like the example below.
  2. An X-RequestDigest message header is required for POST operations against the REST API.  Its value differs between environments and apps.  If this header is missing or incorrect, the server will return a 403 Forbidden error.  When executing within a SharePoint-hosted or on-premise context, it should be available on the page itself, and you can grab it with $("#__REQUESTDIGEST").val().  For other contexts, you'll have to retrieve the value like this MSDN article describes.

The complete Ajax call:

$.ajax({
    url: "http://sharepointificate/_api/web/Lists/GetByTitle('Orders')/GetItems(query=@v1)?@v1={'ViewXml':'<View><Query></Query></View>'}",

    type: "POST",

    headers: {
        "Accept": "application/json;odata=verbose",
        "X-RequestDigest": $("#__REQUESTDIGEST").val()
    },

    success: function (data, textStatus, jqXHR) {
        console.log(data.d.results[0].Product.Label);
    },

    error: function (jqXHR, textStatus, errorThrown) {
        console.dir(jqXHR);
    }
});


Powershell Variable as Property Name

Thursday, May 08, 2014

0

I discovered something beautiful and wonderous today. It's possible in Powershell to use a variable directly as a property name!  For instance:

Windows PowerShell
Copyright (C) 2012 Microsoft Corporation. All rights reserved.

PS C:\> $d = Get-Date
PS C:\>
PS C:\> $prop = "DayOfWeek"
PS C:\>
PS C:\> $d.$prop
Thursday
PS C:\>

Mind blown.


JavaScript Promise vs Deferred

Monday, May 05, 2014

1

jQuery Promises have been around for a while, but I've always slinked away tail between my legs at the mere mention of them. Running might have attracted the beast's attention. Alas, my new project uses Promises religiously. They are actually nifty constructs once you wrap your head around them.  I had to stew and mull for about a week before I could grasp the difference Promises and Deferred (I know, what a scrub)

This post is for anyone specifically wondering about Promises vs Deferred.  I'm not going to talk about Promises in general--there are already a ton of good articles on that topic.

There is really nothing special about Promises and Deferred--they are both plain ol' JavaScript objects.  Every Deferred object has a corresponding Promise object.  The Promise is essentially a copy of the Deferred with limited functionality.

When you are running an async operation, you create a Deferred.  You can then use it to attach handlers, and more importantly, Resolve or Reject when the operation completes.  In the meantime, you get the Deferred's corresponding Promise and return that to your caller.


As the caller, you get back a Promise object with which you can also attach handlers. The difference is that you can NOT trigger those handlers by calling Resolve or Reject.  This makes sense, because the caller wouldn't / shouldn't know about the async operation and when it completes.
 


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

3

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();