To ‘this’ or not to ‘this’ with the Client Object model

Recently I developed a CustomAction on a list with some client script in the CommandAction. Nothing fancy or new.
I put a .js file in the _layouts folder and referenced the file by a CustomAction with Location ‘ScriptLink’ and ScriptSrc. Put some code in the file to get some data I needed and called executeQueryAsync with two delegates like this:

clientContext.executeQueryAsync(Function.createDelegate(this, this.OnSucceeded), Function.createDelegate(this, this.OnFailed));

Deployed as a Farm solution on the environment and everything went well so far.

After this, minds changed and it had to become a sandboxed solution. Ok, let’s do it.
1. Set the Sandboxed Solution property to True on the project
2. Removed the CustomAction with Location=’ScriptLink’
3. Put the code from the js file from the _layouts folder in the CommandUIHandler’s attribute CommandAction
4. Deployed the sandboxed solution

I always use FireFox when programming and all of a sudden I received the following error:
b is undefined

Client object model this FireFox

and things stopped working. While in the Farm solution the CustomAction worked as expected and the OnSucceeded and OnFailed were executed.

I put in some alert statements to see where the code broke and it seemed the OnSucceeded and the OnFailed methods weren’t executed at all.
By removing the ‘this’ keyword in the createDelegate everything went well again:

clientContext.executeQueryAsync(Function.createDelegate(this, OnSucceeded), Function.createDelegate(this, OnFailed));

This behaviour seems a bit strange to me. When putting the code in a separate js file and deploying it to the _layouts folder the ‘this’ keyword can be used, but putting the exact same code in the CommandAction of CommandUIHandler the OnSucceeded and OnFailed methods can’t be found anymore using the ‘this’ keyword.

By the way, in IE you will receive the following error:
‘b’ is null or not an object

Client object model this IE

Client side social dashboard with SharePoint 2010 and SPServices

The article I wrote for DIWUG SharePoint eMagazine 4 now also online here:
 
 
One of the new features of SharePoint 2010 is tagging. Users can tag several items within SharePoint, such as pages and list items. The tags a user used can be viewed in a tag cloud on their profile page.
 
In Central Administration the page ‘Manage Social Tags and Notes’ is available to manage the users’ social terms. Here social terms can be found and eventually deleted. To manage the users’ social terms a user has to have sufficient permissions to Central Administration.
 
Besides using the ‘Manage Social Tags and Notes’ page, the SocialDataService web service can be used to retrieve information about tagging. This web service exposes a lot of operations to get all kinds of information about social data in SharePoint 2010. To retrieve the list of operations http://Site/_vti_bin/SocialDataService.asmx can be called within a SharePoint site. When a specific operation is selected from the list the expected request xml is displayed and the response xml the web service returns.
 
The jQuery library SPServices abstracts SharePoint’s Web Services and makes them easier to use. A list of implemented web services within SPServices can be found at CodePlex: http://spservices.codeplex.com
 
The SPServices library can be used fully client side and is an ideal solution to accomplish the goal of this article: creating a client side dashboard of some social analytics within SharePoint 2010. Marc D. Anderson has developed the SPServices library and is very open to community requests for enhancements as well as to help with any bugs, questions, or issues. You can contact him through the Discussions on the CodePlex site at any time.

The social dashboard functionality

Imagine an intranet portal at a certain company. The intranet portal is setup with SharePoint 2010 and the board wants to promote the social aspects of SharePoint, especially tagging, to the employees of the company.
 
At every company a lot of things are said between employees during a visit to the coffee machine or water cooler. To help discover how employees really interact and how the organization functions as a whole social interaction within SharePoint is really helpful. The board wants to give the employees a voice and thereby uncover what they find most important.
 
Besides this, the use of tagging will expose hidden knowledge in the organization. When an employee decides to move on to another company SharePoint keeps this information from being lost.
 
The SharePoint search will also benefit by the use of tagging. Often tagged content will have a positive effect on the contents search result ranking.
 
Unfortunately, adaptation of this new feature is currently poor in the organization and the use of tagging has to be stimulated in a way. Since people always like to be rewarded for their effort, the board decided to give away a bottle of champagne every month to the user who tags the most content.
 
To give the employees some information on how they’re doing related to their colleagues, some statistics about tagging will be published on a page within SharePoint which is accessible to all employees. To make this page attractive, not only the most active users are displayed, but also the most used tags and the tags the current user used already.
 
Additional information is available to managers and members of the board to stimulate persons even more.
 
A tabular overview of the functionality based on the role of the user:
Functionality
 
Employee
 
Manager
 
Board member
 
Tags of current user
 
V
 
V
 
V
 
Most used tags
 
V
 
V
 
V
 
Drill down to url
 
V
 
V
 
V
 
Top active users
 
V
 
V
 
V
 
Drill down to tag
 
X
 
V
 
V
 
Activity of employees in own department
 
X
 
V
 
V
 
Activity of employees in a selectable department
 
X
 
X
 
V
 
 
All the code displayed in the article is based on jQuery or html. With references to jQuery, the SPServices library and the jQuery template plugin all the code can be pasted in a Content Editor web part on a SharePoint page.
 

<script src="/jQueryLibrary/jquery-1.4.4.min.js" type="text/javascript"></script>
<script src=" /jQueryLibrary/jquery.SPServices-0.5.8.min.js" type="text/javascript"></script>
<script src=" /jQueryLibrary/jquery.tmpl.js" type="text/javascript"></script>

Tags of current user

To determine the tags a certain user used, one call to the SocialDataService web service with the help of SPServices will do. SPServices exposes an operation called ‘GetTagsOfUser’ which expects the parameter user account name. In this case the user account name is the account name of the currently logged in user.
 
The SPServices library exposes a function to get the account name of the current logged in user, ‘SPGetCurrentUser’. The code to get the account name of the current user:
 
var currentUserAccount = $().SPServices.SPGetCurrentUser({
            fieldName: "Name"
        });
The SPGetCurrentUser function does an AJAX call to grab /_layouts/userdisp.aspx?Force=True and ‘scrapes’ the values from the page based on the internal field name. The ‘Name’ field used in the code is the internal field name of the account name.
 
