Posts Tagged ‘jQuery’

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.

Imtech

Branche: ICT & Internet dienstverlening 

Februari 2011 – mei 2011

Lead developer SharePoint

  

Oasen maakt drinkwater voor 750.000 mensen en 7.200 bedrijven in het oosten van Zuid-Holland. Het voorzieningsgebied bestaat uit 32 gemeenten.

Op dit moment is de publieke website van Oasen gebaseerd op MOSS2007. Door een nieuwe visie op de vindbaarheid van content, indeling, interactie met de klant en dynamiek is er een nieuwe website ontwikkeld op basis van SharePoint 2010. Een derde partij heeft de ontwikkeling van het design op zich genomen. Het team van Imtech aangevuld met Anita mag de website ontwikkelen op basis van het User Experience document wat het design laat zien.

Basis voor de technische oplossing voor de nieuwe website zijn het User Experience document en de website van Oasen. De technische oplossing en implementatie van de nieuwe website zijn bepaald door het projectteam. Anita is verantwoordelijk voor deze technische keuzes en oplossingen.

Het projectteam bestaat uit een projectleider, consultant, lead engineer, twee engineers en drie designers. Anita vervult de rol van lead engineer. Scrum wordt toegepast met sprints van 2 a 3 weken. De totale doorlooptijd van het project is 9 weken.

Er wordt waar mogelijk gebruik gemaakt van standaard SharePoint componenten, welke aangepast en uitgebreid worden om de exact gewenste functionaliteit te verkrijgen.

De nieuwe website van Oasen maakt zeer intensief gebruik van de SharePoint Search functionaliteit. Dit is de technische basis en komt in vele onderdelen van de website terug. Content in de website wordt voorzien van metadata (onder andere tags) om de vindbaarheid te verbeteren en om de content te kunnen verfijnen op basis van deze tags. De content is onderverdeeld in verschillende types als nieuws, artikelen, applicaties, veelgestelde vragen, foto’s, video’s en storingen.

De traditionele navigatie binnen een website door middel van een menustructuur is grotendeels vervangen: er wordt gebruik gemaakt van de mogelijkheid om content te verfijnen op basis van tags. Om dit te realiseren wordt er gebruik gemaakt van het standaard RefinementPanel met een aangepaste filterdefinitie (inclusief bijbehorende managed/crawled properties) en xslt. Buiten het standaard RefinementPanel is er een custom refiner ontwikkeld welke content binnen een bepaalde tijdsperiode filtert.

Om de content te tonen is het Core Results webpart diverse keren geimplementeerd in een zeer aangepaste vorm. Content wordt dynamisch toegevoegd aan de zoekresultaten op de verschillende pagina’s middels intensief gebruik van http handlers, jQuery en JSON. Vanuit de zoekresultaten kan er genavigeerd worden naar het weergegeven type content. Foto’s en video’s worden getoond in een lightbox met extra informatie over het getoonde item.

Op de website wordt intensief gebruik gemaakt van het Content Query webpart. De nieuwe ‘slots’ functionaliteit wordt toegepast en er zijn nieuwe xslt item styles ontwikkeld.

Uiteraard is er een masterpage ontwikkeld en diverse page layouts om de website structuur te geven en van een consistente look & feel te voorzien in de stijl welke bij Oasen past. Het design van de website is zeer vernieuwend en dynamisch en brengt de nodige uitdagingen met zich mee in combinatie met SharePoint. Er is geen onderdeel in de website wat overeenkomt met standaard SharePoint design.

Buiten nieuw ontwikkelde functionaliteit zijn er diverse bestaande modules gemigreerd naar de nieuwe site. Als klant van Oasen kan er gebruik worden gemaakt van modules als het doorgeven van meterstanden of een verhuizing, het rekeningnummer wijzigen en het voorschot aanpassen.

Verder worden er grafieken over het waterverbruik getoond. Hier kan bijvoorbeeld mee worden bepaald wanneer de rust in een belangrijke voetbalwedstrijd is.

Diverse watermeters worden getoond om de hoeveelheid water aan te geven welke Oasen levert in diverse regio’s. Deze meters worden grafisch weergegeven door middel van flash technologie. Diverse modules hebben een BizTalk koppeling met achterliggende systemen.

Naast de beschreven (basis) functionaliteit zijn er vele onderdelen ontwikkeld, welke binnenkort op de website van Oasen zullen worden gepubliceerd op http://www.oasen.nl

Diverse onderdelen van SharePoint zijn gebruikt om de functionaliteit te kunnen realiseren, zoals sitecolumns, contenttypes, listinstances, jQuery, HTTP Handlers, application pages, css, xslt, page layouts, masterpage, custom actions, eventreceivers, workflows, user controls, webcontrols, webparts, delegates, timerjobs, Excel services.

