Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

AJAX WAS Here - Part 3 : Auto Complete TextBox

0.00/5 (No votes)
2 May 2005 4  
A custom AJAX - ASP.NET control.

Introduction

If you�ve been following along with this article series then you know in Part 1 we talked briefly about Asynchronous JavaScript And XML or AJAX for short. We created the CallBackObject to help simplify the callback initiation process. In Part 2 we looked at integrating the CallBackObject with ASP.NET controls and events. Now, in Part 3, we are going to put all that knowledge to good use and create a custom ASP.NET control using everything we�ve learned so far. That control, as you may have guessed, is the Auto Complete TextBox (ACTB).

Before we get to the good stuff I want to say there is a lot of room for improvement for this control. Functionality, design-time integration, and ease-of-use can all be drastically improved. I leave that to you!

The Good Stuff

Before we talk about creating the control, I want to show you just how easy it is to use the control. We�ll start with Listings 1 and 2 (Actb.aspx and Actb.aspx.cs). After you calm down from your overwhelming excitement, we will examine the JavaScript portion of the ACTB Listing 3 (AutoCompleteTextBox.js) and the ASP.NET portion Listing 4 (AutoCompleteTextBox.cs).

Actb.aspx

This page is pretty straightforward. We are setting up some input fields for the user. Most of you will probably note that not all countries use a Zip code. I put this in as a demonstration of how you might customize the results of the Auto Complete TextBox based on values from other fields. (more on this later.) To use our custom control we have to register the assembly with the page and give it a tag prefix. I�ve modestly used my initials as the tag prefix but you can choose any prefix you desire. We also specify the namespace and assembly name where the control(s) can be found.

<%@ Register TagPrefix="wcp" Namespace="WCPierce.Web.UI.WebControls" 
           Assembly="WCPierce.Web" %>

After that, the only interesting piece of HTML is the actual declaration of the ACTB.

<wcp:AutoCompleteTextBox runat="server" id="actbCountry" 
  OnTextChanged="actbCountry_TextChanged" ListItemCssClass="ListItem"
  ListItemHoverCssClass="ListItemHover" />

The declaration is nearly identical to that of a normal ASP TextBox. The only added attributes are the ListItemCssClass and ListItemHoverCssClass used to beautify our dropdown list. These CSS classes are defined in the external style sheet AutoCompleteTextBox.css.

Actb.aspx.cs

Things begin to get slightly more interesting when we examine the code-behind file. The first thing to note is the using statements at the top of the code:

. . .
using Microsoft.ApplicationBlocks.Data;

using WCPierce.Web;
using WCPierce.Web.UI.WebControls;

The Data Application Block provided by Microsoft is used to simplify some database access code. The other two using directives give us access to the Auto Complete TextBox and CallBackHelper classes.

Note the actbCountry_TextChanged event that we referenced in our HTML. Everything in this function is wrapped in a try/catch block and any error is returned to the client by the use of CallBackHelper.HandleError.

try
{
  //. . . 

}
catch(Exception ex)
{
  CallBackHelper.HandleError( ex );
}

This is important because errors that occur during a Call Back can be very difficult to debug (trust me on this). With this code in place and the proper JavaScript, it makes debugging errors a breeze.

The first step is to examine the contents of the txtZip entry field. If there is a value in there, actbCountry will default to �United States� (apologies to other countries that use Zip codes).

if( txtZip.Text.Length > 0 )
{
  CallBackHelper.Write( "United States" );
}

For the next part of the code it is important to remember that this event is firing every time the user presses a key while in the ACTB. So, if the Zip field is empty what happens is we take what the user has currently entered into the Country field, we�ll use �U� for example, and we search our database for every country that begins with the letter �U�.