Since the fieldname ‘Name’ in the above code is the default option of the SPGetCurrentUser function, the call can be combined with the operation ‘GetTagsOfUser’.
$().SPServices({
    operation: "GetTagsOfUser",
    userAccountName: $().SPServices.SPGetCurrentUser(),
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("SocialTagDetail").each(function () {
            tagName = $("Term>Name", $(this)).text();
            tagsofuser.push({ Tag: tagName, Count: 1 });
        });
        tagsofuser = uniqueTags(tagsofuser);
        SortByCount(tagsofuser);
        $("#currentUserTagsText").show();
        $("#currentUserTags").html("");
        $("#tagsofuserTemplate").tmpl(tagsofuser)
            .appendTo("#currentUserTags");
    }
});
The callback function gets the response of the web service. A part of the response is displayed here: 

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <GetTagsOfUserResponse xmlns="http://microsoft.com/webservices/SharePointPortalServer/SocialDataService">
      <GetTagsOfUserResult>
        <SocialTagDetail>
          <Url>http://sp2010/Lists/Tasks/AllItems.aspx</Url>
          <Owner>sp2010\mark</Owner>
          <LastModifiedTime>2010-12-19T13:28:09.437</LastModifiedTime>
          <Title>Tasks - All Tasks</Title>
          <Term>
            <Id>974a854f-31b4-431f-91cb-a6289f58c978</Id>
            <Name>I like it</Name>
          </Term>
          <IsPrivate>false</IsPrivate>
        </SocialTagDetail>
        Rest of the SocialTagDetails are intentionally left out�
      </GetTagsOfUserResult>
    </GetTagsOfUserResponse>
  </soap:Body>
</soap:Envelope>
The term name is stored in an array of objects. The Count is set to one, because this tag is default present one time. When the duplicates are removed in the uniqueTags method the Count of the Tag is adjusted if the tag is present more than once. Since this is a helper method it is not showed here. Afterwards the tags are sorted descending by the number stored in the variable Count. A jQuery template, tagsofuserTemplate, is used to show the results on the page. The template is rendered with the data and appended to a div with the id currentUserTags. The template uses the objects in the array: 
<div><script id="tagsofuserTemplate" type="text/x-jquery-tmpl">
<div> </div>
<div><div style="float:left;width:40%">
<div> </div>
<div>        ${Tag}
<div> </div>
<div></div>
<div> </div>
<div><div style="float:left;width:20%">
<div> </div>
<div>        ${Count}
<div> </div>
<div></div><br />
<div> </div>
<div></script>
<div>
The tags of the current user are displayed as show in the figure below:
 
Since detailed information of the tags is available at the users’ profile page, a link is displayed to get there. 

Most used tags

The web service doesn’t provide an operation to get the most used tags, but only an operation to get all the tags. Therefor all the tags terms have to be retrieved and afterwards counted and sorted to get the most used ones.

$().SPServices({
    operation: "GetAllTagTerms",
    debug: false,
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("SocialTermDetail").each(function () {
            termName = $("Term>Name", $(this)).text();
            termGuid = $("Term>Id", $(this)).text();
            counter = $("Count", $(this)).text();
            terms.push({ Term: termName, Count: counter, TermGuid: termGuid });
        });
        SortByCount(terms);
        terms.length = 5;
        $("#showterms").html("");
        $("#termTemplate").tmpl(terms)
            .appendTo("#showterms");
    }
});
In the above code the operation ‘GetAllTagTerms’ of the SPServices library is used. This operation gets all the tag terms. The name, id and the counter of each term are retrieved from the returned xml. The tag terms are sorted by the Count variable and the number of items returned is minimized to the top 5 most used tag terms. The result is displayed on the page by a jQuery template:
<div><div style="float:left;width:40%">
<div> </div>
<div>     <a href="#" id=${TermGuid}>
<div> </div>
<div>        ${Term}
<div> </div>
<div>    </a>
<div> </div>
<div></div>
<div> </div>
<div><div style="float:left;width:20%">
<div> </div>
<div>        ${Count}
<div> </div>
<div></div>
<div>
The most used tags are displayed as show in the figure below:
To drill down on a tag the method GetAllTagUrls with the parameter this.id, in this case the guid of the term selected, is called when a user selects one of the tags. 

    $().SPServices({
        operation: "GetAllTagUrls",
        termID: termId,
        debug: true,
        completefunc: function (xData, Status) {
            $(xData.responseXML).find("SocialUrlDetail").each(function () {
                url = $("Url", $(this)).text();
                countUrl = $("Count", $(this)).text();
                urlsofterm.push({ Url: url, Count: countUrl });
            });
            SortByCount(urlsofterm);
            $("#urlsoftermContainer").show();
            $("#showurlsofterm").html("");
            $("#urlTemplate").tmpl(urlsofterm)
                                .appendTo("#showurlsofterm");
        }
    });
The ‘GetAllTagUrls’ of the SPServices library is called with the termID as parameter. The result is displayed with another jQuery template:

Top active users

The SocialDataService web service doesn’t provide an operation to get the top active users either. To determine the top active users in the site collection first all the available users have to be retrieved.    The SPServices library allows a call to the operation ‘GetUserCollectionFromSite’, which the web service Users and Groups exposes.

$().SPServices({
    operation: "GetUserCollectionFromSite",
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("User").each(function () {
            userName = $(this).attr("LoginName");
            userNames.push({ UserName: userName });
        });
        GetTopTagsOfUsers(userNames);
    }
});
In the code above the request to the web service is made and the result, the login names of all the users, is stored in an array. 
With the array of login names the operation ‘GetTopTagsOfUsers’ is called to count the tags used by each user and format the result in a clear way.
 
To count the tags used for a single user the SocialDataService web service provides the ‘CountTagsOfUser’ operation. The operation is called with the account name of the user as parameter. The result is formatted in an array of objects to store the account name and the number of tags of the user. 

function CountTagsOfUser(useraccountname, f) {
    var tagCount = "";
    var tagsofuser = [];
    $().SPServices({
        operation: "CountTagsOfUser",
        userAccountName: useraccountname,
        debug: true,
        completefunc: function (xData, Status) {
            $(xData.responseXML).find("CountTagsOfUserResult").each(function () {
                tagCount = $(this).text();
                tagsofuser.push({ Count: tagCount, UserAccountName: useraccountname });
            });
            SortByCount(tagsofuser);
            if (typeof f == "function") f(tagsofuser);
            return tagsofuser;
        }
  });
}
A callback function is necessary to use the CountTagsForUser in the function GetTopTagsOfUsers and returns the result array when the counting of the tags of the specified user has finished.

