Thursday, March 29, 2012

search as you type functionality for ASP.NET site using JQuery and ASMX web services

This post is for search as you type functionality (on demand loading) for ASP.NET site using JQuery and ASMX web services.

Approach:
  • Step 1: Handle textbox keydown event to get user input(‘keyup’ could have been easier choice but it has a problem, will tell u that later in this post)
  • Step 2: Send an AJAX hit to server to get search results corresponding to user input.
  • Step 3: Catch and display result on client side
  • Step 4: Separate handling for down/up key for traversing between search results, enter and tab keys for selecting specific search result and escape key to remove search result.
Cool things to look for:
  • CSS stuff (z-index, etc.) that give search results google search type floating feel (exaggerating a bit to keep your interest here ;)
  • AJAX calls to ASMX web service.
 
 Code blocks: (Download Code)

ASPX (Just a textbox to type and a div to hold results)  
< span style="color:White;">Search As You Type:
        < input id="txtTest" type="text" style="width: 400px;" />
        < div id="divPredictedResultsContainer" class="divPredictedResultsContainer" >


Jquery (Code to handle keydown event and make Ajax hits with entered text)
<script type="text/javascript">
        $(document).ready(function() {
            var selectedResultCounter = 1;
            $("#txtTest").keydown(function(event) {
                //check if key pressed is alphabet, a number or backspace
                if ((event.keyCode > 64 && event.keyCode < 91) || (event.keyCode > 47 && event.keyCode < 58) || (event.keyCode == 8)) { //handle alphabets, numbers and backspace
                    var currentValue = ''
                    if (event.keyCode == 8 && $('#txtTest').val().length > 0) {
                        currentValue = $('#txtTest').val().substring(0, $('#txtTest').val().length - 1);
                    } else {
                        currentValue = $('#txtTest').val() + String.fromCharCode(event.keyCode);
                    }

                    selectedResultCounter = 1;
                    $.ajax({
                        type: "POST",
                        url: "GetData.asmx/GetData",
                        data: "{'startsWith':'" + currentValue + "'}",
                        contentType: "application/json; charset=utf-8",
                        dataType: "json",
                        success: function(msg) {
                            $("#divPredictedResultsContainer").html(msg.d);
                            if (msg.d.indexOf("divPredictedResultStyle") > -1) {
                                $("#divPredictedResultsContainer").addClass("roundedCorners");
                            }
                        },
                        error: function(xhr, ajaxOptions, thrownError) {
                            alert(xhr.status);
                            alert(thrownError);
                        }
                    });
                } else if (event.keyCode == 40) {       //handle down key
                    var resultCount = $(".divPredictedResultStyle").size();
                    if (selectedResultCounter == resultCount + 1) {
                        selectedResultCounter = 1;
                        $("#divPredictedResult" + resultCount).removeClass("divPredictedResultSelected");
                    }
                    $("#divPredictedResult" + selectedResultCounter).addClass("divPredictedResultSelected");
                    $("#divPredictedResult" + (selectedResultCounter - 1)).removeClass("divPredictedResultSelected");
                    selectedResultCounter = selectedResultCounter + 1;

                } else if (event.keyCode == 38) {       //handle up key
                    selectedResultCounter = selectedResultCounter - 1;
                    var resultCount = $(".divPredictedResultStyle").size();
                    if (selectedResultCounter == 1) {
                        selectedResultCounter = resultCount + 1;
                        $("#divPredictedResult1").removeClass("divPredictedResultSelected");
                    }
                    $("#divPredictedResult" + (selectedResultCounter - 1)).addClass("divPredictedResultSelected");
                    $("#divPredictedResult" + selectedResultCounter).removeClass("divPredictedResultSelected");
                } else if (event.keyCode == 9 || event.keyCode == 13) {       //handle tab key and enter key
                    if (event.preventDefault) {                               //this is to select one of the records
                        event.preventDefault();                               //from search results
                        event.stopPropagation();
                    }
                    $('#txtTest').val($(".divPredictedResultSelected").html());
                    return false;
                } else if (event.keyCode == 27) {                           //handle escape key
                    $(".divPredictedResults").hide();                       //hide results div, if escape key is pressed
                    $("#divPredictedResultsContainer").removeClass("roundedCorners");
                }
            });
        });
       
    script>
Points to ponder (read these points only if u r not in hurry, else move to ASMX code block :)
      • ·         Here if I would have used keyup event in place of keydown, It wouldn’t have been possible to select result by pressing tab or enter key as in that case, event has already been occurred thus prevent default can’t work.
      • ·         Code in red (see above) is a known bug :( u can c it as coding exercise): This code block handles backspace key but assumes that user will press it end of text only and not from between. I have this problem because on keydown I don’t have final value of textbox, I just have the keycode of key pressed, so I am forced to assume that key pressed is at end only(I’ll update this post once I find the proper solution).
      • ·         Code in green is heart of this all, it sends request to an ASMX service and captures response coming from there.
      • ·         In code blocks that handle up/down key there is logic to keep selecting search result records in loop, means if down key is pressed and last result found was currently selected then automatically selection will get changed to first search result.
 
ASMX Service code (To serve results from server)
< WebMethod() > _
    Public Function GetData(ByVal startsWith As String) As String
        Dim strResults As New StringBuilder(String.Empty)
        Dim dtResults As DataTable = GetDataForIntellisense(startsWith)
        Dim counter As Integer = 1
        strResults.Append("< div class='divPredictedResults'>")
        If dtResults.Rows.Count > 0 Then
            For Each dr As DataRow In dtResults.Rows
                strResults.Append("< div class='divPredictedResultStyle' id='divPredictedResult" + counter.ToString() + "'>" + dr("code").ToString() + "")
                counter += 1
            Next
        Else
            strResults.Append("No results found.")
        End If
        strResults.Append("")
        Return strResults.ToString()
    End Function

There is nothing typical in above ASMX code, its just about fetching records from database and then generating html corresponding to records retreived. One thing worth noticing is that one div is created corresponding to each search result and they have been given ids in incremental order. Other thing is method ‘GetDataForIntellisense’, implementation of this is upon you. Current code expects this method to return a datable with a column named “code”.


CSS (last but not least)
In above css, class ‘divPredictedResultsContainer’ has two lesser known attributes, namely z-index and border-radius.
Z-index is to specify stack order of an element. It lets results to float above other contents and thus you don’t need to reserve space for search results and can show search results without disturbing other contents of site.
Border-radius property is for giving rounded corners effect to div. This might not work in older browsers.


Download sample code from here.

2 comments:

  1. Nice one. IS there C# version of this please?

    ReplyDelete
    Replies
    1. Hi Mutie,

      Below is C# version of that web-method

      [WebMethod()]
      public string GetData(string startsWith)
      {
      StringBuilder strResults = new StringBuilder(string.Empty);
      DataTable dtResults = GetDataForIntellisense(startsWith);
      int counter = 1;
      strResults.Append("< div class='divPredictedResults'>");
      if (dtResults.Rows.Count > 0) {
      foreach (DataRow dr in dtResults.Rows) {
      strResults.Append("< div class='divPredictedResultStyle' id='divPredictedResult" + counter.ToString() + "'>" + dr["code"].ToString() + "");
      counter += 1;
      }
      } else {
      strResults.Append("No results found.");
      }
      strResults.Append("");
      return strResults.ToString();
      }

      I use below tool to convert VB code to C#
      http://www.developerfusion.com/tools/convert/vb-to-csharp/

      Delete