AutoCompleteTextBox actb = s as AutoCompleteTextBox;
string str = String.Format("SELECT [Text] 
                            FROM Lists 
                            WHERE ListName='Country' 
                            AND [Text] LIKE '{0}%' 
                            ORDER BY [Text]", actb.Text);

All of the matching countries (Uganda, Ukraine, Uruguay, etc.) are returned in a SqlDataReader. We then set the DataSource property of the Auto Complete TextBox equal to the SqlDataReader. This process is identical to that used for binding to a DropDownList or a DataGrid.

SqlDataReader sdr = SqlHelper.ExecuteReader(@"Server=(local);
                                              Database=DotNetNuke;
                                              Integrated Security=SSPI;", 
                                              CommandType.Text, str);
actb.DataSource = sdr;
actb.DataTextField = "Text";
actb.BindData();

Savvy readers will notice the only difference here is the call to BindData(). Normally with DataBound controls, you would call DataBind(). Unfortunately, I wasn�t able to make this work properly. The reason is a bit complicated but you can see why if you attempt to use an Auto Complete TextBox in a DataGrid with ViewState disabled.

That�s it! I�ve encapsulated all the hard work in the ACTB control. You can now use it just like any other data bindable control. Surf over to this site to see a demo of the ACTB in action. Leave the Zip blank, and enter a letter into the Country box and viola, you should see a drop down list with matches as well as the auto completion of the first country in the list. Sweet!

AutoCompleteTextBox.js

Before we start discussing this little gem, I need to recognize the great individuals who helped me make this control a reality:

The concepts from those two articles plus the CallBackObject yielded the ACTB. There is a lot of code in this section and rather than go over the portions covered in the two articles above, I�m only going to cover the new stuff.

AutoCompleteTextBox.prototype.TextBox_KeyUp = function(oEvent)
{
  var iKeyCode = oEvent.keyCode;

  if( iKeyCode == 8 )
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    return;
  }
  else if( iKeyCode == 16 || iKeyCode == 20 )
  {
    this.DoAutoSuggest = true;
  }
  else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || 
                         (iKeyCode >= 112 && iKeyCode <= 123)) 
  {
    return;
  }
  else
  {
    this.DoAutoSuggest = true;
  }
  
  var txt = this.TextBox.value;
  if( txt.length > 0 )
  {
    this.Cbo.DoCallBack(this.TextBox.name, txt);
  }
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    this.Cbo.AbortCallBack();
  }
}

Whenever a key is pressed in the ACTB, this event fires. We do some checking to see what key was pressed and if there is any data in the TextBox. If so, we make the call to Cbo.DoCallBack. As you know from Part 1, this is what initiates the server side request.

When the server side request completes, our method AutoCompleteTextBox.prototype.Cbo_Complete is executed, and we process the results.

AutoCompleteTextBox.prototype.Cbo_Complete = function(responseText, responseXML)
{ 
  while ( this.Div.hasChildNodes() )
    this.Div.removeChild(this.Div.firstChild);
            
  // get all the matching strings from the server response

  var aStr = responseText.split('\n');
         
  // add each string to the popup-div

  var i, n = aStr.length;
 
  if( n > 0 )
  {
    for ( i = 0; i < n; i++ )
    {
      var oDiv = document.createElement('div');
      this.Div.appendChild(oDiv);
      try
      {
        oDiv.innerHTML    = aStr[i];
      }
      catch(e)
      {
        this.Cbo_Error('405','Error','Text returned from Call Back was invalid');
        return;
      }
      oDiv.noWrap       = true;
      oDiv.style.width  = '100%';
      oDiv.className    = this.ListItemClass;
      oDiv.onmousedown  = AutoCompleteTextBox.prototype.Div_MouseDown;
      oDiv.onmouseover  = AutoCompleteTextBox.prototype.Div_MouseOver;
      oDiv.onmouseout   = AutoCompleteTextBox.prototype.Div_MouseOut;
      oDiv.AutoCompleteTextBox = this;            
    }
    this.Div.style.display = 'block';
      
    if( this.DoAutoSuggest == true )
      this.AutoSuggest( aStr );
  }  
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display='none';
  }
}

The data returned from the server is a new-line delimited list of country names. After we remove any current entries from the drop down list, we split the return value into an array of country names. Then, we loop through the array and add each county to the drop down list, assign some events, and set any styles. The populated list is then displayed to the user, and we call the AutoSuggest method, to perform the type-ahead feature and put the first entry from the list in the TextBox and select the proper characters.