De website van Oasen is een publieke website, waarbij performance en optimalisatie uitermate belangrijk zijn.

Voor de registratie van de te realiseren onderdelen is Team Foundation Server 2010 gebruikt. Anita is verantwoordelijk voor het bepalen van te realiseren onderdelen in elke sprint en het creeren van taken gerelateerd aan deze sprint backlog items. Daar Anita verantwoordelijk is voor de technische keuzes in het project is het van belang dat zij ook de inhoud van de sprints bepaalt, zodat er snel een basis voor de website staat. Mede omdat de doorlooptijd van het project maar 9 weken is. Voordat een sprint start wordt in een sessie met de engineers en designer de sprint backlog items verdeeld met in achtneming van de beschikbare uren van de projectleden en de te verdelen items. Tijdens het project is Anita verantwoordelijk voor de planning, voortgang en kwaliteit van het opgeleverde. Anita straalt rust uit over het team en staat ten alle tijde open voor vragen van elk lid van het projectteam.

SharePoint Server 2010, Visual Studio 2010, jQuery, JSON, xslt, Excel services, Team Foundation Server 2010, TFS Sidekicks, Imtech OCD, Scrum.

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.

Portal for Business

  

Branche: Informatie technologie

Januari 2011

SharePoint consultant/developer

 

Voor diverse klanten van Portal for Business heeft Anita de huisstijl toegepast op intranet sites door middel van het ontwikkelen van custom masterpages en style sheets.
Een intranet bestaat bij de klanten uit meerdere web applicaties. Om globale navigatie consistent te houden op de diverse web applicaties is er een custom site map provider ontwikkeld om dit te realiseren. 

Hiernaast zijn er custom ribbon buttons ontwikkeld en een news aggregator webpart. Dit webpart verzamelt nieuws items van een bepaald niveau binnen een web applicatie tot alle onderliggende subsites en toont deze items. 

De genoemde functionaliteiten zijn zowel voor SharePoint Server 2010 als voor SharePoint Foundation 2010 ontwikkeld. De solutions zijn zoveel mogelijk sandboxed solutions, tenzij de gewenste functionaliteit en daarbij behorende oplossing dit niet toelaat, zoals de custom site map provider. 

SharePoint Server 2010, SharePoint Foundation 2010, Visual Studio 2010, jQuery

SPServices and jQuery templates: Upcoming birthday’s overview

The use of jQuery templates is very neat. It easily displays content in a structured way according to a defined template. Microsoft created the jQuery template plugin (https://github.com/nje/jquery-tmpl) which can be used by referencing the template script file. 

In this article both SPServices and the jQuery template will be used to show the strength of combining these libraries. 

Purpose

All employees are highly appreciated at the company there are working (of course). The company decides to send all employees flowers on their birthday. The employee data is stored in a standard SharePoint Contacts list extended with a column to register the birth days. Employees of the HR department are responsible for sending the flowers on time, but they it’s difficult for them to get the upcoming birthdays from the Contact list.
It is desirable to provide the HR department with an overview of upcoming birthdays.    

Set up the Contacts list

To create the overview of upcoming birthdays a standard SharePoint Contacts list is used extended with a column with the name ‘Date of birth’ of type Date and Time. To display some additional information in the overview the column ‘Department’, type single line of text, is added to the list.
The list is filled with some data:

Create the overview    

The SPServices operation GetListItems is used to get the data from the Contacts list:    


$().SPServices({
    operation: "GetListItems",
    async: false,
    debug: true,
    listName: "Contacts",
    CAMLViewFields: "<ViewFields></ViewFields>",
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("[nodeName=z:row]").each(function () {
            daysleft = CalculateDaysLeft($(this).attr("ows_Date_x0020_of_x0020_birth"));
            contacts.push({ FirstName: $(this).attr("ows_FirstName"), LastName: $(this).attr("ows_Title"), Department: $(this).attr("ows_Department"), DaysLeft: daysleft });
        });
        SortByDaysLeft(contacts);
        $("#showContacts").html("");
        $("#contactTemplate").tmpl(contacts)
                        .appendTo("#showContacts");
    }
});

The callback function gets the response of the web service. The function CalculateDaysLeft calculates the days left until the date stored in ‘Date of birth’ will occur in the current year. Since this is a helper method it is not showed here.
The firstname, lastname, department and the daysleft are stored in an array of objects named contacts. To display the birthdays in upcoming order the function SortByDaysLeft is called. This is also a helper function and for readability not displayed here.
A jQuery template, contactTemplate, is used to show the results on the page. The template is rendered with the data and appended to a div with the id showContacts.  The div functions as the template container.
The template uses the objects in the array: 

<script id="contactTemplate" type="text/x-jquery-tmpl">
 <div>
    ${FirstName} ${LastName}
    <ul style="list-style-type:none;">
    <li>${Department}</li>
    <li>${DaysLeft} days left until birthday!!</li>
    </ul>
 </div>
