|
An ASP.NET 2.0 AutoComplete DropDownList...
In this article I'll show you how to easily create a DropDownList control in ASP.Net 2.0 with Auto-Complete...
By: Brett Burch
Date: March 28, 2006
Download the code.
Printer Friendly Version
Introduction
In this article I'll show you how to easily create a DropDownList control in ASP.Net 2.0 that solves the same
problem as a previous version solved in ASP.Net 1.* (seen at
http://www.dotnetjohn.com/articles.aspx?articleid=132).
This problem is commonly known to ASP.Net developers. As stated in the previous article, the problem is:
"The [DropDownList / html select list] control responds only to the first character you type. For example, if you
wanted to find "Brown" in a list of names you would first type the letter 'b'. The default control will take you to
the first 'b' in the list. If you then type in 'r' instead of taking you to the first item beginning with 'br', it
takes you to the first item beginning with an 'r'. Every character you type will take you to the first item
beginning with that character. This behavior is not very useful."
Creating the Server Control
Our first task is to create a new Web Control Library project, which is under the Windows section of your language
of choice.
Once your project is created you can begin by deleting the default WebCustomControl1 file in the project and
add a reference to the System.Design namespace. Start with a new item of type WebCustomControl called
AutoCompleteDropDownList. For simplicity of adding a little design-time richness, add using declarations for
the System.Web.UI.Design and System.Drawing.Design namespaces. Instead of deriving from the WebControl class,
derive directly from the DropDownList class instead. Clear out the generated code so that your class is
essentially bare:
namespace BrettResources.ServerControls
{
[ToolboxData("<{0}:AutoCompleteDropDownList runat=server></{0}:AutoCompleteDropDownList>")]
[Description("HTML Select list derived from the DropDownList control which enables auto-complete selection of items in the DropDownList as the user types in the control")]
public class AutoCompleteDropDownList : DropDownList
{
}
}
|
The JavaScript used for this example is the essentially the same as in the previous article, with a single
method named KeySortDropDownList_onkeypress taking two parameters (the dropdownlist object and the boolean
caseSensitive). This function is shown below.
function KeySortDropDownList_onkeypress (dropdownlist,caseSensitive)
{// check the keypressBuffer attribute is defined on the dropdownlist
var undefined;
if (dropdownlist.keypressBuffer == undefined)
{
dropdownlist.keypressBuffer = '';
}
// get the key that was pressed
var key = String.fromCharCode(window.event.keyCode);
dropdownlist.keypressBuffer += key;
if (!caseSensitive)
{
// convert buffer to lowercase
dropdownlist.keypressBuffer = dropdownlist.keypressBuffer.toLowerCase();
}
// find if it is the start of any of the options
var optionsLength = dropdownlist.options.length;
for (var n=0; n < optionsLength; n++)
{
var optionText = dropdownlist.options[n].text;
if (!caseSensitive)
{
optionText = optionText.toLowerCase();
}
if (optionText.indexOf(dropdownlist.keypressBuffer,0) == 0)
{
dropdownlist.selectedIndex = n;
return false; // cancel the default behavior since
// we have selected our own value
}
}
// reset initial key to be inline with default behavior
dropdownlist.keypressBuffer = key;
return true; // give default behavior
}
|
|
A new feature in ASP.Net v2.0 allows us to compile resources such as JavaScript files directly into our
DLLs to be pulled out later at runtime. Add a file called AutoComplete.js to the project and set the
Build Action property to Embedded Resource. This will be the default location for our JavaScript in the
case that developers choose not to write their own implementation of this functionality.
|
|
Now for the actual DropDownList code. I'll start by adding three virtual properties to allow the developer
to control the behavior of the control. The first allows the user to turn off the AutoComplete feature. The
second allows the developer to supply the caseSensitive parameter to the JavaScript function from the
control. The last of these allows the user to provide an external JavaScript file where their version of
KeySortDropDownList_onkeypress function exists. The Editor and UrlProperty attributes aid the design time
experience when selecting a custom JavaScript file.
#region Public Virtual Properties
/// <summary>
/// When set to true, javascript enabling auto-complete selection of items
/// in the DropDownList as the user types in the control is added
/// </summary>
[Category("Behavior")]
[DefaultValue(true)]
[Description("When set to true, javascript enabling auto-complete selection of items in the DropDownList as the user types in the control is added")]
public virtual Boolean AutoCompleteEnabled
{
get
{
return ((ViewState["AutoCompleteEnabled"] == null) ? true : (Boolean)ViewState["AutoCompleteEnabled"]);
}
set
{
ViewState["AutoCompleteEnabled"] = value;
}
}
[Category("Behavior")]
[DefaultValue(false)]
public virtual Boolean CaseSensitiveKeySort
{
get
{
return ((ViewState["CaseSensitiveKeySort"] == null) ? false : (Boolean)ViewState["CaseSensitiveKeySort"]);
}
set
{
ViewState["CaseSensitiveKeySort"] = value;
}
}
/// <summary>
/// Ex: "~/clientscript/AutoComplete.js"
/// </summary>
[Category("Behavior")]
[Description("Ex: ~/clientscript/AutoComplete.js")]
[Editor(typeof(System.Web.UI.Design.UrlEditor), typeof(UITypeEditor))]
[UrlProperty("*.js")]
public virtual string ExternalScriptSourceRelativeUrl
{
get
{
return ((string)ViewState["ExternalScriptSourceRelativeUrl"] == null) ? String.Empty : (string)ViewState["ExternalScriptSourceRelativeUrl"];
}
set
{
ViewState["ExternalScriptSourceRelativeUrl"] = value;
}
}
#endregion
|
You can set the AutoCompleteEnabled property as the default property by adding an additional attribute to
your class declaration like the following:
[DefaultProperty("AutoCompleteEnabled")]
|
These properties are useless until we override a method to inject our additional JavaScript reference to the
control's Page and add the onkeypress attribute to select HTML element which our DropDownList ultimately
renders on the browser as. I've done this with the following code:
#region Overridden Events
protected override void AddAttributesToRender(HtmlTextWriter writer)
{
base.AddAttributesToRender(writer);
if (this.BrowserSupportsJavascript && this.AutoCompleteEnabled)
{
writer.AddAttribute("onkeypress", this.GetOnKeyPressAttributeValue());
}
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
if (this.BrowserSupportsJavascript && this.AutoCompleteEnabled)
{
this.RegisterAutoCompleteJavaScript();
}
}
#endregion
|
The BrowserSupportsJavascript and AutoCompleteEnabled properties are referenced in both of these methods so
that our AutoComplete additions are only added to the response stream when both conditions are met. The
BrowserSupportsJavascript is declared below.
|
private Boolean BrowserSupportsJavascript
{
get
{
if (this.Context != null &&
this.Context.Request != null &&
this.Context.Request.Browser != null &&
this.Context.Request.Browser.EcmaScriptVersion != null)
{
return (this.Context.Request.Browser.EcmaScriptVersion.Major >= 1);
}
else
{
//for testing...
return true;
}
}
}
|
If you're not using a unit test framework to test you can shorten the above code to the following:
|
private Boolean BrowserSupportsJavascript
{
get { return (this.Context.Request.Browser.EcmaScriptVersion.Major >= 1); }
}
|
The GetOnKeyPressAttributeValue and RegisterAutoCompleteJavaScript methods are also referenced in the
overridden methods. They are defined as shown below. You'll also see additional code referencing
CustomSettings. I'll leave exploration of this as an excersize for the reader, but hopefully the source
code will provide the needed details.
#region Private Methods
private void RegisterAutoCompleteJavaScript()
{
if (this.UseCustomSettings)
{
this.ValidateCustomSettings();
if (string.IsNullOrEmpty(this.CustomSettings.ExternalScriptSourceRelativeUrl))
{
this.RegisterClientScriptBlock(this.CustomSettings.ScriptBlock);
}
else
{
this.RegisterClientScriptInclude(this.CustomSettings.ExternalScriptSourceRelativeUrl);
}
}
else
{
if (string.IsNullOrEmpty(this.ExternalScriptSourceRelativeUrl))
{
this.RegisterClientScriptInclude(
this.Page.ClientScript.GetWebResourceUrl(typeof(AutoCompleteDropDownList),
"BrettResources.ServerControls.AutoComplete.js"));
}
else
{
this.RegisterClientScriptInclude(this.ExternalScriptSourceRelativeUrl);
}
}
}
private void RegisterClientScriptBlock(String script)
{
if (!this.Page.ClientScript.IsClientScriptBlockRegistered("AutoCompleteDropDownListBlock"))
{
this.Page.ClientScript.RegisterClientScriptBlock(
this.GetType(),
"AutoCompleteDropDownListBlock",
script
);
}
}
private void RegisterClientScriptInclude(String relativeUrl)
{
if (!this.Page.ClientScript.IsClientScriptIncludeRegistered("AutoCompleteDropDownListInclude"))
{
this.Page.ClientScript.RegisterClientScriptInclude(
"AutoCompleteDropDownListInclude",
base.ResolveClientUrl(relativeUrl)
);
}
}
private String GetOnKeyPressAttributeValue()
{
string AttributeValue = "";
if (this.UseCustomSettings)
{
this.ValidateCustomSettings();
AttributeValue = "return " + this.CustomSettings.FunctionName + this.CustomSettings.FunctionSignature + ";";
}
else
{
AttributeValue = "return KeySortDropDownList_onkeypress(this," + this.CaseSensitiveKeySort.ToString().ToLower() + ");";
}
return AttributeValue;
}
private void ValidateCustomSettings()
{
if (this.CustomSettings == null)
throw new NullReferenceException("UseCustomSettings is set to true but no CustomSettings data was supplied.");
}
#endregion
|
That's a lot of code, but the only necessary details are in the RegisterClientScriptBlock,
RegisterClientScriptInclude and GetWebResourceUrl calls. The RegisterClientScriptBlock and
RegisterClientScriptInclude methods render JavaScript references (a block of inline JavaScript or
reference to an external JavaScript file respectively) and are probably familiar to you already. In the
case where no CustomSettings are used by the developer, the ClientScript.GetWebResourceUrl method will
be called to add a client script include to the response stream. In this case, the script reference will
look something like
|
<script src="/WebResource.axd?d={0}&t={1}" type="text/javascript"></script>
|
The {0} and {1} placeholders will be encrypted assembly information that aid in identifying the
Embedded Resource (our JavaScript file containing the KeySortDropDownList_onkeypress function in this
case) and a build timestamp for caching on both the server and client. The only important detail remaining
is how we identify the AutoComplete.js file in order to extract it from the assembly. We need to add the
following to our project:
//syntax: [assembly: WebResource("{namespace}.{filename}", "{content-type}")]
[assembly: WebResource("BrettResources.ServerControls.AutoComplete.js", "text/javascript")]
|
In this case it is likely that only one file in my project will reference this resource, so I added the
declaration in the AutoCompleteDropDownList.cs file. If you have a lot of Embedded Resources declared
throughout a large control library, you will likely choose to add this information to AssemblyInfo.cs in
order to consolidate these resource name declarations.
Using the Server Control
If you like, you can use this control just as you would normally declare a normal DropDownList and have the
AutoComplete functionality added to your page.
In my web.config
file, I added the following declaration to enable easy intellisense within VS.NET.
<configuration>
<system.web>
<pages>
<controls>
<add namespace="BrettResources.ServerControls" assembly="BrettResources.ServerControls" tagPrefix="brett"/>
|
Finally on my .aspx page, I added the control.
|
<brett:AutoCompleteDropDownList runat="server"
ID="AutoCompleteDropDownList1"
DataSourceID="xds" DataTextField="title" DataValueField="id"></brett:AutoCompleteDropDownList>
|
To populate the Items collection I'm referencing an XmlDataSource with title and id as my Text and Value
attributes for each item in the list. View the example at
http://www.brettresources.net/dotnet/samples/AutoCompleteDropDownList.aspx.
If you chose to not use the default JavaScript and wanted to use your own file, named MyAutoComplete.js, you
would declare the control as follows on your page (assuming the same Data Source):
|
<brett:AutoCompleteDropDownList runat="server"
ID="AutoCompleteDropDownList1"
ExternalScriptSourceRelativeUrl="~/clientscript/MyAutoComplete.js" DataSourceID="xds" DataTextField="title"
DataValueField="id"></brett:AutoCompleteDropDownList>
|
If you chose to completely roll your own implementation of the JavaScript and wanted to use the
CustomSettings you could do so using a method like the following in your code-behind file:
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
BrettResources.ServerControls.AutoCompleteCustomClientSettings CustomSettings =
new BrettResources.ServerControls.AutoCompleteCustomClientSettings();
CustomSettings.ExternalScriptSourceRelativeUrl = "~/clientscript/AutoComplete.js";
CustomSettings.FunctionName = "AnotherKeySortDropDownList_onkeypress";
CustomSettings.FunctionSignature = "(this,true)";
this.AutoCompleteDropDownList1.UseCustomSettings = true;
this.AutoCompleteDropDownList1.CustomSettings = CustomSettings;
}
}
|
This would render a script reference like
|
<script src="clientscript/AutoComplete.js" type="text/javascript"></script>
|
and would add an onkeypress attribute of onkeypress="return
AnotherKeySortDropDownList_onkeypress(this,true); " to your page.
Enjoy, and please contact me about bugs or desired enhancements.
Additional Resources
A Custom DropDownList Control With Autocomplete for ASP.NET
Whats with System.Web.UI.WebResourceAttribute?
|