I apologize if I breezed over this portion but the articles referenced at the beginning of this section explain the remainder of the code.

AutoCompleteTextBox.cs

I tried to be thorough in my commenting of the control itself. We will cover the highlights of the control in lieu of taking you through line by line (plus my fingers are getting tired).

The first thing to note is that AutoComplete TextBox inherits from the built-in ASP TextBox:

public class AutoCompleteTextBox : System.Web.UI.WebControls.TextBox

This saves us a tremendous amount of work. ACTB has a number of additional properties used to specify some CSS information and the path to the JavaScript file to be used for the client side portion of things. The Render method is where the action begins:

protected override void Render(HtmlTextWriter output)
{
  string uId = this.UniqueID;
  string newUid = uId.Replace(":", "_");
  string divId = newUid + "_Div";
  string jsId = newUid + "_Js";

  StringBuilder acScript = new StringBuilder();
  acScript.Append("<script type='\"text/javascript\"'>");
  acScript.AppendFormat("var {0} = new AutoCompleteTextBox('{1}','{2}');
                         {0}.ListItemClass='{3}';
                         {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                         this.ListItemCssClass, this.ListItemHoverCssClass);
  acScript.Append("</script>");

  Page.RegisterStartupScript(newUid, acScript.ToString());

  base.Attributes.Add("AutoComplete", "False");
  base.Render(output);
  output.Write(String.Format("<DIV id={0}></DIV>", divId));
}

In order for the client side JavaScript to work, it needs a reference to the TextBox that will be acting as the ACTB, as well as the <DIV> tag that will act as the drop-down portion of the control. ASP.NET controls have an ID, which you generally use in your code, and a UniqueID, which is a unique identifier for the control at the page level. Sometimes the ControlID and the UniqueID are the same, but you start running into trouble when using controls in User Controls, Server Controls, or DataList controls. So, we grab a reference to our ACTB UniqueID and create ID�s for our <DIV> tag and for use in our dynamic JavaScript code.

  string uId = this.UniqueID;
  string newUid = uId.Replace(":", "_");
  string divId = newUid + "_Div";
  string jsId = newUid + "_Js";

Next we dynamically create the JavaScript needed on the client side to create the Auto Complete TextBox. We initialize the JavaScript object with CSS properties and have ASP.NET put it in the proper place on the page for us with RegisterStartupScript.

  StringBuilder acScript = new StringBuilder();
  acScript.Append("<script type='\"text/javascript\"'>");
  acScript.AppendFormat("var {0} = new AutoCompleteTextBox('{1}','{2}');
                         {0}.ListItemClass='{3}';
                         {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                         this.ListItemCssClass, this.ListItemHoverCssClass);
  acScript.Append("</script>");

  Page.RegisterStartupScript(newUid, acScript.ToString());

Finally, we have our base class (TextBox) render itself, then render the <DIV> tag needed for the drop-down feature:

  base.Attributes.Add("AutoComplete", "False");
  base.Render(output);
  output.Write(String.Format("<DIV id={0}></DIV>", divId));

We also override the OnTextChanged event. We only want this event to fire during a CallBack and only if our control is the target of the event:

protected override void OnTextChanged(EventArgs e)
{  
  if( Page.Request.Params["__EVENTTARGET"] == 
         this.UniqueID && CallBackHelper.IsCallBack )
  {
    base.OnTextChanged( e );
  }
}

The last few things to address are AutoPostBack and DataBind. Obviously if AutoPostBack is enabled, we defeat the purpose of AJAX. The DataBind method had to be overridden to eliminate its functionality. If one of you figure out how to make it work, please let me know. For now, developers have to call BindData instead.

public override void DataBind()
{
  // Do Nothing

}

public override bool AutoPostBack
{
  get { return false; }
}

public virtual void BindData()
{
  this.OnDataBinding(EventArgs.Empty);
}

Conclusion

Finally the fruits of our labor have delivered a neat little control that brings some new functionality to the ASP.NET websites. There is a lot of code to digest and I encourage you to download the source and play around. I hope I laid the ground work and planted some ideas for future development. There is a lot of potential for this technology and it is finally getting the attention it deserves.

Listing 1 � Actb.aspx

<%@ Register TagPrefix="wcp" Namespace="WCPierce.Web.UI.WebControls" 
Assembly="WCPierce.Web" %>
<%@ Page language="c#" Codebehind="Actb.aspx.cs" AutoEventWireup="false" 
Inherits="TestWeb.Actb" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
  <HEAD>
    <title>AutoCompleteTextBox Example</title>
    <meta name=vs_defaultClientScript content="JavaScript">
    <meta name=vs_targetSchema 
          content="http://schemas.microsoft.com/intellisense/ie5">
    <link href="Css/AutoCompleteTextBox.css" 
          type="text/css" rel="stylesheet" />
  </HEAD>
  <body>
    <form id="Form1" method="post" runat="server">
      <table>
        <tr>
          <td colspan="2">Please enter your informaiton below.</td>
        </tr>
        <tr>
          <td>Name:</td>
          <td><asp:TextBox Runat="server" ID="txtName" /></td>
        </tr>
        <tr>
          <td>Zip:</td>
          <td><asp:TextBox Runat="server" ID="txtZip" /></td>
        </tr>
        <tr>
          <td>Country:</td>
          <td>
            <wcp:AutoCompleteTextBox runat="server" id="actbCountry" 
              OnTextChanged="actbCountry_TextChanged" 
              ListItemCssClass="ListItem"
              ListItemHoverCssClass="ListItemHover" />
          </td>
        </tr>
      </table>
    </form>
  </body>
</HTML>

Listing 2 � Actb.aspx.cs

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

using Microsoft.ApplicationBlocks.Data;

using WCPierce.Web;
using WCPierce.Web.UI.WebControls;

namespace TestWeb
{
  public class Actb : System.Web.UI.Page
  {
    protected System.Web.UI.WebControls.TextBox txtName;
    protected WCPierce.Web.UI.WebControls.AutoCompleteTextBox actbCountry;
    protected System.Web.UI.WebControls.TextBox txtZip;
  
    private void Page_Load(object sender, System.EventArgs e) { }

    protected void actbCountry_TextChanged(object s, EventArgs e)
    {
      try
      {
        if( txtZip.Text.Length > 0 )
        {
          CallBackHelper.Write( "United States" );
        }
        else
        {
          AutoCompleteTextBox actb = s as AutoCompleteTextBox;
          string str = String.Format("SELECT [Text] 
                                      FROM Lists 
                                      WHERE ListName='Country' 
                                      AND [Text] LIKE '{0}%' 
                                      ORDER BY [Text]", actb.Text);
          SqlDataReader sdr = SqlHelper.ExecuteReader(@"Server=(local);
                                             Database=DotNetNuke;
                                             Integrated Security=SSPI;", 
                                             CommandType.Text, str);
          actb.DataSource = sdr;
          actb.DataTextField = "Text";
          actb.BindData();
        }
      }
      catch(Exception ex)
      {
        CallBackHelper.HandleError( ex );
      }
    }

    #region Web Form Designer generated code
  }
}

Listing 3 � AutoCompleteTextBox.js

function AutoCompleteTextBox(TextBoxId, DivId, DivClass)
{
  // initialize member variables

  var oThis = this;
  var oText = document.getElementById(TextBoxId);
  var oDiv  = document.getElementById(DivId);
  this.TextBox = oText;
  this.Div = oDiv;
    
  // CallBackObject + Event Handlers

  this.Cbo = new CallBackObject();
  this.Cbo.OnComplete = function(responseText,responseXML)
                      {oThis.Cbo_Complete(responseText,responseXML);};
  this.Cbo.OnError = function(status,statusText,responseText)
                      {oThis.Cbo_Error(status,statusText,responseText);};
            
  // attach handlers to the TextBox

  oText.AutoCompleteTextBox = this;
  oText.onkeyup = AutoCompleteTextBox.prototype.OnKeyUp;
  oText.onblur  = AutoCompleteTextBox.prototype.OnBlur;
 
  // align the drop down div

  var c = GetCoords(oText);
  var n = oText.style.pixelHeight;
  if( !n )
  { 
    n = 25;
  }
  else
  {
    n += 2;
  }
  oDiv.style.left = c.x;
  oDiv.style.top = c.y + n;
  oDiv.style.display = 'none';
  oDiv.style.position = 'absolute';
  
  // Set some default styles

  if( DivClass )
    oDiv.className = DivClass;
  else
  {
    oDiv.style.border = '1';
    oDiv.style.borderColor = 'black';
    oDiv.style.borderStyle = 'solid';
    oDiv.style.backgroundColor = 'white';
    oDiv.style.padding = '2';
  }
}

AutoCompleteTextBox.prototype.DoAutoSuggest = false;

AutoCompleteTextBox.prototype.ListItemClass = '';

AutoCompleteTextBox.prototype.ListItemHoverClass = '';

// TextBox OnBlur

AutoCompleteTextBox.prototype.OnBlur = function()
{
  this.AutoCompleteTextBox.TextBox_Blur();
}

AutoCompleteTextBox.prototype.TextBox_Blur = function()
{
  this.Div.style.display='none';
}

// TextBox OnKeyUp

AutoCompleteTextBox.prototype.OnKeyUp = function(oEvent)
{
  //check for the proper location of the event object

  if (!oEvent) 
  {
    oEvent = window.event;
  }    
  this.AutoCompleteTextBox.TextBox_KeyUp(oEvent);      
}

AutoCompleteTextBox.prototype.TextBox_KeyUp = function(oEvent)
{
  var iKeyCode = oEvent.keyCode;

  if( iKeyCode == 8 )
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    return;
  }
  else if( iKeyCode == 16 || iKeyCode == 20 )
  {
    this.DoAutoSuggest = true;
  }
  else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || 
          (iKeyCode >= 112 && iKeyCode <= 123)) 
  {
    return;
  }
  else
  {
    this.DoAutoSuggest = true;
  }
  
  var txt = this.TextBox.value;
  if( txt.length > 0 )
  {
    this.Cbo.DoCallBack(this.TextBox.name, txt);
  }
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    this.Cbo.AbortCallBack();
  }
}