</script>

The first thing you’ll probably notice is the script tags. Because of these tags the template is embedded in the body of the page. In the template html can be used to format the data to be displayed. The ${FirstName}, ${LastName}, ${Departement} and ${DaysLeft} expressions are used in the template as placeholders for the data present in the array of objects ‘contacts’. ${..} tells the parser the fields have to be replaced with the values passed in the template by the array of objects.    

The template is rendered once for each item in the array named contacts.
The upcoming birthdays are displayed with the contactTemplate as shown in the figure below:

This template is quite simple and fulfills the purpose of displaying upcoming birthdays from the Contacts list.

The template displays ‘.. days left until birthday!’ for all items. It’s much nicer to display a message based on the number of days left to provide the HR department with some additional information.
This can be accomplished with code in the template itself with an ‘if ..else..’ statement as shown in the code below. 

<script id="contactTemplateInlineCode" type="text/x-jquery-tmpl">
 <div>
    ${FirstName} ${LastName}
    <ul style="list-style-type:none;">
        <li><a href="#" id=${Department} title="Show all employees of ${Department}">${Department}</a></li>
        <li>
            {{if (DaysLeft < 10)}} Did you order flowers already? It's almost his birthday, only ${DaysLeft} days!
            {{else DaysLeft < 150}} It takes a little while until his birthday: ${DaysLeft} days!
            {{else (DaysLeft < 360 )}} A long time until his birthday: ${DaysLeft} days!
            {{else (DaysLeft > 359 )}} If you didn't sent any flowers, you're too late now...{{/if}}
        </li>
    </ul>
 </div>
</script>

Besides adjusting the text based of the number of days left, the Department is adjusted to be a link. When selecting the link the function GetAllContactsFromDepartment is called with the ID of the link. The ID of the link is set with the Department value. The values of the array used by the template can be used everywhere in the template. The function GetAllContactsFromDepartment will display the firstname, lastname and day and month of the birthdate of the specified Department with the help of the SPServices library operation GetListItems.
The results of this operation are stored in an array of objects and another template is rendered with this array.

 $().SPServices({
    operation: "GetListItems",
    async: false,
    debug: true,
    listName: "Contacts",
    CAMLViewFields: "<ViewFields></ViewFields>",
    CAMLQuery: "<Query><Where><Eq><FieldRef Name='Department'/><Value Type='Text'>" + Department + "</Value></Eq></Where></Query>",
    completefunc: function (xData, Status) {
        $(xData.responseXML).find("[nodeName=z:row]").each(function () {
            var temp = $(this).attr("ows_Date_x0020_of_x0020_birth");
            var formattedDate = "";
            if (temp.length > 0) {
                var month = temp.substring(5, 7);
                var day = temp.substring(8, 10);
                formattedDate = day + " " + month;
                daysLeft = CalculateDaysLeft(temp);
            }
            contactsOfDepartment.push({ FirstName: $(this).attr("ows_FirstName"), LastName: $(this).attr("ows_Title"), BirthDate: formattedDate, DaysLeft: daysLeft });
        });

        SortByDaysLeft(contactsOfDepartment);
        $("#showContactsFromDepartment").html("<h4>" + Department + "</h4>");
        $("#DepartmentTemplate").tmpl(contactsOfDepartment)
                        .appendTo("#showContactsFromDepartment");
    }
});

The DepartmentTemplate used in the code above:

<script id="DepartmentTemplate" type="text/x-jquery-tmpl">
<div style="float:left;width:100%">
    <div style="float:left;width:15%">
     ${FirstName} ${LastName}
    </div>
    <div style="float:right;width:85%">
     ${BirthDate}
    </div>
</div>
</script>

So within a template a call to fill another template with data can be made.    

The upcoming birthdays are now displayed as in the figure below:�

When selecting the Marketing department another template will be filled with data:

All the code displayed in this post is based on jQuery. 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.6.min.js" type="text/javascript"></script>
<script src="/jQueryLibrary/jquery.tmpl.js" type="text/javascript"></script>

Summary    

By combining libraries like SPServices and jQuery templates it is quite straightforward to display a list of upcoming birthdays from a standard SharePoint Contacts list.    

The strength of the use of SPServices here is to get the necessary data in a straightforward way; the strength of the use of jQuery templates is to display the data in a nicely formatted way. Using a combination of both libraries I think the best of both worlds are used the appropriate way.

Extended cascading with SPServices and jQuery

Recently I wrote a plugin which provided cascading dropdowns with jQuery and SPServices, which you can find here.

To be more flexible about how to display this to the user I extended the plugin. With the extended plugin it is possible to show the parent values of the cascading part with radiobuttons and the child part still with a dropdown list. Also a new option is added to load the values of the childdropdown by default or to wait for the user to make a selection in the parent list.

