Cloud

Using Pagination with Asynchronous Keyword Queries

Recently, I finished development on a SharePoint 2013 provider-hosted app that uses Keyword Query Language and the SharePoint Search API to run asynchronous queries against a site collection to grab all of the site collections under that domain name. What I found almost most troubling was figuring out a way not just to display these results, but to implement pagination while still taking advantage of asynchronous callbacks and the SharePoint Search API. Since I only want to grab a given number of results at a time against a collection of close to 1000 site collections, I decided to paginate using the RowLimit and StartRow properties in the Keyword Query builder. StartRow denotes what index to start at in a result set, and RowLimit specifies how many results to return for each query called. These, along with a page index, are dynamically set each time a query is called, all based on if the Previous or Next button is called. I have posted the main chunks of code from my solution to demonstrate incorporating pagination with the jQuery $.ajax() calls.

When a New Search is Executed

    function searchButtonClickHandler() {
        resetSearch();
        userSearchVal = $('#searchBox').val();
        var data = JSON.stringify({ searchParam: userSearchVal });
        $.ajax({
            url: "/Search/noRedirectExecuteSearch",
            contentType: "application/json",
            type: "POST",
            data: data,
            success: function (searchData) {
                var searchResultsObject = JSON.parse(searchData);
                appendResultsToHtml(searchResultsObject, '#resultsTableBody');
            },
            error: function () { }
        });
    }

When the “Previous” Button is Clicked

    function prevButtonClickHandler() {
        $('#nextButton').removeAttr("disabled");
        adjustSearchVariables(false);
        var data = JSON.stringify({ row: startRow, page: pageIndex, searchParam: userSearchVal });
        $.ajax({
            url: "/Search/paginateButtonClick",
            contentType: "application/json",
            type: "POST",
            data: data,
            success: function (searchData) {
                var searchResultsObject = JSON.parse(searchData);
                appendResultsToHtml(searchResultsObject, '#resultsTableBody');
                checkForFirstPage();
            },
            error: function () { alert("Error from previous button"); }
        });
    }
Microsoft - The Essential Guide to Microsoft Teams End-User Engagement
The Essential Guide to Microsoft Teams End-User Engagement

We take you through 10 best practices, considerations, and suggestions that can enrich your Microsoft Teams deployment and ensure both end-user adoption and engagement.

Get the Guide

When the “Next” Button is Clicked

    function nextButtonClickHandler() {
        $('#prevButton').removeAttr("disabled");
        adjustSearchVariables(true);
        var data = JSON.stringify({ row: startRow, page: pageIndex, searchParam: userSearchVal });
        $.ajax({
            url: "/Search/paginateButtonClick",
            contentType: "application/json",
            type: "POST",
            data: data,
            success: function (searchData) {
                var searchResultsObject = JSON.parse(searchData);
                if (searchResultsObject.length === 0) {
                    $('#nextButton').attr("disabled", "disabled");
                } else {
                    appendResultsToHtml(searchResultsObject, '#resultsTableBody');
                    checkForNotEnoughResults(searchResultsObject);
                }
            },
            error: function () { alert("Error from next button"); }
        });
    }

Client-Side Utilities

    function resetSearch() {
        startRow = 0;
        pageIndex = 1;
        resetSearchButtons();
    }
    function resetSearchButtons() {
        if ($('#prevButton').attr("disabled") === undefined) {
            $('#prevButton').attr("disabled", "disabled");
        }
        if ($('#nextButton').attr("disabled", "disabled")) {
            $('#nextButton').removeAttr("disabled");
        }
    }
    function adjustSearchVariables(increment) {
        if (increment) {
            pageIndex++;
            startRow += startRowIncrementer;
        } else {
            pageIndex--;
            startRow -= startRowIncrementer;
        }
    }
    function checkForFirstPage() {
        if (pageIndex === 1) {
            $('#prevButton').attr("disabled", "disabled");
        }
    }
    function checkForNotEnoughResults(searchResultsObject) {
        if (searchResultsObject.length < rowLimit) {
            $('#nextButton').attr("disabled", "disabled");
        }
    }

Server-Side Query Code (note: the only server-side code shown is the part that involves setting the Keyword Query properties)

public KeywordQuery setKeywordQueryFields(KeywordQuery query, string qText)
{
    query.QueryText = qText;
    query.SelectProperties.Add("Title");
    query.SelectProperties.Add("Path");
    query.SelectProperties.Add("WebTemplate");
    query.RowLimit = rowLimit;
    query.StartRow = startIndex;
    query.EnableSorting = true;
    query.TrimDuplicates = false;
    return query;
}

Overall, this is fairly simple code. But some items to note:

  • The page index is 1-based, not 0-based
  • The row limit stays constant the entire time, while start row and page index are incremented/decremented based on the button clicked.
  • The row limit is always one more than the specified number of results to display. While this may seem odd, there is good reason for this. Let’s say you are displaying 10 results at a time and you have 90 results total from a given query. On page 1, the query returns 11 results; however, you only display 10 in your results table. The same goes for pages 2-8. By knowing that on page 9 you only get 10 results back from your asynchronous call, you can determine there are no more results from that query, i.e. you are on the last page.
About the Author

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up