function GetTopTagsOfUsers(usernames) {
    var result = [];
    $.each(usernames, function (key, value) {
        CountTagsOfUser(value["UserName"], function (items) {
            $.each(items, function (i, n) {
                result.push({ Count: n["Count"], UserAccountName: n["UserAccountName"] });
            });
           if (usernames.length == result.length) {
                SortByCount(result);
                result.length = 5;
                $("#showcounttagsofuser").html("");
                if (isCEO || isManager) {
                    $("#counttagsofuserTemplate").tmpl(result)
                    .appendTo("#showcounttagsofuser");
                } else {
                    $("#counttagsofuserTemplateForUser").tmpl(result)
                    .appendTo("#showcounttagsofuser");
                }
            }
        });
    });
}
The ‘GetTopTagsOfUser’ function retrieves for every user present in the site collection the number of tags and stores the result in an array of objects. Once this function completes, the number of usernames are equal to the tagcount result, the results are sorted descending to get the most active user on top and the number of results is limited to the top five users.
When showing the results on the screen the first difference is made between a ‘regular’ employee and a manager/board member.
For a ‘regular’ employee a different template is used for rendering, because they aren’t allowed to drill down on a user to see which tags these users did use.
The template for a ‘regular’ employee:

<script id="counttagsofuserTemplateForUser" type="text/x-jquery-tmpl">
<div style="float:left;width:40%">
        ${UserAccountName}
</div>
<div style="float:left;width:20%">
        ${Count}
</div>
</script>
The top active users part on the page for a ‘regular’ employee:
And for a manager/board member:

<script id="counttagsofuserTemplate" type="text/x-jquery-tmpl">
<div style="float:left;width:40%">
    <a href="#" id=${UserAccountName}>
        ${UserAccountName}
    </a>
</div>
<div style="float:left;width:20%">
        ${Count}
</div>
</script>
The top active users part on the page for a manager/ board member:
The GetTagsOfUser operation used to drill down on a user is the same method used by displaying the tags of the current user mentioned in the beginning of the article. 

What’s the difference between a ‘regular’ employee, a manager and a board member?

The differences are stored in the profile properties of the user profile.
 
The user profiles of all the people present in the company are set up with the Department field filled and/or the Manager field filled. The Department field of the board has to be filled with ‘CEO’ to know this user a not a regular manager, but a member of the board.
 
The following table lists some examples of user profiles, their properties and their roles:
UserName
 
Department
 
Manager
 
Role
 
Alex
 
ICT
 
Andrew
 
Employee
 
Andrew
 
ICT
 
 
 
Manager
 
Anita
 
Marketing
 
Mark
 
Employee
 
Chris
 
HR
 
Dave
 
Employee
 
Dave
 
HR
 
 
 
Manager
 
Jeff
 
Marketing
 
Mark
 
Employee
 
Mark
 
Marketing
 
 
 
Manager
 
Paul
 
CEO
 
 
 
Board member
 
 
To give an example on how to read the table: Jeff is an employee and member of the Marketing department. His manager is Mark. This makes Jeff a ‘regular’ employee. 
The role column lists the role of the user based on the previous columns.
 
The information about the department and manager of a person is stored in their User Profile. The SPServices library supports a lot of operations on the UserProfileService web service. The one needed here is ‘GetUserProfileByName’ to get the properties ‘Department’ and ‘Manager’. This method expects a parameter ‘AccountName’, which is the currently logged in user.

var CEOdepartment = "CEO";
$().SPServices({
    operation: "GetUserProfileByName",
    AccountName: currentUserAccount,
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("GetUserProfileByNameResult>PropertyData").each(function () {
            //check for CEO department
            if ($("Name", $(this)).text().toUpperCase() == "Department".toUpperCase()) {
                departmentOfCurrentUser = $("Values>ValueData>Value", $(this)).text();
                if (departmentOfCurrentUser.toUpperCase() == CEOdepartment.toUpperCase()) {
                    isCEO = true;
                    GetDepartmentsAndUsers();
                }
            }
            else if ($("Name", $(this)).text().toUpperCase() == "Manager".toUpperCase()) {
                managerName = $("Values>ValueData>Value", $(this)).text();
                if (managerName == "") {
                    if (!isCEO) {
                        isManager = true;
                        GetDepartmentsAndUsers();
                    }
                }
            }
        });
    }
});
The callback function gets the response of the web service. A part of the xml response looks like:

<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body>
    <GetUserProfileByNameResponse xmlns="http://microsoft.com/webservices/SharePointPortalServer/UserProfileService">
      <GetUserProfileByNameResult>
        <PropertyData>
          <IsPrivacyChanged>false</IsPrivacyChanged>
          <IsValueChanged>false</IsValueChanged>
          <Name>AccountName</Name>
          <Privacy>Public</Privacy>
          <Values>
            <ValueData>
              <Value xsi:type="xsd:string">sp2010\paul</Value>
            </ValueData>
          </Values>
        </PropertyData>
        <PropertyData>
          <IsPrivacyChanged>false</IsPrivacyChanged>
          <IsValueChanged>false</IsValueChanged>
          <Name>Department</Name>
          <Privacy>Public</Privacy>
          <Values>
            <ValueData>
              <Value xsi:type="xsd:string">CEO</Value>
            </ValueData>
          </Values>
        </PropertyData>
      </GetUserProfileByNameResult>
    </GetUserProfileByNameResponse>
  </soap:Body>
</soap:Envelope>
The current user in the response xml has the account name sp2010\paul and is a member of the CEO department.  
 
The find method of the responseXML gets to the PropertyData in the xml and tries to find the ‘Department’ and ‘Manager’ properties. Once found the value of the property is retrieved and in this case the variable ‘isCEO’ is set to true, because Paul is a member of the CEO department. The department of the current user is stored in the variable departmentOfCurrentUser and the operation ‘GetDepartmentsAndUsers’ is called for later use on other functionality.
 
The above code is used to determine the role of the current logged in user and the ‘Top active users’ part on the page will display itself differently based on this role. 
 
Worried about security? 
All information is gathered by calls to web services. Calls on the web services make everything security trimmed, so the approach is secure.

Activity of employees in own or a selectable department

Managers are allowed to view the activity of all the employees which are a member of their department. The account names of these employees are displayed in a dropdown box on the page.
 
Besides viewing the activity of a single user, the board wants to view the activity within a whole department. The departments are listed in a dropdown box cascading the users of the selected department.
 
These two functionalities can be implemented in a single operation. The method ‘GetDepartmentsAndUsers’ is called in the previous code. This operation is responsible to provide the values to set up the cascading functionality between departments and account names of the users. This method is only called when the current logged in user is a manager of a CEO.
 
To retrieve all the departments and account names the Search web service is called. One call is made to the web service to get the departments and the account names by formatting the query to return these properties in the result. The SQL syntax is used and the scope is set to the People scope.