To show the values of the parent list in a radiobutton list use the id of a div element present in the page:

<div id="countriesRadio"></div>

and call the plugin on this div:

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

$(document).ready(function () {

    $('#countriesRadio').itidea_spcascadingdropdownExtended(
    {
        relationshipList: "Cities",
        relationshipParentList: "Countries",
        relationshipParentListColumn: "Title",
        relationshipListChildColumn: "Title",
        relationshipListParentColumn: "Country",
        childdropdown: "citiesRadio",
        fillChildByDefault: true
    });

}); 

</script>

In the above code sample the new option fillChildByDefault is set to false. This means the values of the parent will be showed, but no child values will be showed  when the page loads. If this option is omitted in the above code the value is set to true by default in the plugin and the child values will be shown when the page loads.

With the above settings the controls look like this:

By omitting the option fillChildByDefault (which is the same as setting this to true):

When an item is selected:

Summary

With the extended plugin more flexibility is added to show the values to the user. With this plugin it is still possible to show both lists as dropdownlists by replacing the div element by a select element with the same id as shown in the previous post about cascading dropdowns.

Download

Download the plug-in: jquery.itidea_spcascadingdropdownExtended.js

SharePoint 2007 Author search refiner

SharePoint 2010 has a lot of nice features. One of them are search refiners. The search results can be narrowed down by Result Type, Site, Author, Modified Date, etc.

When knowing and seen that feature I’m really disappointed SharePoint 2007 doesn’t has this. Ok, the Advanced Search page is available, but it’s by far not that easy to use as the search refiners in SharePoint 2010. 

Another thing popped up into my mind when using the SharePoint 2010 search refiners. Why can’t I select for example multiple authors? Not all, not one, but two or three or whatever I like at that moment with that search action. 

Purpose 

These two things: Search refiner in SharePoint 2007 and the possibility to select more than one item
are the starting point of this post.

One other thing: I like SPServices and jQuery, so I’m going to try to use these techniques instead of a farm solution.

So let’s create a search refiner on authors in this post. When the user performs a search and the results are displayed the option to select one or more authors to narrow down the search results will be available. All the different authors are displayed with a checkbox to select one or more and narrow down the search results accordingly.

While writing this post a lot of explaining has to be done so I decided to show it first. After that I’ll explain how to get there.

See it in action 

User types a search text

The results are displayed

The unique authors are listed, the user selects one

Search results based on the author selection and the initial search text:

Now you know what it does, let me explain it. 

Analyzing the search 

When a user types his search text in the standard search box, SharePoint 2007 performs a search by encoding the query and posting it to the results page. That’s basically just it, a basic query.

The Advanced Search page uses SQL syntax and can contain almost every query and result one can possibly imagine. For the purpose in this post we have to use the SQL syntax.

The next step is to analyze the Advanced Search box webpart and how a search is performed.

By typing a search string in the ‘All of these words’ box and performing the search, the results are displayed. When analyzing the source of this page the SQL query can be found.
Searching on the word ‘item’ and the property ‘Author Contains anita’ results in the following SQL query (removed the encoding):

SELECT WorkId, Rank, Title, Author, Size, Path, Description, Write, SiteName, CollapsingStatus, HitHighlightedSummary, HitHighlightedProperties, ContentClass, IsDocument, PictureThumbnailURL  from scope() where freetext(defaultproperties, ‘+item’) And (Author like ‘%anita%’) Order By Rank desc

How does SharePoint know how to format the query like this? How does it know to put ‘item’ and the author search in the right place of the query?

With the help of the IE Developer toolbar:
‘All of these words’ has the name ASB_TQS_AndQ_tb.
The first ‘(Pick property)’ has the name ASB_PS_plb_0, the operator ASB_PS_olb_0 and the value ASB_PS_pvtb_0. The number at the end of the name will increase when multiple properties are used.  For the second property the names ending with _1 are used. The And Or operator when using multiple properties is called ASB_PS_lolb_0 and increases when more than two properties are used. These names are used to make up the SQL query.

There are more controls on this page but only the ones mentioned above will be used in this example.

The code 

To perform the right search, the SQL syntax will be used the way the Advanced Search webpart is using it by using html controls with the same names mentioned above. 

Getting the authors 

When the user performs a search, the results are displayed. The get all the authors of these search results to display the names in a checkbox label later on, the operation QueryEx of the SPServices library is used. 

First the query is set up: 

var queryTextSQL = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>"
queryTextSQL += "<Query>"
queryTextSQL += "<Context>"
queryTextSQL += "<QueryText language='en-US' type='MSSQLFT'>"
//use the same scope (Advanced Search)
queryTextSQL += "SELECT Title, Rank, Size, Description, Write, Path, Author FROM scope() WHERE FREETEXT (DEFAULTPROPERTIES, '+" + sessvars.searchValue + "') ORDER BY \"Rank\" DESC"
queryTextSQL += "</QueryText>"
queryTextSQL += "</Context>"
queryTextSQL += "</Query>"
queryTextSQL += "</QueryPacket>";