AutoCompleteTextBox.prototype.Cbo_Complete = 
                      function(responseText, responseXML)
{ 
  while ( this.Div.hasChildNodes() )
    this.Div.removeChild(this.Div.firstChild);
            
  // get all the matching strings from the server response

  var aStr = responseText.split('\n');
         
  // add each string to the popup-div

  var i, n = aStr.length;
 
  if( n > 0 )
  {
    for ( i = 0; i < n; i++ )
    {
      var oDiv = document.createElement('div');
      this.Div.appendChild(oDiv);
      try
      {
        oDiv.innerHTML    = aStr[i];
      }
      catch(e)
      {
        this.Cbo_Error('405','Error', 
                'Text returned from Call Back was invalid');
        return;
      }
      oDiv.noWrap       = true;
      oDiv.style.width  = '100%';
      oDiv.className    = this.ListItemClass;
      oDiv.onmousedown  = AutoCompleteTextBox.prototype.Div_MouseDown;
      oDiv.onmouseover  = AutoCompleteTextBox.prototype.Div_MouseOver;
      oDiv.onmouseout   = AutoCompleteTextBox.prototype.Div_MouseOut;
      oDiv.AutoCompleteTextBox = this;            
    }
    this.Div.style.display = 'block';
      
    if( this.DoAutoSuggest == true )
      this.AutoSuggest( aStr );
  }  
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display='none';
  }
}

