Protiviti / SharePoint Blog

SharePoint Blog

August 08
SharePoint 2013 Search Part 2

In the first part of this series, we explored how SharePoint 2013 uses the URL hash value to perform asynchronous queries and display results.  In this post, we will look at some code that can help you build custom components that integrate with this process. 

First, we will learn how to handle the hash change event in the browser.  Then we will learn how to parse the hash value and update it.  Finally, we will briefly touch on some server side code that can be used to make a solution more flexible.
Disclaimer: The code examples provided in this post are “as is” and may or may not work for your particular situation.  Always test thoroughly and use this as a guide, not a solution!
Handling the Hash Change Event
You may be asking yourself right now, “Why bother handling the hash change event?  I just want to manipulate the query or change the hash.  Why would I need to handle the situation when the hash changes when I’m doing the changing?”  A fair question, but remember that the out-of-the-box search web parts might not be aware of what you are doing.  They may overwrite anything you put in the hash when someone uses them.  Therefore, you should take care to handle when they change the hash and ensure your modifications remain in place.
Modern browsers support the “onhashchange” event, but you can still handle hash changes with outdated browsers using an interval:

if ("onhashchange" in window) {
   
if (window.attachEvent) {
        window.attachEvent(
"onhashchange", HandlerFunction);
    }
else {
        window.addEventListener(
"hashchange", HandlerFunction, false);
    }
}
else {
   
var storedHash = window.location.hash;
    window.setInterval(
function () {
       
if (window.location.hash != storedHash) {
            storedHash = window.location.hash;
            HandlerFunction();
        }
    }, 100);
}


Note that in the above, some browsers (namely Internet Explorer) use window.attachEvent while others (Chrome and Firefox) require window.addEventListener.  Also, it is not a typo in the window.addEventListener call.  You will need to use hashchange as opposed to onhashchange as the event name. 
 
Parsing the Hash
Parsing the URL hash can get complicated, especially when you need to support both single and multiple query groups. Before you can worry about that however, you have to actually get the hash value from the URL.  Now you may be tempted to jump right in and use the window.location.hash value:
var hash = window.location.hash;

This, however, will not work because some browsers (Firefox) have decided to handle this property differently by automatically decoding the value upon retrieval.  That’s a problem here because you will need to split the string up on characters that may show up in the decoded hash.
Instead, you should use the window.location.href property and then find the hash in a way similar to:
var hash = "";
var
fullUrl = window.location.href;
var i = fullUrl.indexOf("#");
if (i > -1)
    hash = fullUrl.substring(i);
This will give you the URL hash value in a still decoded form in all major browsers (at least at the time of this writing).
Now, you will need to determine if the hash contains a single or multiple query groups. Remember from part one that when there is only one query group and no refiners, the hash is formatted like this:
j1.png 
And where there are multiple, it is formatted using JSON like this:
j2.png
 
Detecting this difference isn’t that difficult.  You can easily check if the hash contains a “#k=” string.
For the sake of argument however, let’s assume there are multiple query groups active on the page.  The URL hash might look something like this:
#Default=%7B%22k%22%3A%22test%22%7D#2e86face-1d20-4756-8921-4deaeec26cdb=%7B%22k%22%3A%22%22%7D
Decoded, this looks like:
#Default={"k":"test"}#2e86face-1d20-4756-8921-4deaeec26cdb={"k":""}
Notice that there are two hash values in the string, one for each query group.  In order to parse this into an object, you will need to handle each query group separately.  Simply split the string on the hash character and then loop through the array:
var queryGroups = hash.split("#");
for (var i = 1; i < queryGroups.length; i++) {
      
}
Within the loop, you can use the JSON.parse() method to parse the value into an object.  Note that the queryGroups array holds full “query group = value” strings and you will only pass the value into the JSON.parse() method.  Again, use basic string manipulation to separate the group name from the value:
var keyValue = queryGroups[i].split("=", 2);
var key = keyValue[0];
var encodedValue = keyValue[1];
Finally, use the JSON.parse() method to get an object.  This method takes whatever you give it and tries to make an object out of it.  If the string you pass in does not represent a JSON object, it will throw an exception.  The encoded value as it appears in the URL will not work, so pass it through a decoding method beforehand:
var jsonStringValue = decodeURIComponent(encodedValue);
var queryObject = JSON.parse(jsonStringValue);
The queryObject variable will now contains members that represent what was in the JSON string.  You will probably want to store each queryObject instance in an array for quick retrieval.  All together, the above process might look something like the following:
var hash = GetHash();
var hashQuery = [];
if (IsMultipleQueryGroups() == true) {
    var queryGroups = hash.split("#");
    for (var i = 1; i < queryGroups.length; i++) {
        var keyValue = queryGroups[i].split("=", 2);
        var key = keyValue[0];
        var encodedValue = keyValue[1];

        var jsonStringValue = decodeURIComponent(encodedValue);
        var queryObject = JSON.parse(jsonStringValue);  

        hashQuery[key] = queryObject;
    }
}
else {
    hashQuery[
"Default"] = hash;
}
 