var queryTextSQL = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>"
queryTextSQL += "<Query>"
queryTextSQL += "<Context>"
queryTextSQL += "<QueryText language='en-US' type='MSSQLFT'>"
if (isCEO) {
    queryTextSQL += "SELECT Title, Rank, Size, Description, Write, Path, AccountName, Department FROM scope() WHERE ( (\"SCOPE\" = 'People') ) ORDER BY \"Rank\" DESC"
} else {
    queryTextSQL += "SELECT Title, Rank, Size, Description, Write, Path, AccountName, Department FROM scope() WHERE ( (\"SCOPE\" = 'People') ) AND (CONTAINS (Department,'" + departmentOfCurrentUser + "')) ORDER BY \"Rank\" DESC"
}
queryTextSQL += "</QueryText>"
queryTextSQL += "</Context>"
queryTextSQL += "</Query>"
queryTextSQL += "</QueryPacket>";
$().ready(function () {
    var resultText = "";
    $().SPServices({
        operation: "QueryEx",
        queryXml: queryTextSQL,
        completefunc: function (xData, Status) {
            $(xData.responseXML).find("RelevantResults").each(function (i) {
                accountName = $("ACCOUNTNAME", $(this)).text();
                departments[i] = $("DEPARTMENT", $(this)).text();
                users.push({ AccountName: accountName, Department: departments[i] });
            });
            if (isCEO) {
                $('#departmentContainer').show();
                fillDepartmentsDropdown('#departmentSelection', $.unique(departments), "Select department");
            }
            if (isCEO || isManager) {
                $('#userContainer').show();
                fillUsersDropdown('#userSelection', users, "Select user");
            }
        }
    });
});
Dependent if the user is a manager or board member different queries are fired. When a manager only the users which are member of the department, set earlier in the variable ‘departmentOfCurrentUser’, are retrieved.
 
When the response is retrieved the departments are stored in an array of strings and the users are stored in an array of objects. The object contains the account name of the user and the department which the user is a member of. The department of the user is needed to accomplish the cascading dropdown between departments and account names. The department dropdown is only filled and showed when the current logged in user is member of the CEO department. If the user is a manager of another department only the users of that department are shown in the users’ dropdown box.
 
There are two different methods to fill the dropdown boxes for departments and users, because the users are stored in an array of objects, only the account name is used to fill the box: 

function fillUsersDropdown(dropdownName, arrayToUse, text) {
    $(dropdownName).html("");
    $.each(arrayToUse,
        function (key, value) {
            $(dropdownName).append('<option value="' + value["AccountName"] + '">' + value["AccountName"] + '</option>');
        });
    $(dropdownName).prepend("<option value='0' selected='true'>" + text + "</option>");
    $(dropdownName).find("option:first")[0].selected = true;
}
The id’s departmentContainer and userContainer used in the code contain the markup:

    <div style="float: left; width: 100%; display: none;" id="departmentContainer">
        <div style="float: left; width: 15%">
            Select department</div>
        <select id="departmentSelection" style="float: left; width: 15%">
        </select>
    </div>
    <br />
    <div id="userContainer" style="float: left; width: 100%; display: none;">
        <div style="float: left; width: 15%">
            Select user</div>
        <select id="userSelection" style="float: left; width: 15%">
        </select>
        <div style="clear: both; float: left; width: 25%">
            <a href="#" onclick="GetTagsOfSelection(userSelection.value, departmentSelection.value);return false">
                Show tags of selection</a></div>
        <h4 style="clear: both; float: left; display: none;" id="showtagsofuserText">
            Tags of selection:</h4>
        <div id="showtagsofuser" style="clear: both;">
        </div>
    </div>
The page now looks like this when the current logged in user is a board member:
And a manager:
The cascading functionality is now easy: 

$('select').change(function (e) {
    if (this.id == "departmentSelection") {
        $('#userSelection option').each(function (i, option) {
            $(option).remove();
        });
        j = 0;
        childArray.length = 0;
        if (this[this.selectedIndex].value != 0) {
            departmentSelected = this[this.selectedIndex].text;
            //filter the users array on the selected department
            filteredUsers = $.grep(users, function (filter) {
                return filter["Department"] == departmentSelected;
            });
            fillUsersDropdown('#userSelection', filteredUsers, "Select user");
        }
        else {
            fillUsersDropdown('#userSelection', users, "Select user");
        }
    }
});
The code first empties the items in the users’ dropdown box and fills it again based on the selected department.
 
The picture below shows the cascading functionality between departments and users:
After making a selection of a department and/or a user ‘Show tags of selection’ can be selected. Selecting this fires the operation ‘GetTagsOfSelection()’ with the values of the selected department and/or user as parameters. 

function GetTagsOfSelection(userSelection, departmentSelection) {
    var result = [];
    if (userSelection != 0) {
        GetTagsOfUser(userSelection, true, true);
    } else if (departmentSelection != 0) {
        $.each(filteredUsers, function (key, value) {
            GetTagsOfUser(value["AccountName"], false, true, false, function (items) {
                $.each(items, function (i, n) {
                    result.push({ Tag: n["Tag"], Count: 1 });
                });
                result = uniqueTags(result);
                SortByCount(result);
                $("#showtagsofuserText").show();
                $("#showtagsofuser").html("");
                $("#tagsofuserTemplate").tmpl(result)
            .appendTo("#showtagsofuser");
                //}
            });
        });
    }
}
When a single user is selected the operation GetTagsOfUser() is used again. When a single department is selected the tags of all the users’ member of the selected department are retrieved, counted and sorted descending. The jQuery template is rendered with the results.
 
The page now looks like this when the current logged in user is a board member:
And a manager:

The result

Grabbing all the functionality together the social dashboards look like:
 
For an employee:
For a manager:
For a board member:
This month the board member, Paul, wins the bottle of champagne, because he tagged the most content. Since this isn’t the wanted outcome, employees have to win the bottle of course; Paul throws in two bottles of champagne for the winner next month.
 
This company just started to use the social aspects of SharePoint. The number of tags used isn’t very high yet. The search results ranking within SharePoint will not benefit from this tagging; neither the hidden knowledge in the organization is stored in SharePoint yet. When the stimulation of the use of tagging continues, employees will embed the use of tagging in their daily work and it will not take long to see the positive effects of it.
 

Summary

The SocialDataService web service is a good starting point on retrieving and displaying social information like tagging on a page. To use the service fully client side the library SPServices is an excellent solution.
 
To display information as shown in this article the SocialDataService web service does not provide all the necessary information and other web services have to be used. Other web services used here are the Users and Groups, the UserProfile and the Search web services. With the use of all these services and additional jQuery the goal of creating a client side dashboard of some social analytics is accomplished.
 