AutoCompleteTextBox.prototype.Cbo_Error = 
                    function(status, statusText, responseText)
{
  alert('CallBackObject Error: status=' + status + '\nstatusText=' + 
                                  statusText + '\n' + responseText);
}

AutoCompleteTextBox.prototype.Div_MouseDown = function()
{
  this.AutoCompleteTextBox.TextBox.value = this.innerHTML;
}

AutoCompleteTextBox.prototype.Div_MouseOver = function()
{
  if( this.AutoCompleteTextBox.ListItemHoverClass.length > 0 )
    this.className = this.AutoCompleteTextBox.ListItemHoverClass;
  else
  {
    this.style.backgroundColor = 'black';
    this.style.color = 'white';
  }
}

AutoCompleteTextBox.prototype.Div_MouseOut = function()
{
  if( this.AutoCompleteTextBox.ListItemClass.length > 0 )
    this.className = this.AutoCompleteTextBox.ListItemClass;
  else
  {
    this.style.backgroundColor = 'white';
    this.style.color = 'black';
  }
}

AutoCompleteTextBox.prototype.AutoSuggest = 
                             function(aSuggestions /*:array*/) 
{
  if (aSuggestions.length > 0) 
  {
    this.TypeAhead(aSuggestions[0]);
  }
}

AutoCompleteTextBox.prototype.TypeAhead = 
                            function( sSuggestion /*:string*/)
{
  if( this.TextBox.createTextRange || this.TextBox.setSelectionRange)
  {
    var iLen = this.TextBox.value.length; 
    this.TextBox.value = sSuggestion; 
    this.SelectRange(iLen, sSuggestion.length);
  }
}