Updating the Hash
In one form or another, we now have something that represents the current hash value.  We either have a simple string (one query group, no refiners) or we have JSON objects (multiple query groups, etc.).  Updating the query is as simple as modifying what we have and placing it back into the URL. 
There isn’t much to say if we are dealing with a simple string.  You just have to use basic string manipulation and reset the value of the window.location.hash property.  If we have JSON objects, each object will contain a property that represents what was in the hash value when it was retrieved.  In this case, we can use the “k” property to access and update the query (if you are dealing with refinements, you can use the “r” property).  Getting the object back into a string that can be placed in the window.location.hash property requires use of the JSON.stringify() method.  Remember that there may be multiple query groups and each one needs to be placed back into the hash.  You will need to loop through the objects and build the new hash incrementally.  Since we built our hashQuery object as a keyed array where each key is the name of a query group, we can easily loop through the groups and get the JSON objects back out like this:
for (var group in hashQuery) {
   
var queryObject = hashQuery[group];
   
}
You can then build each part of the hash by serializing the object and then encoding it as follows:
var json = JSON.stringify(queryObject);
var encodedJson = encodeURIComponent(json);
var part = group + "=" + encodedJson;
You will want to store each part in an array and then set the new hash value by combining each item in the array with a ‘#’ character.  All together, the above process to get the updated hash value might look something like this:
function GetNewHash() {
    var
newHash = "";
    if (IsMultipleQueryGroups() == true) {
        
var groupParts = [];

        for (var group in hashQuery) {
            
var queryObject = hashQuery[group];

            var json = JSON.stringify(queryObject);
            var encodedJson = encodeURIComponent(json);
            var part = group + "=" + encodedJson;

            groupParts.push(part);
        }

        
if (groupParts.length > 0)
            newHash =
"#" + groupParts.join("#");
    }
else {
        newHash =
"#" + encodeURIComponent(hashQuery["Default"]);
    }

    return newHash;
}
Because we intend to modify the hash value and we are also handling the hash change event, it would be wise to set a global flag when we change the hash value so we do not end up running the hash change function multiple times.  This would also allow us to handle our changes and out-of-the-box changes differently. 
var internalUpdate = false;
 
function UpdateHash() {
    var
newHash = GetNewHash();
    var decodedCurrentHash = decodeURIComponent(window.location.hash);
    var decodedNewHash = decodeURIComponent(newHash);
    if (decodedCurrentHash != decodedNewHash) {
        internalUpdate =
true;
        window.location.hash = newHash;
    }
}

Note in the above that we compare the decoded hash strings.  This is again because Firefox decodes the window.location.hash property upon retrieval whereas all other browsers do not.
 
Server Side Code
Most of what you can do to effect the outcome of search queries will be done client side, but Microsoft does provide the ScriptApplicationManager class that you can use server side to gather information about the search setup on a particular page.  This includes the available query groups as well as the search box and display parts that are associated with each group.
In order to get an instance of this class, use the GetCurrent() method, passing a Page object.  For example, assuming you are getting details for your own control, you might use:
ScriptApplicationManager appManager = ScriptApplicationManager.GetCurrent(this.Page);
To get the available query groups, use the QueryGroups property.  That will return a dictionary of groups keyed by name and holding QueryGroup objects:
Dictionary<string, QueryGroup> queryGroups = appManager.QueryGroups;
Each QueryGroup object will return the SearchBoxScriptWebPart instances associated with the group via the SearchBoxes property.  Likewise, it will return a list of DisplayScriptWebPart instances via the Displays property.  You can use these properties to get information such as web part client ID’s that can be used from JavaScript to manipulate the search boxes and other display web parts on the page.
One possible real-world scenario is providing the ability to associate your custom web part with a particular query group.  You could use the ScriptApplicationManager class in an EditorPart to provide a configuration interface for your web part.  Another example is to pass the search box client IDs into the script and then use that to hide any custom search terms you add to the “k” value in the hash (because out-of-the-box search box web parts will display anything in the “k” value in the search text box).
 
Final Thoughts
Using a combination of server and client side code, you can achieve just about any result you would like with SharePoint 2013 search. Do you want to allow users to check taxonomy terms to add to the search query?  Go ahead!  Do you want to provide a list of common search phrases and keywords that perform specific queries?  Do it!  You are not limited to only allowing a user to type text in a search box to query for data with SharePoint 2013.

Quick Launch


© Protiviti 2019. All rights reserved.   |   Privacy Policy