AJAX WAS Here - Part 3 : Auto Complete TextBox






4.79/5 (32 votes)
May 2, 2005
8 min read

496329

3745
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:
- Guyon Roche, “Creating a Textbox with JavaScript Auto-Complete”
- Nicholas C. Zakas, “Creating an Autosuggest Textbox with JavaScript”
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>
[Bindable(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.