AutoCompleteTextBox.prototype.SelectRange = 
                         function (iStart /*:int*/, iLength /*:int*/) 
{
  //use text ranges for Internet Explorer

  if (this.TextBox.createTextRange) 
  {
    var oRange = this.TextBox.createTextRange(); 
    oRange.moveStart("character", iStart); 
    oRange.moveEnd("character", iLength - this.TextBox.value.length);      
    oRange.select();
   
  //use setSelectionRange() for Mozilla

  } 
  else if (this.TextBox.setSelectionRange) 
  {
      this.TextBox.setSelectionRange(iStart, iLength);
  }     

  //set focus back to the textbox

  this.TextBox.focus();      
}
             
function GetCoords(obj /*:object*/) 
{   
  var newObj = new Object();
  newObj.x = obj.offsetLeft;
  newObj.y = obj.offsetTop;
  theParent = obj.offsetParent;
  while(theParent != null)
  {
    newObj.y += theParent.offsetTop;
    newObj.x += theParent.offsetLeft;
    theParent = theParent.offsetParent;
  }
  
  return newObj;
}

Listing 4 � AutoCompleteTextBox.cs

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

using WCPierce.Web.UI;

[assembly:TagPrefix("WCPierce.Web.UI.WebControls", "wcp")]
namespace WCPierce.Web.UI.WebControls
{
  /// <SUMMARY>

  /// AutoCompleteTextBox is similar to the WinForm 

  /// ComboBox control.  As the 

  /// user types into the box, the enter is 

  /// "auto-completed" based on values 

  /// databound to the TextBox by the developer.

  /// </SUMMARY>

  [DefaultProperty("Text"), 
       ToolboxData("<{0}:AutoCompleteTextBox runat="server">")]
  public class AutoCompleteTextBox : System.Web.UI.WebControls.TextBox
  {
    #region Member Variables

    /// <SUMMARY>

    /// The (relative) path to the AutoCompleteTextBox JavaScript file.

    /// </SUMMARY>

    private string _scriptPath = string.Empty;

    /// <SUMMARY>

    /// For using databinding with your AutoCompleteTextBox

    /// </SUMMARY>

    private object _dataSource = null;

    /// <SUMMARY>

    /// Data returned to the client is in 

    /// the form of "entry"-newline-

    /// "entry"-newline...If you wanted to get 

    /// cute, we could return in an XML

    ///  format.

    /// </SUMMARY>

    private static readonly string _FormatString = "{0}\n";

    /// <SUMMARY>

    /// If a ScriptPath isn't specified, check the 

    /// web.config file for the 

    /// following key.

    /// </SUMMARY>