To use the SQL syntax the type attribute is set to MSSQLFT.
The sessvars.searchValue is filled with the initial search text which the user typed in the search box.  Sessions variables without cookies are used here. More information can be found here: http://www.thomasfrank.se/sessionvars.html 

After performing a search this search text is displayed in the querystring of the url.

In code it is stored in a variable using $().SPServices.SPGetQueryString() with the following code: 

var queryStringVals = $().SPServices.SPGetQueryString();
var searchItem = queryStringVals["k"];

This value is needed to get it into the SQL query and to put it back in the search box after performing the search. As you know when performing a ‘regular’ advanced search query, the search text is gone and not displayed in the search box anymore. To prevent ‘losing’ the user who performed the search the value will be put back in the search box. 

After setting up the query let’s get the unique authors for the results:

    $().SPServices({
        operation: "QueryEx",
        queryXml: queryTextSQL,
        completefunc: function(xData, Status) {
        k=0;
            $(xData.responseXML).find("RelevantResults").each(function() {
                $(this).find("AUTHOR").each(function() {
                    authors[k]=$(this).text();
                    k++;
                });
            });

            if(authors.length > 0)
            {
                authors = $.unique(authors);
                CreateCheckboxes(authors, "Author", "Contains");
            }
        }
    });

The code fires the query and gets the authors from the results. The CreateCheckboxes method will create all the necessary controls and will be explained in the next part. 

The controls

In this example an author search refiner will be built where a name of an author is the value in the query. To accomplish this, the control (using a checkbox in this example) which displays a single author has to have the name ASB_PS_pvtb_0, the next author ASB_PS_pvtb_1, etc. 

The other names of controls which have to be used are ASB_PS_plb_x (x starting at 0) to set the property name and ASB_PS_olb_x as the operator. Authors are used here, so ASB_PS_plb_x has always the value ‘AUTHOR’ and the operator has always the value ‘Or’ since an item can have one author at the same time. These two controls are hidden controls, since these controls are of no use to the user. Only the checkboxes are displayed. 

The CreateCheckboxes method will set this up for each individual author: 

    function CreateCheckboxes(authors, propertyName, operator)
    {
        var authorContainer= $('#someElementId');
        var j=1;
        $.each(authors, function( iteration, item )
        {
            //add or statement
            if(j==1)
            {
            //<input value="Or" />
            authorContainer.append(

                $(document.createElement("div"))
                .append(

                    createHiddenNamedElement("input","nameprefix$ASB_PS_lolb_" + (j-1), "Or")
                )
              )
            }

            authorContainer.append(
                $(document.createElement("div"))
                .append(
                        $(document.createElement("input")).attr({
                                type:  'checkbox'
                                ,name: 'nameprefix$ASB_PS_pvtb_' + j
                                ,id:    'author_' + item
                                ,value: item

                        })
                        .click( function( event )
                        {
                            var cbox = $(this)[0];
                            if(cbox.checked)
                            {
                                //remember selected checkboxes to put back after postback
                                sessvars['numberOfAuthorsSelected']++;
                                sessvars['numberOfAuthorsSelected' + sessvars['numberOfAuthorsSelected']] = escape(cbox.value);
                             }
                             else{
                                //remember only the selected items
                                var total = sessvars['numberOfAuthorsSelected'];
                                sessvars['numberOfAuthorsSelected']=0;
                                for(var i=1;i<=total;i++)
                                {
                                    if(sessvars['numberOfAuthorsSelected' + i] != escape(cbox.value))
                                    {
                                        sessvars['numberOfAuthorsSelected']++;
                                        sessvars['numberOfAuthorsSelected' + sessvars['numberOfAuthorsSelected']] = sessvars['numberOfAuthorsSelected' + i];
                                    }
                                }
                             }
                        } )
                )
                .append(
                    $(document.createElement('label')).attr({
                            'for':  'author_' + item
                    })
                    .text( item )
                )
                //add hidden input values
                .append(
                    createHiddenNamedElement("input","nameprefix$ASB_PS_plb_" + j, propertyName)
                )
                .append(
                    createHiddenNamedElement("input","nameprefix$ASB_PS_olb_" + j, operator)
                )
            )
            j++;
        } );
        SetSelection();
    }

First the operator ‘Or’ is created. After that, the author checkboxes are created. In the click event the tracking of selecting and unselecting the checkboxes is performed. Then the label is set for the checkbox and last the hidden controls are created to store the property name and the operator.
At last the SetSelection() method will be called. This method just selects the appropriate checkboxes based on the stored values.�
To perform the actual search a search button is needed:

<input onclick='WebForm_DoPostBackWithOptions(new WebForm_PostBackOptions("nameprefix$ASB_BS_SRCH_1", "", false, "", "results.aspx", false, false))' type="submit" value="Search" />

The url has to be set to the current page so the results, according to the selected authors and the search text provided, are displayed in the search results webpart on the same page. 

One last important issue

All properties are treated as numbers by default. When performing a search with the code created right now, an error will occur when selecting an author and perform the search:
Invalide parameter: Author. Expect a number. ITIDEA\anita.boerboom is given instead.’ 

To tell SharePoint to treat the Author property like text the following control has to be added to the page with the name ASB_TextDT_Props :

<input type="hidden" name="ASB_TextDT_Props" id="Hidden4" value="Author" />

When multiple properties have to be treated like text the values can be separated by #;#, e.g. Title#;#Author 

When properties have to be treated like dates, use ASB_DateTimeDT_Props the same way as the text property described. 

Summary 

Before creating a search refiner a lot of investigating was necessary, but it can be built for SharePoint 2007 fully in script. 

Also this example can be extended to show the number of items per author and of course other refiners can be built on other properties.

Cascading dropdowns with jQuery and SPServices on a page or webpart

The SPServices library on Codeplex (http://spservices.codeplex.com) provides a lot of useful functionality to access SharePoint data.
One of the functions is the SPServices.SPCascadeDropdowns function. Here is a link to the documentation of this function: http://spservices.codeplex.com/wikipage?title=$().SPServices.SPCascadeDropdowns&referringTitle=Documentation  

As explained on the site this function can be used to set up cascading dropdown on SharePoint forms. Repeat: on SharePoint forms.
Sometimes it’s handy to use cascading dropdown lists on a regular page or in a webpart. For this purpose I made a simple jQuery plug-in to provide in this need. The functionality is not that extended as the SPServices.SPCascadeDropdowns, but I’ll guess you get the point.  

Purpose 

The purpose in this example is to make cascading dropdowns with countries and cities. This information is stored in two SharePoint lists and displayed in a Content Editor Webpart on a page.  

Set up the lists

Create a list named Countries.  Fill in some country names in the default Title column.
Create a list named Cities. Create a lookup column to the Countries list and name the column Country. Do not allow multiple values, the plug-in isn’t ready for that. Fill in some cities and link them to a country.
Add a document library e.g. with the name ICTLibrary. Of course you are free to choose the names, but be aware of the references in the code in this article, I use the names mentioned here.
Upload three items to this list: 

  1. jquery-1.4.4.min.js
  2. jquery-SPServices-0.5.8.min.js
  3. jquery.itidea_spcascadingdropdown.js

The last file is the plug-in which provides the cascading functionality on a page.  

Create the cascading dropdownlists

To create the cascading dropdownlists on a page is quite easy. Just paste the following code to a Content Editor Webpart on a page:

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

<script type="text/javascript">

$(document).ready(function() {
    $('#countries').itidea_spcascadingdropdown(
    {
        relationshipList: "Cities",
        relationshipParentList : "Countries",
        relationshipParentListColumn : "Title",
        relationshipListChildColumn : "Title",
        relationshipListParentColumn : "Country",
        childdropdown : "cities"
    });
});
</script>

<select id="countries" style="width:150px;">
</select>
<select id="cities">
</select>

There are a few things to know about the options:   

relationshipList
The name of the list which contains the parent/child relationships.  

relationshipParentList
The name of the list which contains the parent items.  

relationshipParentListColumn
The StaticName of the values column in the parent list.  

relationshipListChildColumn
The StaticName of the child column in the relationshipList  

relationshipListParentColumn
The StaticName of the parent column in the relationshipList  

childdropdown
The id of the child dropdownlist  

promptText
The default text displayed in the dropdownlists  

Most of the options are named the same as in the SPServices.SPCascadeDropdowns, because you are probably familiar with these names.  

The cascading dropdownlists are looking like this now when nothing is selected:
  

   

Countries and all cities are loaded in the dropdownlists:
   

   

When a country is selected:
  

Summary

The plug-in is helpful if you want simple cascading dropdown lists on a page or in a webpart with the use of SharePoint data.     

Download

Download the plug-in: jquery.itidea_spcascadingdropdown.js Updated version (November 30 2010; order by was hard-coded on ‘Title’ column)

For an extended plugin check out this post.

Example of using the SPServices Search web service

Instead of CAML you can also use a search query by using SQL or keyword syntax to search the SharePoint environment client-side by using the Search web service. The SPServices library supports the Query operation to do this.      

Hereby I show you a few examples of how to use it.      

First let’s start building the query. The query is based on the Microsoft.Search.Query schema, which can be found here: http://msdn.microsoft.com/en-us/library/ms563775.aspx      

A query using keyword syntax:      

var queryText = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>"
queryText += "<Query>"
queryText += "<Context>"
queryText += "<QueryText language='en-US' type='STRING'>"
queryText += "Estonia";
queryText += "</QueryText>"
queryText += "</Context>"
queryText += "</Query>"
queryText += "</QueryPacket>";

To fire this query by the Query operation of the Search web service via SPServices the following code can be used:

        var title, url = "";

        $().SPServices({
            operation: "Query",
            queryXml: queryText,
            completefunc: function(xData, Status) {
                $(xData.responseXML).find("QueryResult").each(function() {
                    //let's see what the response looks like
                    $("#result").text($(this).text());
                });
            }
        });

       

#results is a div element with the id results to show the response of the query web service. The response format returned by the query web service for the Query operation is described by the Microsoft.Search.Response schema (http://msdn.microsoft.com/en-us/library/ms578335.aspx)      

The result of running the above example:

<ResponsePacket xmlns="urn:Microsoft.Search.Response">
  <Response>
    <Range>
      <StartAt>1</StartAt>
      <Count>1</Count>
      <TotalAvailable>1</TotalAvailable>
      <Results>
        <Document relevance="428" xmlns="urn:Microsoft.Search.Response.Document">
          <Title>Estonia</Title>
          <Action>
            <LinkUrl size="0" fileExt="aspx">http://your_url/Lists/Country/DispForm.aspx?ID=24</LinkUrl>
          </Action>
          <Description />
          <Date>2010-04-08T13:45:47+02:00</Date>
        </Document>
      </Results>
    </Range>
    <Status>SUCCESS</Status>
  </Response>
</ResponsePacket>

       

One result is returned here to show a simple and small piece of xml. The results element contains the actual result of the query, the document element contains a single content item in the search results.      

A query using SQL syntax:

var queryText = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>"
queryText += "<Query>"
queryText += "<Context>"
queryText += "<QueryText language='en-US' type='MSSQLFT'>"
queryText += "SELECT Title, Rank, Size, Description, Write, Path FROM portal..scope() WHERE CONTAINS ('Estonia') ORDER BY \"Rank\" DESC"
queryText += "</QueryText>"
queryText += "</Context>"
queryText += "</Query>"
queryText += "</QueryPacket>";

And the result:

<ResponsePacket xmlns="urn:Microsoft.Search.Response">
  <Response>
    <Range>
      <StartAt>1</StartAt>
      <Count>1</Count>
      <TotalAvailable>1</TotalAvailable>
      <Results>
        <Document xmlns="urn:Microsoft.Search.Response.Document">
          <Action>
            <LinkUrl fileExt="aspx">http://your_url/Lists/Country/DispForm.aspx?ID=24</LinkUrl>
          </Action>
          <Properties mlns="urn:Microsoft.Search.Response.Document.Document">
            <Property>
              <Name>TITLE</Name>
              <Type>String</Type>
              <Value>Estonia</Value>
            </Property>
            <Property>
              <Name>RANK</Name>
              <Type>Int64</Type>
              <Value>1000</Value>
            </Property>
            <Property>
              <Name>SIZE</Name>
              <Type>Int64</Type>
              <Value>0</Value>
            </Property>
            <Property>
              <Name>WRITE</Name>
              <Type>DateTime</Type>
              <Value>2010-04-08T13:45:47+02:00</Value>
            </Property>
            <Property>
              <Name>PATH</Name>
              <Type>String</Type>
              Value>http://your_url/Lists/Country/DispForm.aspx?ID=24</Value>
            </Property>
          </Properties>
        </Document>
      </Results>
    </Range>
    <Status>SUCCESS</Status>
  </Response>
</ResponsePacket>

The results of the keyword and SQL syntax are slightly different. Keep this in mind while traversing the xml and getting the title for instance.
By using the SQL syntax and getting the title in the resulting xml, the Property element is involved:

        $().SPServices({
            operation: "Query",
            queryXml: queryText,
            completefunc: function(xData, Status) {
                $(xData.responseXML).find("QueryResult").each(function() {
                    var x = $("<xml>" + $(this).text() + "</xml>");
                    //let's see what the response looks like
                    //$("#result").text($(this).text());

                    //traverse the xml to get the items
                    x.find("Document").each(function() {

                        //when using SQL syntax
                        url = $("Action>LinkUrl", $(this)).text();
                        $(this).find("Property").each(function() {
                            if ($("Name", $(this)).text() == "TITLE") {

                                title = $("Value", $(this)).text();
                            }
                        });
                        //end SQL syntax

                        $("#result").text("title: " + title + " - LinkUrl: " + url);
                    });
                });
            }

        });

By using the keyword syntax and getting the title, it’s just a direct child element of the Document element and this code can be used:      

        $().SPServices({
            operation: "Query",
            queryXml: queryText,
            completefunc: function(xData, Status) {
                $(xData.responseXML).find("QueryResult").each(function() {
                    var x = $("<xml>" + $(this).text() + "</xml>");
                    //let's see what the response looks like
                    //$("#result").text($(this).text());

                    //traverse the xml to get the items
                    x.find("Document").each(function() {
                        url = $("Action>LinkUrl", $(this)).text();
                        //when using keyword syntax
                        title = $("Title", $(this)).text();
                        //end keyword syntax
                        $("#result").text("title: " + title + " - LinkUrl: " + url);
                    });
                });
            }
        });

       

The examples described here give just one result for demonstration purposes. This can be seen at the elements count and totalavailable in the result xml. Count is the actual number of items returned, while totalavailable is the total number of results returned by the query web service. When getting real life results please mind the number of items returned by default is 10. Of course the query can be adjusted with a custom count:      

var queryText = "<QueryPacket xmlns='urn:Microsoft.Search.Query' Revision='1000'>"
queryText += "<Query>"
queryText += "<Range><Count>50</Count></Range>";
queryText += "<Context>"
queryText += "<QueryText language='en-US' type='STRING'>"
queryText += "Estonia";
queryText += "</QueryText>"
queryText += "</Context>"
queryText += "</Query>"
queryText += "</QueryPacket>";

      

To fully optimize the query to your needs please check out the Microsoft.Search.Query schema for a lot of useful options.

Simple Alert Me option on a SharePoint page with SPServices

On SharePoint lists you can receive email notifications when items change in a list, the ‘Alert me’ function in the Actions menu of a list.

In some occasions there are multiple lists where the data in it is interesting to monitor. For example values of currencies or commodities. When a webpart is used to display values of these lists to users it can be difficult for users to set the email notifications on one or more of these lists, because the user doesn’t (have to) know which lists are involved.To make it all a bit user friendly I’m going to give the user the possibility to easily add an email notification at the same page the webpart is displayed. The user can stay on one page to analyze some data and create a notification for the lists used (the source data lists) in the webpart.

The webpart (not specified further here) to analyze some data uses a subset of all the lists available at the SharePoint site. The lists used are listed in another list, from now on called BaseList. So the BaseList contains all the lists (static name) used as source data.
  

Content editors can easily add other lists with data to the SharePoint site for analyzing purposes. To use these lists for analyzing purposes in the webpart they can add (or remove) that list to the BaseList. The webpart uses all the lists specified in BaseList and the users can set notifications to these lists.

For displaying all the possible email notifications to set on the page, SPServices comes to the rescue.  

Because of the BaseList it is easy to determine which source data lists are involved. To set an email alert always the same default SharePoint page from the _layouts folder is used: SubNew.aspx with the listguid as parameter. If these two are combined, the solution is already there.  

First a function is created to retrieve all the lists from the BaseList:


        function GetAvailableLists(listName) {
            counter = 0;
            $().SPServices({
                operation: "GetListItems",
                listName: listName,
                async: false,
                CAMLViewFields: '<ViewFields><FieldRef/><FieldRef/></ViewFields>',
                completefunc: function(xData, Status) {
                    $(xData.responseXML).find("[nodeName= z:row]").each(function() {
                        GetListId($(this).attr('ows_NameOfList'), $(this).attr('ows_DisplayNameOfList'));
                        counter += 1;
                    });
                }
            });
        }

The ViewFields contains two columns of the BaseList, one for the static name of the list and one with a self made up display name. The display name will be used as text of the link generated below, the static name for retrieving the guid of the list.  

In the code above the function GetListId() is used:

        function GetListId(listName, displayName) {
            var id = "";

            $().SPServices({
                operation: "GetList",
                listName: listName,
                async: false,
                completefunc: function(xData, Status) {
                    id = $(xData.responseXML).find("List").attr("ID");
                    if (id != null) {
                        items[counter] = "<a href='/_layouts/SubNew.aspx?List=" + id + "&Source=" + returnToPage + "'>" + displayName + "</a>";
                    }
                }
            });
        }

This piece of code gets the guid of the list and formats the right url for the notification. The Source parameter here is set to a returnpage. It is convenient for the user to be redirected to page he was coming from otherwise he will be redirected to the AllItems.aspx page from the list where the notification has been set. The formatted url is stored in an array, because I have another function for displaying it nicely at the page, that I’m going to leave to your imagination.  

Copy all the code in a Content Editor webpart  and you’re done.
The result on the page is dependent on how you format it, but practically no more than a bunch of links, e.g.:
  

With the link set to:  

http://<your site>/_layouts/SubNew.aspx?List={1FAFC5F9-F8D8-4CB6-852E-5AD4DB12CB04}&Source=Default.aspx