Aboo Bolaky

{.NET, C#, Sitecore ...} Free your mind...

Bold style for matching results with AutoCompleteExtender - Implemented using a Trie

clock October 7, 2009 09:22 by author Aboo Bolaky

This article will focus on the implementation of the AjaxControlToolkit's AutoCompleteExtender into a simple ASP.NET application. What I'm trying to do here is pull the data from a database and use the AutoCompleteExtender to display the data as the user types characters in a textbox.

Server Implementation

Im not going to focus on how to retrieve the data (from a table of around 60,000 records). I'm going to use the simplistic SQLConnection/SQLCommand/SQLDataReader classes and passing sql command as plain text (as opposed to Stored Procedures). The key issue is how and what is the best way to store the data once being retrieved. Generally, Autocomplete comes in 2 flavours:-

1: Find words based on letters anywhere in a phrase (performing a LIKE '%ar%'  in SQL).

2: Find words that start with specific characters ( LIKE 'ar%' in SQL).

In the first instance, you're pretty much tied up to using the LIKE clause (be it in a stored proc or simple text). The determining factor here is how and when to cache the data since each character typed in invokes the webservice and queries the database. Each query will yield a different result set.

In the second instance (the focus of this article), the more you add characters to the keyword, you are, in fact, narrowing down the scope of search. This means that the data we initially load is deterministic and hence we can cache it somewhere. But the real deal here is :which  data structure do we hold our words in? List<>, DataTable, Linked List??

Trie 

You might have already figured this one out. Anyway, a trie is an ordered tree data structure (a.k.a prefix tree) that stores the information about the contents of each node in the path from the root to the node, rather than the node itself. What this means is that each part between the root and any leaf represents a key and the goal is to find the key by traversing the tree.

Implementation

Eyal Mey-Tal has implemented a Trie structure in C#.Head to the CodePlex site to download it.

WebMethod to retrieve data

   1:   [WebMethod]
   2:          public string[] GetNames(string prefixText, int count)
   3:          {
   4:              Trie trie = (Trie)Context.Cache["Trie"];
   5:               if (trie == null)
   6:               {
   7:                   trie = new Trie();     
   8:   
   9:                   using (SqlConnection con = new SqlConnection("server=(local);database=autocomplete;user id=xxx;Password=xxx;"))
  10:                   {
  11:                       SqlCommand cmd = new SqlCommand("select word from AutoCompleteData", con);
  12:                       cmd.CommandType = System.Data.CommandType.Text;
  13:                       con.Open();
  14:                       SqlDataReader dr = cmd.ExecuteReader();
  15:                       if (dr.HasRows)
  16:                       {
  17:                           while (dr.Read())
  18:                           {
  19:                              trie.Add(dr.GetString(0));
  20:                           }
  21:                           con.Close();
  22:                       }
  23:                   }
  24:               //simple caching here  
  25:               Context.Cache["Trie"] = trie;
  26:               }
  27:              List<string> list = trie.GetCompletionList(prefixText);
  28:              return list.Take(count).ToArray();
  29:          }

 

Setting up the AutoCompleteExtender was easy. To get the autocomplete working, I only had to modify a few of the extender's attributes (.e.g. CompletionSetCount, TargetControlID, ServicePath, ServiceMethod, DelimiterCharacters ..) and data was being returned as I typed in characters. However, making the extender look good was a different story.

Client-Side Implementation 

This is the bit where I'm open to suggestions. I'm no Javascript/JQuery expert but however, I finally got the extender working as I wanted it to. Characters relevant to the ones the user has typed become highlighted in the drop down list. It took me a while to dig this solution out. The javascript is awkward and needs to be refactored. Netherless, the solution works for IE and Firefox [ good enough for me :) ]

 

   1:  <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="AutoComplete.aspx.cs" Inherits="AutoComplete.AutoComplete" %>
   2:   
   3:  <%@ Register Assembly="AjaxControlToolkit" Namespace="AjaxControlToolkit" TagPrefix="cc1" %>
   4:   
   5:  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   6:   
   7:  <html xmlns="http://www.w3.org/1999/xhtml" >
   8:  <head runat="server">
   9:      <title></title>
  10:      
  11:      <style>
  12:     
  13:      .autocomplete_completionListElement 
  14:  {  
  15:      margin : 0px!important;
  16:      background-color : inherit;
  17:      color : windowtext;
  18:      border : buttonshadow;
  19:      border-width : 1px;
  20:      border-style : solid;
  21:      cursor : 'default';
  22:      overflow : auto;
  23:      height : 200px;
  24:      text-align : left; 
  25:      list-style-type :  none;
  26:      padding-left:1px;
  27:  }
  28:   
  29:  /* AutoComplete highlighted item */
  30:   
  31:  .autocomplete_highlightedListItem
  32:  {
  33:      background-color: #aaff90;
  34:      color: black;
  35:      padding: 1px;
  36:      cursor:hand;
  37:  }
  38:   
  39:  /* AutoComplete item */
  40:   
  41:  .autocomplete_listItem 
  42:  {
  43:      background-color : window;
  44:      color : windowtext;
  45:      padding:1px;
  46:  }
  47:      
  48:      </style>
  49:      
  50:      <script type="text/javascript">
  51:   
  52:         
  53:          function acePopulated(sender, e) {
  54:          
  55:          var behavior = $find('AutoCompleteEx');
  56:                      
  57:                      var target = behavior.get_completionList();
  58:                      if (behavior._currentPrefix != null)
  59:                      {
  60:                          var prefix = behavior._currentPrefix.toLowerCase();
  61:                          var i;
  62:                              for (i = 0; i < target.childNodes.length; i++)
  63:                              {
  64:                                  var sValue = target.childNodes[i].innerHTML.toLowerCase();
  65:                                  if (sValue.indexOf(prefix) != -1)
  66:                                  {
  67:                                   
  68:                                  var fstr = target.childNodes[i].innerHTML.substring(0, sValue.indexOf(prefix));
  69:                                  var pstr = target.childNodes[i].innerHTML.substring(fstr.length, fstr.length + prefix.length);
  70:                                  var estr = target.childNodes[i].innerHTML.substring(fstr.length + prefix.length, target.childNodes[i].innerHTML.length);
  71:                                  target.childNodes[i].innerHTML = "<div class='autocomplete-item'>" + fstr + '<B>' + pstr + '</B>' + estr + "</div>";
  72:                                  
  73:                                 
  74:                                  }
  75:                              }
  76:                      }
  77:                    
  78:              }
  79:          
  80:          
  81:       
  82:              function aceSelected(sender, e) 
  83:              {
  84:                  var value = e.get_value();
  85:                  if (!value) {
  86:                      if (e._item.parentElement && e._item.parentElement.tagName == "LI")
  87:                          value = e._item.parentElement.attributes["_value"].value;
  88:                      else if (e._item.parentElement && e._item.parentElement.parentElement.tagName == "LI")
  89:                          value = e._item.parentElement.parentElement.attributes["_value"].value;
  90:                      else if (e._item.parentNode && e._item.parentNode.tagName == "LI") 
  91:                          value = e._item.parentNode._value;
  92:                      else if (e._item.parentNode && e._item.parentNode.parentNode.tagName == "LI")
  93:                          value = e._item.parentNode.parentNode._value;
  94:                      else value = "";
  95:                  }
  96:                  var searchText = $get('<%=txtWord.ClientID %>').value;
  97:                  searchText = searchText.replace('null', '');
  98:                                  
  99:                  sender.get_element().value = searchText + value;
 100:                        }
 101:                                       
 102:   
 103:      </script>
 104:      
 105:  </head>
 106:  <body>
 107:      <form id="form1" runat="server">
 108:       <asp:ScriptManager ID="ScriptManager1" runat="server">
 109:       
 110:       </asp:ScriptManager>
 111:       <div style="text-align:center;margin-top:200px">
 112:     Search : <asp:TextBox ID="txtWord" runat="server" AutoComplete="off" Width="200px"   /> 
 113:           </div>
 114:       <cc1:AutoCompleteExtender
 115:                  runat="server" 
 116:                  BehaviorID="AutoCompleteEx"
 117:                  ID="autoComplete1" 
 118:                  TargetControlID="txtWord"
 119:                  ServicePath="MyService.asmx" 
 120:                  ServiceMethod="GetNames"
 121:                  MinimumPrefixLength="1" 
 122:                  CompletionInterval="1000"
 123:                  EnableCaching="true"
 124:                  CompletionSetCount="20"
 125:                  CompletionListCssClass="autocomplete_completionListElement" 
 126:                  CompletionListItemCssClass="autocomplete_listItem" 
 127:                  CompletionListHighlightedItemCssClass="autocomplete_highlightedListItem"
 128:                  OnClientPopulated="acePopulated"
 129:                  OnClientItemSelected="aceSelected"
 130:                  DelimiterCharacters=";"
 131:                  ShowOnlyCurrentWordInCompletionListItem="true" >
 132:              </cc1:AutoCompleteExtender>
 133:       
 134:      </form>
 135:  </body>
 136:  </html>

Result

 

 

Downloads

The source code (with database script in ~/App_Data (around 60,000 records )) can be downloaded below.

AutoCompleteVS2008Sln.zip (4.84 mb)

Enjoy ....

Currently rated 3.0 by 5 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5


Comments

Add comment


(Will show your Gravatar )  

  Country flag

biuquote
  • Comment
  • Preview
Loading



A b o u t M e

Annoying

Brilliant

Open and

Objective

in every way..
Only Human >>
 
"First learn computer science and all the theory.

Next develop a programming style.

Then forget all that and just hack." Carrette (1990)