    private static readonly string _ScriptPath = 
                           "AutoCompleteTextBox.ScriptPath";

    /// <SUMMARY>

    /// CSS Class name for the list item of the dropdownlist.

    /// </SUMMARY>

    private string _listItemCssClass = string.Empty;

    /// <SUMMARY>

    /// CSS Class name for the "hover" effect of 

    /// the list item of the dropdownlist.

    /// </SUMMARY>

    private string _listItemHoverCssClass = string.Empty;

    #endregion

    #region Public Properties

    /// <SUMMARY>

    /// The path to the AutoComplete.js file.  

    /// If you leave it blank, it will

    /// automatically look in the web.config 

    /// for the value under the key

    /// "AutoCompleteTextBox.ScriptPath". 

    /// Should be a path relative to the 

    /// application root i.e. "~\scripts\AutoCompleteTextBox.js".

    /// </SUMMARY>

    public string ScriptPath
    {
      get 
      { 
        if( _scriptPath != string.Empty )
          return ResolveUrl(_scriptPath); 

        try
        {
          return 
            ResolveUrl(System.Configuration.ConfigurationSettings.AppSettings[
                                             AutoCompleteTextBox._ScriptPath]);
        }
        catch 
        { 
          return null;
        }
      }
      set { _scriptPath = value; }
    }

    /// <SUMMARY>

    /// CSS Class name for the list item of the dropdownlist.

    /// </SUMMARY>

    public string ListItemCssClass
    {
      get { return _listItemCssClass; }
      set { _listItemCssClass = value; }
    }

    /// <SUMMARY>

    /// CSS Class name for the "hover" effect of the 

    /// list item of the dropdownlist.

    /// </SUMMARY>

    public string ListItemHoverCssClass
    {
      get { return _listItemHoverCssClass; }
      set { _listItemHoverCssClass = value; }
    }

    /// <SUMMARY>

    /// For use with databinding.

    /// </SUMMARY>

    true), 
      DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
     DefaultValue((string) null)>
    public virtual object DataSource
    {
      get
      {
        return _dataSource;
      }
      set
      {
        if (((value != null) && !(value is IListSource)) && 
                                !(value is IEnumerable))
        {
          throw new ArgumentException("Invalid_DataSource_Type: " + 
                                                      this.ID);
        }
        _dataSource = value;
      }
    }

    /// <SUMMARY>

    /// For use with databinding.

    /// </SUMMARY>