Performance wise the solutions are not always the most optimal. For example when the most active users are gathered. First it grabs all the users from the site collection, counts the tags for all of these users and then it is limited to show the top 5 of highest tag counts. It’s not the web service not being efficient; it’s the lack of the availability of a function to get the highest tags counts without the need to make the count on a specific user.

Bind (SP)Gridview field to generic list of strings

Probably this is my lack of knowledge in the (sp)gridview area, but I want to share it anyway.

I realized I always bound gridviews to custom objects only. Recently I wanted to bound a generic list of strings (List<string>) to a field in a gridview and didn’t know how to accomplish this. After trying a few options and some searching I found the answer:
An exclamation mark: !

Example:

BoundField field = new BoundField();
field.DataField = "!";

Never thought of this one myself.

Simple Provider Consumer Visual Webparts

Because of a question on http://sharepoint.stackexchange.com/questions/14325/created-connectable-webparts-but-the-connections-menu-item-is-not-showing I decided to create a provider consumer webpart in his most simple form.   

Create an empty SharePoint project in Visual studio.
Add two Visual WebParts:   

  1. ProviderVisualWebpart
  2. ConsumerVisualWebpart

   

In this most simple form of a provider consumer webpart only one string is passed from the provider to the consumer and it displayed in a Label control on the consumer webpart.   

Code in ProviderVisualWebpart.cs:   


public class ProviderVisualWebPart : System.Web.UI.WebControls.WebParts.WebPart, ITransformableFilterValues
{
    private const string _ascxPath = @"~/_CONTROLTEMPLATES/ConsumerProviderVWP/ProviderVisualWebPart/ProviderVisualWebPartUserControl.ascx";
 

    protected override void CreateChildControls()
    {
        Control control = Page.LoadControl(_ascxPath);
        Controls.Add(control);
    }

    public string ParameterName
    {
        get
        {
            return "Letter";
        }
    }

    public ReadOnlyCollection<string> ParameterValues
    {
        get
        {
            List<string> values = this.GetValues();
            return new ReadOnlyCollection<string>(values);
        }
    }

    private List<string> GetValues()
    {
        List<string> valueList = new List<string>();
        valueList.Add("A");
        return valueList;
    }

    [ConnectionProvider("Letter Filter", "ITransformableFilterValues")]
    public ITransformableFilterValues SetConnectionInterface()
    {
        return this;
    }

    public bool AllowEmptyValue
    {
        get { return true; }
    }

    public bool AllowMultipleValues
    {
        get { return false; }
    }

    public bool AllowAllValue
    {
        get { return true; }
    }
}

    

Code in ConsumerVisualWebpart.cs:   


public class ConsumerVisualWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
    private const string _ascxPath = @"~/_CONTROLTEMPLATES/ConsumerProviderVWP/ConsumerVisualWebPart/ConsumerVisualWebPartUserControl.ascx";

    public ConsumerVisualWebPart()
    {
        _filterProviders = new List<IFilterValues>();
    }

    protected override void CreateChildControls()
    {
        ConsumerVisualWebPartUserControl control = (ConsumerVisualWebPartUserControl)Page.LoadControl(_ascxPath);
 

        control.ValueSendByProviderProperty = "From provider: ";
        foreach (IFilterValues filter in FilterProviders)
        {
            if (filter.ParameterValues != null)
            {
                foreach (string item in filter.ParameterValues)
                {
                    control.ValueSendByProviderProperty += item;
                }
            }
        }
 

        Controls.Add(control);
    }

    protected override void Render(HtmlTextWriter writer)
    {
        base.Render(writer);
        foreach (IFilterValues filter in FilterProviders)
        {
            writer.WriteLine(string.Format("Parameter: {0} <br>", filter.ParameterName));
        }
    }

    /// <summary>
    /// Hold incoming filtervalues
    /// </summary>
    private List<IFilterValues> _filterProviders;
    private List<IFilterValues> FilterProviders
    {
        get { return _filterProviders; }
    }

    [ConnectionConsumer("filter", "UniqueIDForConsumer", AllowsMultipleConnections = true)]
    public void SetFilter(IFilterValues filterValues)
    {
        if (filterValues != null)
        {
            List<ConsumerParameter> parameters = new List<ConsumerParameter>();
            parameters.Add(new ConsumerParameter("Letter", ConsumerParameterCapabilities.SupportsSingleValue | ConsumerParameterCapabilities.SupportsAllValue | ConsumerParameterCapabilities.SupportsEmptyValue));
            filterValues.SetConsumerParameters(new System.Collections.ObjectModel.ReadOnlyCollection<ConsumerParameter>(parameters));
            this.FilterProviders.Add(filterValues);
        }
    }
}

Code in ConsumerVisualWebpartUserControl.ascx.cs:   


public partial class ConsumerVisualWebPartUserControl : UserControl
{
    public string ValueSendByProviderProperty { get; set; }
 

    protected void Page_Load(object sender, EventArgs e)
    {
        ValueSendByProvider.Text = ValueSendByProviderProperty;
    }
}

Control added to VisualWebpart.ascx:   


<asp:Label ID="ValueSendByProvider" runat="server" Text="Label"></asp:Label>

That’s it!   

Put both controls on a page and connect them to each other.   

No connection set:   

 Connect:   

Configure:   

Connection established:   

Search Refiners (part 4) – User selection based

In this series:  

  1. Search Refiners part 1 – Expanding the OOTB search Refinement Panel
  2. Search Refiners part 2 – Use of CustomFilters
  3. Search Refiners part 3 – Chart based
  4. Search Refiners part 4 – User selection based (this post)

After building a chart based search refiner, now it’s time to get some interaction in the selection of search terms. In this post I will show you how to use jQuery in combination with the RefinementManager.
With interaction I mean the user can drag a search term to a place on the screen to refine the results. Unfortunately the OOTB Refinement Panel and the results webpart ‘talk’ to each other by the parameters in the url. Therefor a postback must occur to actually refine the results with the selected term. But a little animation just looks great. 

For the drag and drop functionality I used the standard jQuery library and a custom jQuery UI selection. With this functionality in hand I created my own drag and drop implementation for this specific solution. 

Script part

Because I already have a little experience with programming against the RefinementManager class in the chart based example I first focused on the drag drop functionality.
Two divs are placed on the screen:
1. to list all the terms the user can choose from to select
2. the already selected terms by the user 

The items in both divs has to have the ability to drag items from and to drop items to. The user has to have the possibility to drag items from the ‘terms to select’ part to the ’selected terms’ part and vice versa.
Both the divs contain an unordered list with list items.
So a single div with items in it looks in plain HTML like:

<div>
<ul>
<li>item01</li>
<li>item02</li>
</ul>
</div>

A single item has to be draggable to move it from the ‘terms to select’ to the ’selected terms’ part(and vice versa). So the li element has to be draggable.
On the other hand when a li element a dragged, the list to which the single item has to be added has to be droppable, the ul element. 

To accomplish this just a few lines of jQuery are neccesary, the rest is alredy build in the jQuery libraries.

    var $currentSelectedItems = $("#selectedTerms"),
 $baseItemList = $("#termsToSelect");</pre>
 

    //make the li's in the selectedTerms div draggable
    $("li", $currentSelectedItems).draggable({
        appendTo: $baseItemList,
        helper: "clone"
    });
 

    //make the ul in the selectedTerms div droppable
    $("ul", $currentSelectedItems).droppable({
        accept: "#termsToSelect li",
        hoverClass: "ui-state-hover",
        drop: function (event, ui) {
            moveTerm(ui.draggable, $currentSelectedItems);
        }
    });

The draggable functionality is nothing special. The element passed to appendTo is the container during dragging. Second the clone helper is used here, which means a clone of the actual item is dragged around. Another option here is original, which drags the original item around. 

The droppable functionality uses different options. First I’m telling the element what items to accept: an li element within ‘termsToSelect’. Second a nice hover class and finally a function is specified what to do when the actual drop occurs: move the term to from ‘terms to select’ to ’selected terms’ and navigate to the url which the item received from the RefinementPanel (getting there soon). 

    //move term
    function moveTerm($item, $listToAdjust) {
        $item.fadeOut(function () {
            var $list = $("ul", $listToAdjust);
            $item.appendTo($list).fadeIn();
            window.location.href = $item.find('a').attr("href");
        });
    }

  