    public virtual string DataTextField
    {
      get 
      { 
        object o = this.ViewState["DataTextField"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataTextField"] = value;
      }
    }

    /// <SUMMARY>

    /// For use with databinding.

    /// </SUMMARY>

    public virtual string DataTextFormatString
    {
      get
      {
        object o = this.ViewState["DataTextFormatString"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataTextFormatString"] = value;
      }
    }

    /// <SUMMARY>

    /// For use with databinding.

    /// </SUMMARY>

    [DefaultValue("")]
    public virtual string DataMember
    {
      get
      {
        object o = this.ViewState["DataMember"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataMember"] = value;
      }
    }

    #endregion

    #region Overrides

    /// <SUMMARY>

    /// Render this control to the output parameter specified.

    /// </SUMMARY>

    /// <PARAM name="output">The HTML writer 

    /// to write out to.</PARAM>

    protected override void Render(HtmlTextWriter output)
    {
      string uId = this.UniqueID;
      string newUid = uId.Replace(":", "_");
      string divId = newUid + "_Div";
      string jsId = newUid + "_Js";

      StringBuilder acScript = new StringBuilder();
      acScript.Append("<script type='\"text/javascript\"'>");
      acScript.AppendFormat("var {0} = 
                  new AutoCompleteTextBox('{1}','{2}');
                  {0}.ListItemClass='{3}';
                  {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                  this.ListItemCssClass, this.ListItemHoverCssClass);
      acScript.Append("</script>");

      Page.RegisterStartupScript(newUid, acScript.ToString());

      base.Attributes.Add("AutoComplete", "False");
      base.Render(output);
      output.Write(String.Format("<DIV id={0}></DIV>", divId));
    }

    /// <SUMMARY>

    /// Register our common scripts and do default PreRendering.

    /// </SUMMARY>

    /// <PARAM name="e"></PARAM>

    protected override void OnPreRender(EventArgs e)
    {
      this._RegisterCommonScripts();
      base.OnPreRender(e);
    }

    /// <SUMMARY>

    /// Only fire the OnTextChanged event if 

    /// this control is the target and it

    /// is a Call Back

    /// </SUMMARY>

    /// <PARAM name="e"></PARAM>

    protected override void OnTextChanged(EventArgs e)
    {  
      if( Page.Request.Params["__EVENTTARGET"] == 
          this.UniqueID && CallBackHelper.IsCallBack )
      {
        base.OnTextChanged( e );
      }
    }

    /// <SUMMARY>

    /// The original idea was to have the 

    /// Auto Complete Text Box behave like a

    /// normal Data Bindable control.  

    /// But, alas, I couldn't figure that out.

    /// Thank you M$ for the databinding code.

    /// </SUMMARY>

    /// <PARAM name="e"></PARAM>

    protected override void OnDataBinding(EventArgs e)
    {
      base.OnDataBinding(e);

      IEnumerable ie = 
          DataSourceHelper.GetResolvedDataSource(this.DataSource, 
                                                this.DataMember);
      StringBuilder sb = new StringBuilder();

      if( ie != null )
      {
        bool useTextField = false;
        bool useFormatString = false;
        string textField = DataTextField;
        string formatString = DataTextFormatString;
        if( textField.Length != 0 )
        {
          useTextField = true;
        }
        if( formatString.Length != 0 )
        {
          useFormatString = true;
        }
        foreach( object o in ie )
        {
          if( useTextField )
          {
            if( textField.Length > 0)
            {
              sb.AppendFormat(AutoCompleteTextBox._FormatString, 
                DataBinder.GetPropertyValue(o, textField, formatString));
            }
          }
          else
          {
            if( useFormatString )
            {
              sb.AppendFormat( AutoCompleteTextBox._FormatString, 
                                string.Format(formatString, o) );
            }
            else
            {
              sb.AppendFormat(AutoCompleteTextBox._FormatString, 
                                                  o.ToString());
            }
          }  // useTextField

        }  // foreach

      } // ie != null


      // Remove trailing '\n'

      if( sb.Length > 1 )
        sb.Remove(sb.Length-1, 1);


      CallBackHelper.Write( sb.ToString() );
    }


    /// <SUMMARY>

    /// Perhaps in the future, I will figure out 

    /// how to make this work.  Before

    /// you email me with the answer please try 

    /// using an AutoCompleteTextBox in

    /// a DataGrid with ViewState disabled.

    /// </SUMMARY>

    public override void DataBind()
    {
      // Do Nothing

    }

    /// <SUMMARY>

    /// What's the point if the developer turns on AutoPostBack?

    /// </SUMMARY>

    public override bool AutoPostBack
    {
      get { return false; }
    }


    #endregion

    #region Public Methods

    /// <SUMMARY>

    /// For now, Developer's must call this method to bind to their 

    /// Auto Complete Text Box.

    /// </SUMMARY>

    public virtual void BindData()
    {
      this.OnDataBinding(EventArgs.Empty);
    }

    #endregion

    #region Helper Methods

    /// <SUMMARY>

    /// Add a reference to the JavaScript, but only once per page.

    /// </SUMMARY>

    private void _RegisterCommonScripts()
    {
      if (!this.Page.IsClientScriptBlockRegistered("AutoCompleteTextBox"))
      {
        StringBuilder script = new StringBuilder();
        script.AppendFormat("<script src="{0}" type=text/javascript></script>", 
                                                             this.ScriptPath);
        this.Page.RegisterClientScriptBlock("AutoCompleteTextBox", 
                                                           script.ToString());
      }
    }

    #endregion
  }
}

History

  • 2005-05-02
    • Initial release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here