The script above is the script which act on the ’selected terms’ part ($currentSelectedItems or $(“#selectedTerms”)). The same functionality have to be added to the ‘terms to select’ part. Because it’s almost the same this code it’s left out here for readability. 

That’s is with all the script, let’s move the code behind all this. 

The webpart

The code behind this (just a regular webpart) works with the RefinmentManager class of the page as in the chart based example, the previous post in this series. The extra functionality here is that the selected term is needed from the RefinmentManager’s filter. If a term is selected or not is just another node in the refinement xml, just as Value and Url as shown in the following code listing:
 

string filterValue = filter.SelectSingleNode("Value").InnerText;
string filterUrl = filter.SelectSingleNode("Url").InnerText;
 

if (filter.SelectSingleNode("Selection").InnerText.Equals("Selected"))
{
    dateSelectedList.Add(new ListItem(filterValue, filterUrl));
}
else
{
    dateToSelectList.Add(new ListItem(filterValue, filterUrl));
}

dateSelectedList and dateToSelectList are both generic lists of ListItems and in the RenderContents method the items in the lists are added to the appropriate containers. 

After putting this all together it looks like the folowing image when no selection are made by the user:

When dragging starts from ‘terms to select’ the location where to drag to is highlighted:
 

Because ‘helper’ was set to ‘clone’ earlier the item that’s dragged, ‘Past Week’, is still in the ‘terms to select’ container and also moving around the screen. 

When dropping the item for a moment the screen looks like (because of the ‘appendto’ option):
  

And then a postback occurs and it looks like:
    

And the terms displayed behave just like the OOTB refiners.    

Summary

The OOTB refinement functionality is great, but it can be made much more attractive with just a little script as shown in this post. What I’ve shown you here is just a little example with one of the possibilities to make it more attractive. There are a lot of jQuery libraries with excellent functionality which you can use to do all kinds of stuff and don’t forget the design. This example looks nothing like a nice production ready refiner, but go wild and make it look awesome.

DIWUG SharePoint eMagazine 4

A couple of months ago Marianne van Wanrooij and I had a chat at one of the SharePoint conferences in The Netherlands. Before I realised I promised her to write an article for the awesome DIWUG SharePoint eMagazine.

Just kidding, I am pleased I could write an article!

Since I like the jQuery library for SharePoint WebServices called SPServices of Marc Anderson, the choice of a subject for the article wasn’t that hard.

Please download the magazine here and enjoy!

http://www.diwug.nl/Pages/downloads.aspx

Thanks to Marianne and Mirjam for publishing and thanks to Marc D Anderson for reviewing my article!

SDN Magazine

Recently I got the opportunity to write an article for the SDN magazine. SDN is an abbreveation of Software Developement Network and is a Dutch network. Alexander Meijers asked me if I had time and pleasure of writing an article for the SDN magazine, so I did. Here is a link to the contents of the magazine:

http://www.sdn.nl/SDN/Magazine/tabid/66/articleType/ArticleView/articleId/3003/SDN-Magazine-108.aspx

The articles will be soon available at the SDN site: http://www.sdn.nl.

Alexander, do you know when the articles will be available?

Search Refiners (part 3) – Chart based

In this series:  

  1. Search Refiners part 1 – Expanding the OOTB search Refinement Panel
  2. Search Refiners part 2 – Use of CustomFilters
  3. Search Refiners part 3 – Chart based (this post)
  4. Search Refiners part 4 – User selection based

 

In the previous two posts about search refiners no code was written to adjust the OOTB Search Refinement Panel, just some XML modifications.
In this post a chart based search refiner will be built and some code is needed to accomplish this. The refinement will be built on the Modified Date.  

Create the refiner

To get the refinement categories and their values, the RefinementManager class will be used.
The RefinementManager will get an instance of the refinement manager on the current page. This means the OOTB Search Refinement Panel has to be present on the current page. Not only it has to be present, it also has to have the filtercategory on which the refinement is going to perform on, in this case the Modified Date. 

Get the instance of the refinement manager:

RefinementManager refinementManager = RefinementManager.GetInstance(this.Page);

To get the refinement values:

XmlDocument refinementXmlDoc = refinementManager.GetRefinementXml();

The GetRefinementXML method gets the filtered xml document of the refinement manager.
For the example is this post only the Modified Date is of interest, so an XPath expression is used to get to the right filter category and values:

XmlNodeList filters = refXmlDocument.DocumentElement.SelectNodes("/FilterPanel/FilterCategory[@ManagedProperty='" + refiner + "']/Filters/Filter[Count>0]");

The refiner variable is declared as:

private const string refiner = "Write";

By looping through the XMLNodes in the XMLNodeList the neccesary values are available, just as in the XML which is rendered by the OOTB Search Refinement Panel. The elements of the childnodes (filter categories) are shown:  

Actually, this was the most interesting part. What’s left is displaying the values to the user, in this case: putting it in a chart; and binding it all together.  

Putting it all together

In CreateChildControls the chart is setup:

private Microsoft.Office.Server.WebControls.Chart chart;
protected override void CreateChildControls()
{
    ChartArea area = new ChartArea("Pie Chart");

    chart = new Microsoft.Office.Server.WebControls.Chart();
    chart.Width = 160;
    chart.Height = 160;
    chart.ChartAreas.Add(area);

    Controls.Add(chart);
}

And in the OnPreRender the refiment manager, the values of the filter category Modified Date and the chart are put together.

protected override void OnPreRender(EventArgs e)
{
    RefinementManager refManager = RefinementManager.GetInstance(this.Page);

    XmlDocument refXmlDocument = refManager.GetRefinementXml();

    if (refXmlDocument != null)
    {
        Series chartSeries = new Series();
        chartSeries.ChartType = SeriesChartType.Pie;

        XmlNodeList filters = refXmlDocument.DocumentElement.SelectNodes("/FilterPanel/FilterCategory[@ManagedProperty='" + refiner + "']/Filters/Filter[Count>0]");

        if (filters.Count != 0)
        {
            foreach (XmlNode filter in filters)
            {
                string xValue = filter.SelectSingleNode("Value").InnerText;
                string yValue = filter.SelectSingleNode("Count").InnerText;

                int i = chartSeries.Points.AddXY(xValue, yValue);

                // display text in chart
                chartSeries.Points[i].Label = xValue;

                //use the url to make the pie parts clickable
                chartSeries.Points[i].Url = filter.SelectSingleNode("Url").InnerText;
            }

            //add 'Any Modified Date' option
            XmlNodeList xmlFilterNodesAllItems = refXmlDocument.DocumentElement.SelectNodes("/FilterPanel/FilterCategory[@ManagedProperty='" + refiner + "']/Filters/Filter[Count='']");
            chart.Titles.Clear();
            System.Web.UI.DataVisualization.Charting.Title allItemsTitle = new System.Web.UI.DataVisualization.Charting.Title();
            allItemsTitle.Text = xmlFilterNodesAllItems[0].SelectSingleNode("Value").InnerText;
            allItemsTitle.Url = xmlFilterNodesAllItems[0].SelectSingleNode("Url").InnerText;
            allItemsTitle.ToolTip = "Refine by: " + allItemsTitle.Text;
            allItemsTitle.ForeColor = Color.FromArgb(0, 114, 188);
            chart.Titles.Add(allItemsTitle);
        }

        if (chartSeries.Points.Count > 0)
            chart.Series.Add(chartSeries);
    }
}

In the XPath expressions used to get the filter values for Modified Date and ‘Any Modified Date’ are slightly different. Look at the Count differences.
By removing the Count in the first XPath expression the chart will display ‘Any Modified Date’ and the chart kind of gets messed up:

and ‘Any Modified Date’ is not clickable, because it get’s no colored part in the pie chart. The reason for this is that the Count value for Any Modified Date is empty.
This is the reason to use Count>0 for the values displayed in the pie chart and Count=” to get Any Modified Date.
The pie chart now looks like this:
  

And after selecting ‘Past Month’:  


  

Summary

To transform a textual search refiner to a chart based search refiner is shown in this post.
As long as the OOTB Search Refinement Panel is present on the page and the filter category used by the code is present in the Filter Category Definition (property of Search Refinement Panel web part), the code in this post can be used to visualize a search refiner.
If it’s not present, you can extend the Refinement Panel webpart. This is not covered in this post, but maybe I’ll show you this in a future post.  

   

Search Refiners (part 2) – Use of CustomFilters

In this series:  

  1. Search Refiners part 1 – Expanding the OOTB search Refinement Panel
  2. Search Refiners part 2 – Use of CustomFilters (this post)
  3. Search Refiners part 3 - Chart based
  4. Search Refiners part 4 – User selection based

 

The OOTB Search Refinement Panel can be expanded with CustomFilters. An example of the use of a default custom filter used in the OOTB Filter Definition is the Modified Date refiner.
This custom filter is of type RangeMapping. This means a range of values are mapped to a custom value.
The XML below shows the standard custom filters section of the Modified Date refiner.

<CustomFilters MappingType="RangeMapping" DataType="Date" ValueReference="Relative" ShowAllInMore="False">
  <CustomFilter CustomValue="Past 24 Hours">
    <OriginalValue>-1..</OriginalValue>
  </CustomFilter>
  <CustomFilter CustomValue="Past Week">
    <OriginalValue>-7..</OriginalValue>
  </CustomFilter>
  <CustomFilter CustomValue="Past Month">
    <OriginalValue>-30..</OriginalValue>
  </CustomFilter>
  <CustomFilter CustomValue="Past Six Months">
    <OriginalValue>-183..</OriginalValue>
  </CustomFilter>
  <CustomFilter CustomValue="Past Year">
    <OriginalValue>-365..</OriginalValue>
  </CustomFilter>
  <CustomFilter CustomValue="Earlier">
    <OriginalValue>..-365</OriginalValue>
  </CustomFilter>
</CustomFilters>

In this case the DataType is set to Date and the ValueReference to Relative. The DataType represents the type returning from the managed property and the ValueReference compares the data on a relative basis.

Besides the RangeMapping the mapping type ValueMapping can be used. This means a value can be used to map to a customvalue.
This type will be used in the following example.

Intro

To simply add a new custom filter let’s proceed with the example in the first post of this series.
In this example a number of documents were uploaded to the document library. In this library CarLease documents, Employee contracts and other types are stored. The documents are not ´typed´. It´s just the name of the document which suggests a type. Suppose the Employee contracts are the base contracts and other contracts are supplementary.
This distinction is what the search refiner has to show:

  • Employee Contract
  • Additional Contract

and not all the possible types of documents.

Create the refiner

To set the type of a document add a column named Document Type with some document type options like `Car Lease`, ´Health Care´ and ´Employee Contract´. Set the type of each document.

This is a simple way to ´type´ a document. A better real life approach is to create different content types and use these as the base of the documents to distinguish them. Just for now the column with the types is used.

This ´type´ is going to be used in the search refinement panel. Before it can be used a managed property has to be created and a full crawl of the content has to be performed. If you don´t recall how to do this, please check the first post in this serie.

The next step is to add a new refinement category in the refinement panel with the custom filter. The easiest way to define the XML is to use Visual Studio to get some indentation and coloring which will help you format the XML properly.
Copy the existing XML from the Filter Definition in the refinement panel and copy it in Visual Studio. In the first post of this serie a new category has been added, so now the focus is on the custom filter.

The parent element of a single custom filter the the CustomFilters element.

<CustomFilters MappingType="ValueMapping" DataType="String" ValueReference="Absolute" ShowAllInMore="False">
...
</CustomFilters>

Since the Document Type is just a piece of text the DataType is set to String. In the Modified Date example the Date type is used. Besides these two types the type Numeric can be used when appropriate.
The ValueReference is set to Absolute for comparison on an absolute basis. Other option is ShowAllInMore. When a user selects the ’show more’ option in the refiner more options for the refiner will be shown. In the Category definition an attribute NumberOfFiltersToDisplay is used which represents the number of filters to show at first glance. When more filters are present the ’show more’ link is displayed. Once selected the other filters are shown. With the ShowAllInMore option set to true all these filters are shown when selecting ’show more’, even if there are no results with this filter.
Now a custom filter can be defined:

<CustomFilter CustomValue="Employee Contract">
  <OriginalValue>Employee Contract</OriginalValue>
</CustomFilter>

The original value is the value of the property returned by the search, the custom value is a value you can choose youself to display in the search refinement panel.
The OriginalValue element is not limited to one. You can define multiple element to map to one custom value:

<CustomFilter CustomValue="Additional Contract">
  <OriginalValue>Health Care</OriginalValue>
  <OriginalValue>Care Lease</OriginalValue>
</CustomFilter>

Edit the search refinement panel and select the Filter Categoy Defintion.
After crawling and preparing the XML for the search refinment panel the property can be used in the search.
The full XML for the new Category with custom filters looks like this:

<Category     Title="Document Type"     Description="Type of document" Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator"     MetadataThreshold="3" NumberOfFiltersToDisplay="3"     MaxNumberOfFilters="20"     ShowMoreLink="True"     MappedProperty="DocumentType" MoreLinkText="show more"     LessLinkText="show fewer">
  <CustomFilters MappingType="ValueMapping" DataType="String" ValueReference="Absolute" ShowAllInMore="False">
    <CustomFilter CustomValue="Additional Contract">
      <OriginalValue>Health Care</OriginalValue>
      <OriginalValue>Care Lease</OriginalValue>
    </CustomFilter>
    <CustomFilter CustomValue="Employee Contract">
      <OriginalValue>Employee Contract</OriginalValue>
    </CustomFilter>
  </CustomFilters>
</Category>

Test the refiner

Perform a search on one of the words in the title of one of the documents in the Document Library. Notice the Document Type refiner.


Notice the refiner displays only two filters, ‘Employee Contract’ and ‘Additional Contract’. Just as defined in the custom filters part of the XML.

Summary

Custom filters can be very useful to guide users in performing a search and refine the results. The example used here is just for demonstration purposes, but I guess you can think of something useful in your organisation. Maybe refiners on file size or different content types.

Search Refiners (part 1) – Expanding the OOTB search Refinement Panel

In this series:  

  1. Search Refiners part 1 – Expanding the OOTB search Refinement Panel (this post)
  2. Search Refiners part 2 – Use of CustomFilters
  3. Search Refiners part 3 – Chart based
  4. Search Refiners part 4 – User selection based

Intro

A large multinational company has several contracts. These contract are stored in SharePoint. Based on the
multinational character of the company the contracts are stored in different languages.
Titles of the documents are all in English, so there is no way to determine from the title in what language the
document is written. Therefor a new column is introduced where the language can be stored.
This column in a lookup column to another list where all the languages are defined.
When people are searching for contracts there is no quick way to see in what language the document is written. To
prevent users to open multiple documents to see the language of the content a search refiner will help them out.  

The existing OOTB refinement panel can be expanded just with XML.
Before doing so some preparations has to be done.  

Create lists

First a custom list has to be created which will store the possible language values.
A document library has to be added to a site which will contain the documents for the contracts. An additional column
has to be added to let the user choose the language in which the document is written. This will be a lookup column to
the previous created list of languages.
A set of documents are uploaded with their languages set.

Crawl content

Start Central Administration, select Application Management and Manage Service Applications. Select Search Service
Application to display the Search Administration page.
Select Content Sources and start an incremental crawl.  

Create managed property

When this is finished create a Managed Property called Language and add a mapping to the crawled property ows_Language.

Start a Full Crawl.  

Check if any results are present in the crawled property.  

Modify Filter Category Definition

Create an Enterprise Search Center and edit the Refinement Panel. In the webpart properties in the section Refinement
select the button next to the Filter Category Definition textbox to show all the text in it.
  

The XML is not difficult to read and understand when copying and pasting the XML into an XML file in Visual Studio..
Even without checking on the full Refinement Panel XML schema it’s pretty clear what to do by copying an existing
Category element and make some changes.
Full explanation of this schema can be found on MSDN here http://msdn.microsoft.com/en-us/library/ee819920.aspx �
Add to the filter category definition in the webpart properties:

<Category Title="Language" Description="Language of document" Type="Microsoft.Office.Server.Search.WebControls.ManagedPropertyFilterGenerator" MetadataThreshold="3" NumberOfFiltersToDisplay="3" MaxNumberOfFilters="20" ShowMoreLink="True" MappedProperty="Language" MoreLinkText="show more" LessLinkText="show fewer" />

Don’t forget to uncheck ‘Use default configuration’.
Unchecking this option is quite important. When this checkbox is selected any changes to the filter category definition
configuration are not persistent. A better experience would be disabling the Filter Category Definition when this
checkbox is selected…  

Apply and save the changes.  

Test the refiner

Start a search on one of the words in the title of one of the documents uploaded in the created Document Library.
Notice the Language refiner on the left.
 

The values displayed of the Language refiner is default sorted by count. This means the value which is used most
appears on top.  

Refiner is not showing

When the refiner is not showing in the Search Center there are a few things to check.

  1. Make sure to fully crawl the content after creating the managed property and confirm the crawled property contains
    values.
  2. Make sure there is enough data which uses in this case the language. In the above XML the value of the
    MetadataThreshold attribute is set to 3. This means the number of results that must contain a value to display the
    filter generator under the filter category is set to 3.
  3. Uncheck Use Default Configuration in the webpart properties of the Refinement Panel, section Refinement.
  4. In the webpart properties of the Refinement Panel in the secion Refinement a value is displayed for Number of
    Categories to Display. If the number of categories exceeds this number and the new category is defined last in the XML,
    it won’t show up.  

Summary

SharePoint provides OOTB an easy way to create your own search refiners.
A crawled property, content and some XML to define the filter category are sufficient.


  • Recent Posts

  • Tags

  •      
  • Archives