Table of Contents
The AutoSuggest ASP.NET Server Control Library provides the ability to use an auto-suggest text-box or drop-down list in your ASP.NET pages. The control can be populated inline through the AutoSuggestTextBox
's items or data source properties, or using AJAX to retrieve the items from a separate page. In order to simplify AJAX retrieval, the AutoSuggestListPage
is provided.
Outputting the list items in an AutoSuggestListPage
is as easy as adding a new item to your web site using the template provided and configuring as you would any other ListControl
. Another perk of using the AJAX functionality of the control is the ability to populate its items dynamically based on the values of other controls, fields, etc., with the Parameters
property.
The goal of this article is to be as comprehensive as possible in order to cover every aspect of this control. As such, the article may be a little too lengthy to take in all in one sitting. It may be helpful to consider this as a three part article all in one. Aside from the customary Introduction, Background, Points of Interest, and History sections, it may be easier to treat the sections described below as three separate parts:
- Using the Code - This section provides a categorized task-orientated breakdown on how to use the library along with code examples from the sample projects.
- AutoSuggest ASP.NET Server Control Library Reference - This section provides an MSDN style quick reference of each of the library's classes and properties they implement as well as a commented class diagram showing how the classes relate to each other.
- How it Works - This section provides a detailed look at what makes the library's controls tick and how they were developed.
As you may have noticed, each of the sections described above are broken down further into subsections that can be navigated to using the article's Table of Contents. In another effort to provide convenience, all references to properties implemented in the library used throughout the article display their description from the reference section as a tool tip. I hope you enjoy the article and find the library useful, and if you do, be sure to vote for it. I didn't put the time into the article and code that I did because I didn't think it could win best of for the month.
Also, this library will be used in a bigger project of my own that I hope will become a very high traffic site, so be as liberal as you want with the bug reports and feature requests, and I'll try to stay on top of them to the best of my ability. I'll be sure to let everyone here know all about it when the bigger project is launched, so be sure to check that out too when the time comes.
The JavaScript that powers the AutoSuggest ASP.NET Server Control Library has its roots right here at CodeProject. The code was first published at CodeProject by zichun with the article here[^]. When the original author gave up supporting the code, Dmitry Khudorozhkov adopted it, improved it, and has been diligently supporting it with the article here[^].
Then, I came along and wanted more... specifically, an ASP.NET server control. After spending a couple days wrapping the existing functionality of the JavaScript in the server control, I still wasn't satisfied. I wanted the ability to send the values of separate controls with the querystring of the AJAX call, and did so by adding a parameters property to the JavaScript and server control and an easy to configure base page for the AJAX output. I accomplished that with a very minimal set of changes to the original JavaScript in hopes of keeping the advantage of Dmitry's fabulous support of the code.
Then I just got greedy. I was so impressed by the intuitiveness and responsiveness of the control and the AJAX output page that I decided to ditch my current implementation for filling DropDownList
s via Web Services for this solution. I considered creating an entirely new server control and JavaScript object for the drop-down list, but ultimately went with butchering, I mean, amending the original JavaScript and providing properties for switching between text box and drop-down list functionality.
This section is a feature by feature approach to describing how to use the AutoSuggest ASP.NET Server Control Library in your web applications. To help prevent the guide from becoming too lengthy, some features are briefly explained and have the properties that pertain to them rattled off one after another, without any explanation as to their function. Keep in mind that you can view the summary of any property implemented in the library by simply hovering the mouse pointer over them wherever they are mentioned in the article.
The script and image paths for the AutoSuggestTextBox
are customizable through the properties displayed under the "Files" category in the property grid. The property names and descriptions are self explanatory as to the purpose of each file. The default values for the files are at the root of the site; however, if you wish to keep the scripts and images tucked away in other directories accessible to the site, you may do so as long as you set the said properties accordingly for each instance of the control.
The appearance of the AutoSuggestTextBox
is customizable through the properties displayed under the "Appearance" category in the property grid. Given the nature of the control, there are two components of the control for which the appearance can be set for: the text-box and the suggestion list.
The appearance of the text-box for the control is determined by the "Appearance" and "Layout" properties inherited form the inherited WebControl
class (with the exception of the Text
property which I added myself) which are: the BackColor
, BorderColor
, BorderStyle
, BorderWidth
, CssClass
, Font
, ForeColor
, Height
, and Width
properties. It must be noted that, should you decide to customize the height or font size of the control, the drop-down images should be modified accordingly to account for the size difference.
The appearance of the suggestion list for the control is determined by the custom "Appearance" properties of the control which are: the ArrowBackColor
, ItemBackColor
, ItemFontNames
, ItemFontSize
, ItemForeColor
, SelectedItemBackColor
, SelectedItemForeColor
, and UseScroll
properties. Most of these properties are self explanatory; however, the UseScroll
property deserves an explanation since it determines whether the suggestion list uses a typical scrollbar to navigate the list or the arrow images are used for navigating at the top and bottom.
The "Behavior" properties of the AutoSuggestTextBox
are somewhat extensive, so I will break them down in the proceeding sections to help put them into context.
The AutoSuggestTextBox
uses the Enabled
, EnableTheming
, EnableViewState
, ToolTip
, and Visible
properties inherited from WebControl
accordingly. Most of the general settings for the control only apply in text-box mode, which are: LimitStart
, MatchFirst
, NoDefault
, ResponseTime
, ResponseTime
, RestrictTyping
, StartCheck
TimeOut
, UseIFrame
, and UseMouse
.
When in text-box mode, the AutoSuggestTextBox
allows for multiple selections from the suggestion list by making use of the MultiSelectDelimiter1
and MultiSelectDelimiter2
properties. The default values for these properties are ";" and "," respectively. To disable multiple selections when in text-box mode, set both of these properties to an empty string.
The AutoSuggestTextBox
has the ability to execute the client script set with the ClientSelectedItemCallback
property. To use this property, set the text to a JavaScript function and it will execute when a user selects an item from the control's suggestion list. The default value of the property provides an example function and parameters expected by the control, as seen below:
function(index, control)
{
}
Since the code within the function is commented out, no code is executed if the property is left as its default value. Not to mislead you, the actual default value is the same code, but all on a single line. The MultilineStringEditor utilized by the property allows you to write multiple lined functions and they will still execute. To give fair warning, a syntax error in the function specified will render the control inoperable.
The AutoSuggestTextBox
has the ability to switch between text-box and drop-down mode, or can be refined to mix and match certain behaviors of each mode. While there are a few properties which control the different behaviors, switching between the two extremes has been made as simple as setting the UseDropDownButton
property. Changing this property will automatically change the FillVisible
, FilterSuggestions
, and ReadOnly
properties accordingly (see the AutoSuggestTextBox
reference section of this article for an explanation of each of these properties' purpose). Should you choose to mix and match behaviors, set the UseDropDownButton
property first to avoid having its linked properties reset.
The AutoSuggestTextBox
's suggestion list is populated in the same manner as the built-in list controls of ASP.NET. Each method of populating the suggestion list is described below and apply to both inline and AJAX list population, which is also described in detail after the method descriptions.
The simplest method of providing suggestions with which to populate the AutoSuggestTextBox
or AutoSuggestListPage
's suggestion list is manually populating the Items
property. This can be done in design mode by invoking the ListItemCollectionEditor from the property grid, or from the source like in the example from the sample project below:
<cc1:AutoSuggestTextBox ID="astAnimalType" runat="server"
FillVisibile="False" FilterSuggestions="False"
ReadOnly="True" SelectedValue="" UseDropDownButton="True"
Width="200px">
<Items>
<asp:ListItem>All</asp:ListItem>
<asp:ListItem>Amphibian</asp:ListItem>
<asp:ListItem>Bird</asp:ListItem>
<asp:ListItem>Fish</asp:ListItem>
<asp:ListItem>Invertebrate</asp:ListItem>
<asp:ListItem>Mammal</asp:ListItem>
<asp:ListItem>Reptile</asp:ListItem>
</Items>
</cc1:AutoSuggestTextBox>
For data binding, the AutoSuggestListBase
class exposes the DataMember
, DataSource
, DataSourceID
, DataTextField
, DataTextFormatString
, and DataValueField
properties. These properties serve the same purpose as in any other ASP.NET list control. In general, binding the suggestion list is accomplished by adding a DataSource
control configured to retrieve a table to the page, setting the AutoSuggest
control's DataSourceID
to its ID, then selecting the field names you wish to use for the DataTextField
and DataValueField
properties.
The AutoSuggestListBase
class' Items
property can be programmatically populated from the page's code-behind as demonstrated in the example from the AnimalListPage
of the sample project below:
public partial class AnimalListPage : AutoSuggestListPage
{
private string[] amphibians = new string[] { "Frog", "Newt" };
private string[] birds = new string[] { "Blue Heron", "Cardinal" };
private string[] fish = new string[] { "Albacore", "Anchovy" };
private string[] invertebrate = new string[] { "Ant", "Aphid" };
private string[] mammals = new string[] { "Aardvark", "Addax" };
private string[] reptiles = new string[] { "Agamid", "Alligator" };
private void Page_Load(object sender, EventArgs e)
{
switch (this.Request.QueryString["type"])
{
case "All":
ArrayList animals = new ArrayList();
foreach (string animal in this.amphibians)
animals.Add(animal);
foreach (string animal in this.birds)
animals.Add(animal);
foreach (string animal in this.fish)
animals.Add(animal);
foreach (string animal in this.invertebrate)
animals.Add(animal);
foreach (string animal in this.mammals)
animals.Add(animal);
foreach (string animal in this.reptiles)
animals.Add(animal);
animals.Sort();
foreach (string animal in animals)
this.AutoSuggestList.Items.Add(animal);
break;
case "Amphibian":
foreach (string animal in this.amphibians)
this.AutoSuggestList.Items.Add(animal);
break;
case "Bird":
foreach (string animal in this.birds)
this.AutoSuggestList.Items.Add(animal);
break;
case "Fish":
foreach (string animal in this.fish)
this.AutoSuggestList.Items.Add(animal);
break;
case "Invertebrate":
foreach (string animal in this.invertebrate)
this.AutoSuggestList.Items.Add(animal);
break;
case "Mammal":
foreach (string animal in this.mammals)
this.AutoSuggestList.Items.Add(animal);
break;
case "Reptile":
foreach (string animal in this.reptiles)
this.AutoSuggestList.Items.Add(animal);
break;
}
}
}
In the previous "Populating Suggestion Lists" sections were two code examples from the sample project. The first example is a manually populated AutoSuggestTextBox
in drop-down mode. The second example is the code-behind of an AutoSuggestListPage
where the Items
property of its AutoSuggestList
control is programmatically populated based on the value of the "type" parameter from the query string.
It is no coincidence that each case
of the switch
statement in the second example corresponds to an item from the first code example. The code example below is from an AutoSuggestTextBox
control in text-box mode on the same page as the first code example:
<cc1:AutoSuggestTextBox ID="astAnimal" runat="server"
Width="200px" AjaxUrl="~/AnimalListPage.aspx"
FullRefresh="True" MatchFirst="True"
NoDefault="False">
<Parameters>
<asp:ControlParameter ControlID="astAnimalType" Name="type"
PropertyName="SelectedValue" />
</Parameters>
</cc1:AutoSuggestTextBox>
The example above populates its suggestion list using AJAX. The page specified with the AjaxUrl
property was added to the sample project using the item template included with the sample. In order to use the item template, simply copy the template's zip file to the appropriate Visual Studio template directory (My Documents\Visual Studio 2008\Templates\ItemTemplates\Visual C#\). The FullRefresh
property being set to true ensures the control's suggestion list is refreshed each time it is displayed since the values to send with the querystring's parameters may have changed.
The Parameters
property of the example contains a ControlParameter
set to the ID of the AutoSuggestTextBox
from the first code example with the name "type" as expected in the query string of the second code example. Adding this parameter was accomplished by invoking the ParameterCollectionEditor from the Parameters
property on the property grid in design mode, which made it as simple as adding a parameter, selecting the ConrolParameter
type, and selecting the linked control's ID from the drop-down list provided by the editor.
It must be noted that the PropertyName
property of the ConrolParameter
will be ignored. This is by design to prevent a postback to the server from being required on the page using the AutoSuggestTextBox
each time the linked control's value is changed. Instead, the parameters of types ConrolParameter
and FormParameter
expect an input element of the ID specified and gets its possibly modified value on the client-side and sends it along with the query string when the suggestion list is to be displayed. The rest of the parameter types have their value set explicitly on the server side when the page containing the AutoSuggestTextBox
loads.
The AutoSuggest ASP.NET Server Control Library consists of the four classes below:
AutoSuggestListBase
- Base control for a data bound list with AJAX Output properties.AutoSuggestList
- Control for configuring an AutoSuggestListPage
in the designer.AutoSuggestListPage
- Base page for outputting lists for autosuggest controls. Requires a linked ASPX file with an AutoSuggestList
control added with the ID of AutoSuggestList (included in the template).AutoSuggestTextBox
- TextBox
control that automatically suggests matching items when the user types.
The AutoSuggestListBase
class inherits from DataBoundControl
to provide common functionality for data-binding and configuring AJAX output delimitation for the AutoSuggestTextBox
and AutoSuggestListPage
classes.
Data Properties
DataMember
- Gets or sets the table or view used for binding against.DataSourceID
- Gets or sets the control ID of an IDataSource
that will be used as the data source.DataTextField
- Gets or sets the field in the data source which provides the item text.DataTextFormatString
- Gets or sets the formatting applied to the text field. For example, {0:d}.DataValueField
- Gets or sets the field in the data source which provides the item value.Items
- Gets the collection of items in the list.
AJAX Output Properties
ItemDelimiter
- Gets or sets the character that delimits entries in the AJAX output.TextValueDelimiter
- Gets or sets the character that delimits text and values in the AJAX output.
The AutoSuggestList
class inherits from AutoSuggestListBase
to be used as a control to configure AutoSuggestListPage
s in the designer. The irrelevant control inherited properties are hidden from the PropertyGrid, and the HTML output is ignored by the AutoSuggestListPage
using the control.
The AutoSuggestListPage
class inherits from the Page
class to output the list items configured through its AutoSuggestList
control (or directly through the class' properties), or programmatically have its items populated through the Page_Load
method of the code-behind.
The AutoSuggestTextBox
class inherits from AutoSuggestListBase
, and implements IPostBackDataHandler
to provide a combination text-box/drop-down list control. In text-box mode, the control allows input and automatically displays matching suggestions for the user based on the list of items made available to it through its Items
property, which can be data-bound, or have its items provided via an AJAX call to an AutoSuggestListPage
. In drop-down mode, the control is read-only and can have its text changed through expanding an unfiltered list of suggestions provided in the same manner as in text-box mode by clicking the control's customizable drop-down button.
AJAX Output Properties
AjaxUrl
- Gets or sets the URL to retrieve the suggestions from.FullRefresh
- Gets or sets a value that indicates whether the script should re-send the AJAX request after each typed character.Parameters
- Gets or sets the parameters to pass to the AJAX page querystring.
Appearance Properties
ArrowBackColor
- Gets or sets the background color for the arrow rows (used if UseScroll
is false).ItemBackColor
- Gets or sets the background color for the suggestion list.ItemFontNames
- Gets or sets the font(s) of suggestion items.ItemFontSize
- Gets or sets the font size of suggestion items.ItemForeColor
- Gets or sets the text color for the non-selected suggestions.SelectedItemBackColor
- Gets or sets the background color for the selected item in the suggestion list.SelectedItemForeColor
- Gets or sets the text color for the selected suggestion.Text
- Gets or sets the text value for the control.UseScroll
- Gets or sets a value that indicates whether the control should use a scroll bar (true) or up/down arrow-buttons (false).
Behavior Properties
ClientItemSelectedCallback
- Gets or sets the client JavaScript callback function to execute when the user selects an item from the suggestion list.EntryLimit
- Gets or sets the number of entries autocomplete will show at a time.FillVisible
- Gets or sets a value that indicates whether the suggestion list is to be visible while the script is filling the list.FilterSuggestions
- Gets or sets a value that indicates whether the suggestions are filtered based on the value input.LimitStart
- Gets or sets a value that indicates whether the autocomplete should be limited to the beginning of the keyword.MatchFirst
- Gets or sets a value that indicates whether exact matches should be displayed first if LimitStart
is false.MultiSelectDelimiter1
- Gets or sets the first delimiter for multiple autocomplete entries. Set both to blank for single autocomplete.MultiSelectDelimiter2
- Gets or sets the second delimiter for multiple autocomplete entries. Set both to blank for single autocomplete.NoDefault
- Gets or sets a value that indicates whether the control should omit selecting the first item in a suggestion list.ReadOnly
- Gets or sets a value that indicates whether the text input is to be read-only but still allow suggestion selection. When using AJAX, if this property is set to true, the suggestions are automatically filled on page load, or if one of the controls specified as a parameter has its value changed, causing the first indexed suggestion to be selected if the existing text value is unavailable in the refreshed selection list.ResponseTime
- Gets or sets the time, in milliseconds, between the last char typed and the actual query.RestrictTyping
- Gets or sets a value that indicates whether the control should restrict to existing members of the array.StartCheck
- Gets or sets the number of characters needed to be typed before suggestions are shown (effective if > 1).TimeOut
- Gets or sets the autocomplete timeout, in milliseconds (0: autocomplete never times out).UseIFrame
- Gets or sets a value that indicates whether the control should use an IFrame element to fix the suggestion list positioning (MS IE only).UseDropDownButton
- Gets or sets a value that indicates whether the control should display a drop-down button. Changing this property will automatically change the FillVisible
, FilterSuggestions
, and ReadOnly
properties for the control to behave like a drop-down list or a text box, but the properties can also be changed independently after changing this property to mix behaviors.UseMouse
- Gets or sets a value that indicates whether mouse support should be enabled for the control.
Files Properties
DownHoverImagePath
- Gets or sets the path to the down arrow hover image file used by the control.DownImagePath
- Gets or sets the path to the down arrow image file used by the control.DropDownHoverImagePath
- Gets or sets the path to the drop-down hover image file used by the control.DropDownImagePath
- Gets or sets the path to the drop-down image file used by the control.DropDownPressedImagePath
- Gets or sets the path to the drop-down pressed image file used by the control.ScriptPath
- Gets or sets the path to the autosuggest JavaScript file used by the control.UpHoverImagePath
- Gets or sets the path to the up arrow hover image file used by the control.UpImagePath
- Gets or sets the path to the up arrow image file used by the control.
Action Events
SelectedIndexChanged
- Fires when the SelectedIndex
property has been changed.SelectedTextChanged
- Fires when the SelectedText
property has been changed.SelectedValueChanged
- Fires when the SelectedValue
property has been changed.TextChanged
- Fires when the Text
property has been changed.
In this section, I will go over what it took to develop the AutoSuggest ASP.NET Server Control Library. Having developed plenty of Windows controls in the past, I already had a pretty solid understanding of .NET component development. Developing this library for a CodeProject article was my way of making sure I would obtain a more solid understanding of developing ASP.NET server controls for my own benefit and in turn pass that knowledge along to you.
Whether you want to learn how to develop an ASP.NET server control as a beginner or you are just wondering how I implemented that one cool or elusive feature, this is the section for you. As mentioned, this was a learning experience for me, so if you are an ASP.NET expert looking to berate my code and point out every little thing I did wrong or could have done better, please do so.
That said, the objective of the library is to implement the functional requirements below, which I will then detail how they were implemented in the proceeding sections.
- Output Text Input - Covers how the HTML text input is output to the page by the
AutoSuggestTextBox
. - Use ViewState - Covers how the
AutoSuggestTextBox
's ViewState is persisted between postbacks. - Implement IPostBackDataHandler - Covers how the
AutoSuggestTextBox
is notified its text has been modified on the client and reflects the changes after a post-back to the server. - Data Bound ListControl Behavior - Covers how the
AutoSuggestListBase
class provides properties and implements methods to bind the ListItemCollection
of the Items
property to a data source. - AutoSuggest JavaScript Class - Briefly covers the JavaScript source that powers the suggestion list and details the changes made to extend the code originally developed by Zichun and Dmitry to support the features of the
AutoSuggestTextBox
. - Output AutoSuggest Script Calls - Covers how the autosuggest object is instantiated for the text input and its properties are set to based on the properties implemented in the
AutoSuggestTextBox
. - Designer Functionality - Covers how all the bells and whistles for configuring the controls from the property grid in design mode are implemented.
- Script Registration - Covers how the autosuggest JavaScript source is registered and the extra measures needed to get the calls to the script to function within an ASP.NET AJAX
UpdatePanel
. - AutoSuggestListPage for AJAX Output - Covers how the base page for the AJAX list output was created and how it uses an instance of the
AutoSuggestList
control for configuration and functionality. - Item Template for AutoSuggestListPage - Covers how the item template was created to simplify adding
AutoSuggestListPage
s to web projects with the New Item dialog.
Like many other server controls, the main purpose of this library is to accept input from the user so that it may be retrieved and used back at the server. To accomplish this, the AutoSuggestTextBox
writes an HTML text type input element to the page by inheriting from WebControl
and overriding its RenderContents
method. The code below shows how the control outputs the text input and accounts for many of the properties the control inherits from WebControl
with its attributes in this method.
protected override void RenderContents(HtmlTextWriter output)
{
if (this.Visible)
{
output.WriteBeginTag("input");
output.WriteAttribute("type", "text");
output.WriteAttribute("value", this.Text);
output.WriteAttribute("autocomplete", "off");
output.WriteAttribute("id", this.ClientID);
output.WriteAttribute("name", this.UniqueID);
if (!this.Enabled)
output.WriteAttribute("disabled", "disabled");
if (this.CssClass != string.Empty)
output.WriteAttribute("class", this.CssClass);
if (this.ToolTip != string.Empty)
output.WriteAttribute("title", this.ToolTip);
output.Write(" style=\"");
if (this.Width != Unit.Empty)
output.WriteStyleAttribute("width", this.Width.ToString());
if (this.Height != Unit.Empty)
output.WriteStyleAttribute("height", this.Height.ToString());
if (this.Font.Name != string.Empty)
output.WriteStyleAttribute("font-family", this.Font.Name);
if (this.Font.Size != FontUnit.Empty)
output.WriteStyleAttribute("font-size", this.Font.Size.ToString());
if (this.BackColor != Color.Empty)
output.WriteStyleAttribute("background-color"
, ColorTranslator.ToHtml(this.BackColor));
if (this.ForeColor != Color.Empty)
output.WriteStyleAttribute("color"
, ColorTranslator.ToHtml(this.ForeColor));
if (this.BorderColor != Color.Empty)
output.WriteStyleAttribute("border-color"
, ColorTranslator.ToHtml(this.BorderColor));
if (this.BorderStyle != BorderStyle.NotSet)
output.WriteStyleAttribute("border-style", this.BorderStyle.ToString());
if (this.BorderWidth != Unit.Empty)
output.WriteStyleAttribute("border-width", this.BorderWidth.ToString());
output.WriteLine("\" />");
}
}
As you can see in the example, the first thing the control does is check if its Visible
property is true; otherwise nothing is written to the page. If visible, the control uses the HtmlTextWriter
object passed with the method's output parameter to call its WriteBeginTag
method to open an element with the tag name of "input". Then, the WriteAttribute
method is used to add the type attribute set to "text", value set to the value of the Text
property of the control, and auto-complete set to off to prevent browsers from displaying their own auto-complete suggestions.
Next, the id and name attributes are written to the element set to the ClientID
and UniqueID
properties inherited from WebControl
, respectively. The values of these properties are output rather than the value of the ID
property for these properties to ensure that the values are unique within the context of the page rather than the context of the container.
Then, the code adds the disabled, class, and title attributes if the respective values of the Enabled
, CssClass
, and ToolTip
properties inherited from WebControl
settings require them.
The last attribute added to the element is the style attribute. This is accomplished by using the Write
method to add the opening of the style attribute, then use the WriteStyleAttribute
method to add each "Appearance" and "Layout" property inherited from WebControl
accordingly, if required. Finally, the closing quote of the style attribute is added and the input tag is closed with the WriteLine
method.
ASP.NET provides the ability to save the state of a control between post backs with the ViewState
property of the base Control
. The ViewState
property is a Dictionary which has its values encoded in a hidden field on the page. The example code below is for the AutoSuggestTextBox
's Text
property which uses the ViewState to persist its value between post backs.
public string Text
{
get
{
if (this.EnableViewState)
{
if (this.ViewState["Text"] != null)
return this.ViewState["Text"].ToString();
else
return string.Empty;
}
else
return this.myText;
}
set
{
if (this.EnableViewState)
this.ViewState["Text"] = value;
else
this.myText = value;
}
}
In this property the EnableViewState
property is checked before using the ViewState
Dictionary
. This is done because the control takes no special precaution to prevent the ViewState
field from being written to when EnableViewState
is set to false on its own. If EnableViewState
is true, the value is set and retrieved from the ViewState
Dictionary
by simply referring to a key defined for the property; otherwise the control falls back on using a private field declared in the class for storing the property value.
In order to allow the client-side modified value of the HTML text input to be reflected by the Text
property of the AutoSuggestTextBox
, the control implements the IPostBackDataHandler
interface. To implement the IPostBackDataHandler
interface, the control needs to implement the LoadPostData
and RaisePostDataChangedEvent
methods as demonstrated in the example code below.
#region IPostBackDataHandler Members
private bool myTextChanged;
private bool mySelectedTextChanged;
private bool mySelectedValueChanged;
private bool mySelectedIndexChanged;
public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
{
if (postCollection[this.UniqueID] != this.Text)
{
this.Text = postCollection[this.UniqueID];
this.myTextChanged = true;
}
if (postCollection[this.UniqueID + "$SelectedText"] != this.SelectedText)
{
this.SelectedText = postCollection[this.UniqueID + "$SelectedText"];
this.mySelectedTextChanged = true;
}
if (postCollection[this.UniqueID + "$SelectedValue"] != this.SelectedValue)
{
this.SelectedValue = postCollection[this.UniqueID + "$SelectedValue"];
this.mySelectedValueChanged = true;
}
if (postCollection[this.UniqueID + "$SelectedIndex"]
!= this.SelectedIndex.ToString())
{
this.SelectedIndex = Convert.ToInt32(postCollection[this.UniqueID
+ "$SelectedIndex"]);
this.mySelectedIndexChanged = true;
}
return (this.myTextChanged || this.mySelectedIndexChanged
|| this.mySelectedTextChanged || this.mySelectedValueChanged);
}
public void RaisePostDataChangedEvent()
{
if (this.myTextChanged)
this.OnTextChanged(new EventArgs());
if (this.mySelectedTextChanged)
this.OnSelectedTextChanged(new EventArgs());
if (this.mySelectedValueChanged)
this.OnSelectedValueChanged(new EventArgs());
if (this.mySelectedIndexChanged)
this.OnSelectedIndexChanged(new EventArgs());
}
#endregion
As you can see from the example, there are actually four properties linked to the post data. Since the LoadPostData
method returns only one boolean value to indicate whether the RaisePostDataChangedEvent
method is called, four private fields are declared to store whether each of the properties have been changed.
Inside the LoadPostData
method, the value of the Text
property is checked against the value indexed in the postCollection
parameter under the value of the control's UniqueID
property. If you recall the previous section, you will remember that the control's UniqueID
property was used to determine the name of the HTML text input. This demonstrates that the postCollection
contains each of the form field values sent back to the server in the post data when the form is submitted. If the value contained in the postCollection
is indeed different from the value of the property, the property is then set to the modified value, and the myTextChanged
value is set to true to indicate the change did take place.
The other three post data linked properties are checked in the same manner as the Text
property except that the control's UniqueID
property is concatenated in the same manner as it was when the hidden fields were registered in the Render
method as shown in the Script Registration section. After each of the post data linked properties have been handled, the LoadPostData
method returns true if any of the property values have been changed.
If the LoadPostData
method returns true, the RaisePostDataChanged
method is executed. Inside the method, each private field that stores whether a property was changed is checked and the method that triggers the appropriate event is called. There is not a whole lot to the event triggering methods, but if you're curious, see the example below which contains the code for the TextChanged
event and its trigger method.
protected void OnTextChanged(EventArgs e)
{
if (this.TextChanged != null)
this.TextChanged(this, e);
}
[Category("Action")]
[Description("Fires when the Text property has been changed.")]
public event EventHandler TextChanged;
Data binding is handled in the AutoSuggestListBase
class. Since this class is to be inherited from by the AutoSuggestTextBox
and the AutoSuggestList
control used by the AutoSuggestListPage
, not all of the ListControl
functionality is required. Instead, the AutoSuggestListBase
inherits from DataBoundControl
and implements its own Items
and "Data" properties, and performs its own data binding by overriding the PerformDataBinding
method, as demonstrated in the example code below.
protected override void PerformDataBinding(IEnumerable dataSource)
{
if (dataSource != null && !this.DesignMode)
{
object text;
object value;
foreach (object row in dataSource)
{
text = null;
value = null;
if (this.DataTextField != string.Empty
&& this.DataTextFormatString != string.Empty)
text = DataBinder.GetPropertyValue(row, this.DataTextField
, this.DataTextFormatString);
else if (this.DataTextField != string.Empty)
text = DataBinder.GetPropertyValue(row, this.DataTextField);
if (this.DataValueField != string.Empty)
value = DataBinder.GetPropertyValue(row, this.DataValueField);
if (text != null)
{
if (value != null)
this.Items.Add(new ListItem(text.ToString(), value.ToString()));
else
this.Items.Add(new ListItem(text.ToString()));
}
}
}
}
In this method, the dataSource
parameter implements the IEnumerable
class. After ensuring the dataSource
is not null
and the control is not in design mode, two objects are declared to temporarily store the text and value fields for each row from the dataSource
. The foreach
statement iterates through each row from the dataSource
. The DataBinder
's static GetPropertyValue
method is used accordingly to get the values from each row object based on the values of the DataTextField
, DataTextFormatString
, and DataValueField
provided by the control. Once the values have been retrieved from the dataSource
, they are added to the ListItemCollection
of the Items
property using the temporary objects' ToString
method to ensure the values are not set to null in the next iteration.
So far, all that's been covered is how the AutoSuggestTextBox
implements the same behavior as a TextBox
, registers a few hidden fields, and how the foundation was laid for the ListControl
behavior. Obviously, the suggestion list and drop-down list behavior don't just magically happen on their own. All of that is handled on the client-side by way of the autosuggest JavaScript class.
As far as this article is concerned, however, how the JavaScript works is going to pretty much stay magical. Instead, I'll just remind you of the original articles the script originated from (copied and pasted from the Background section) and briefly cover the changes I made. The code was first published to CodeProject by zichun with the article here[^]. When the original author gave up supporting the code, Dmitry Khudorozhkov adopted it, improved it, and has been diligently supporting it with the article here[^].
Basically, the autosuggest JavaScript class exposes a property for nearly all of the custom properties implemented in the AutoSuggestTextBox
with the exception of the Items
, ClientItemSelectedCallback
, and AjaxUrl
properties, which are passed in through the counstructor. To meet the the drop-down mode and AJAX parameters requirements for the AutoSuggestTextBox
, equivalents for the control's Parameters
, FillVisible
, FilterSuggestions
, ReadOnly
, and UseDropDownButton
properties were added to the JavaScript class. All properties in the JavaScript class use an all lowercase, underscore spaced naming convention, and some property names on the server control were changed entirely to meet traditional .NET naming conventions.
The fact that the autosuggest JavaScript class was designed in an object orientated manner greatly simplifies outputting the JavaScript code required to initialize the control's functionality. As demonstrated in the code below, the process is as writing the JavaScript to create a new autosuggest object and setting its properties to the values of their equivalent properties in the AutoSuggestTextBox
.
protected void RenderScript(HtmlTextWriter output)
{
if (this.Visible)
{
bool multiValue = false;
foreach (ListItem item in this.Items)
{
multiValue = (item.Text != item.Value);
if (multiValue)
break;
}
output.WriteBeginTag("script");
output.WriteAttribute("type", "text/javascript");
output.WriteLine(">");
if (this.Items.Count > 0 && this.AjaxUrl == string.Empty)
{
output.Write("var " + this.ClientID + "Items = [");
if (multiValue)
{
for (int itemIndex = 0; itemIndex < this.Items.Count; itemIndex++)
{
if (itemIndex == 0)
output.WriteLine("['"
+ this.Items[itemIndex].Text.Replace("'", "\\'")
+ "', '" + this.Items[itemIndex].Value.Replace("'", "\\'")
+ "']");
else if (itemIndex != this.Items.Count - 1)
output.WriteLine(", ['"
+ this.Items[itemIndex].Text.Replace("'", "\\'") + "', '"
+ this.Items[itemIndex].Value.Replace("'", "\\'")
+ "']");
else
output.Write(", ['"
+ this.Items[itemIndex].Text.Replace("'", "\\'")
+ "', '"
+ this.Items[itemIndex].Value.Replace("'", "\\'")
+ "']");
}
}
else
{
for (int itemIndex = 0; itemIndex < this.Items.Count; itemIndex++)
{
if (itemIndex == 0)
output.WriteLine("'"
+ this.Items[itemIndex].Text.Replace("'", "\\'") + "'");
else if (itemIndex != this.Items.Count - 1)
output.WriteLine(", '"
+ this.Items[itemIndex].Text.Replace("'", "\\'") + "'");
else
output.Write(", '"
+ this.Items[itemIndex].Text.Replace("'", "\\'") + "'");
}
}
output.WriteLine("];");
}
if (this.AjaxUrl == string.Empty)
output.WriteLine("var " + this.ClientID
+ "AutoSuggest = new autosuggest('" + this.ClientID + "', "
+ this.ClientID + "Items, null, " + this.ClientItemSelectedCallback
+ ");");
else
output.WriteLine("var " + this.ClientID
+ "AutoSuggest = new autosuggest('" + this.ClientID + "', '', '"
+ ResolveUrl(this.AjaxUrl) + "?', " + this.ClientItemSelectedCallback
+ ");");
if (this.TextValueDelimiter != ",")
output.WriteLine(this.ClientID + "AutoSuggest.item_delimiter = '"
+ this.TextValueDelimiter + "';");
if (this.ItemDelimiter != "|")
output.WriteLine(this.ClientID + "AutoSuggest.ajax_delimiter = '"
+ this.ItemDelimiter + "';");
if (this.MultiSelectDelimiter1 != ";" && this.MultiSelectDelimiter2 != ","
&& this.MultiSelectDelimiter1 != string.Empty)
{
output.WriteLine(this.ClientID + "AutoSuggest.text_delimiter = ['"
+ this.MultiSelectDelimiter1 + "', '" + this.MultiSelectDelimiter2
+ "'];");
}
if (!this.ItemBackColor.Equals(Color.FromArgb(255, 255, 240)))
output.WriteLine(this.ClientID + "AutoSuggest.bg_color = '"
+ ColorTranslator.ToHtml(this.ItemBackColor) + "';");
if (!this.ArrowBackColor.Equals(Color.FromArgb(101, 98, 145)))
output.WriteLine(this.ClientID + "AutoSuggest.ar_color = '"
+ ColorTranslator.ToHtml(this.ArrowBackColor) + "';");
if (!this.ItemForeColor.Equals(Color.Black))
output.WriteLine(this.ClientID + "AutoSuggest.text_color = '"
+ ColorTranslator.ToHtml(this.ItemForeColor) + "';");
if (!this.SelectedItemForeColor.Equals(Color.FromArgb(240, 0, 0)))
output.WriteLine(this.ClientID + "AutoSuggest.htext_color = '"
+ ColorTranslator.ToHtml(this.SelectedItemForeColor) + "';");
if (!this.SelectedItemBackColor.Equals(Color.FromArgb(214, 215, 231)))
output.WriteLine(this.ClientID + "AutoSuggest.hcolor = '"
+ ColorTranslator.ToHtml(this.SelectedItemBackColor) + "';");
if (this.ItemFontNames != "verdana,arial,helvetica")
output.WriteLine(this.ClientID + "AutoSuggest.font = '"
+ this.ItemFontNames + "';");
if (this.ItemFontSize != Unit.Parse("10px")
&& this.ItemFontSize != Unit.Empty)
output.WriteLine(this.ClientID + "AutoSuggest.font_size = '"
+ this.ItemFontSize.ToString() + "';");
if (this.TimeOut != 0)
output.WriteLine(this.ClientID + "AutoSuggest.time_out = "
+ this.TimeOut.ToString() + ";");
if (this.ResponseTime != 500)
output.WriteLine(this.ClientID + "AutoSuggest.response_time = "
+ this.ResponseTime.ToString() + ";");
if (this.EntryLimit != 10)
output.WriteLine(this.ClientID + "AutoSuggest.entry_limit = "
+ this.EntryLimit.ToString() + ";");
if (this.StartCheck != 0)
output.WriteLine(this.ClientID + "AutoSuggest.start_check = "
+ this.StartCheck.ToString() + ";");
if (this.StartCheck != 0)
output.WriteLine(this.ClientID + "AutoSuggest.start_check = "
+ this.StartCheck.ToString() + ";");
if (this.LimitStart)
output.WriteLine(this.ClientID + "AutoSuggest.limit_start = true;");
if (this.MatchFirst)
output.WriteLine(this.ClientID + "AutoSuggest.match_first = true;");
if (this.RestrictTyping)
output.WriteLine(this.ClientID + "AutoSuggest.restrict_typing = true;");
if (this.FullRefresh)
output.WriteLine(this.ClientID + "AutoSuggest.full_refresh = true;");
if (!this.UseIFrame)
output.WriteLine(this.ClientID + "AutoSuggest.use_iframe = false;");
if (!this.UseScroll)
output.WriteLine(this.ClientID + "AutoSuggest.use_scroll = false;");
if (!this.UseMouse)
output.WriteLine(this.ClientID + "AutoSuggest.use_mouse = false;");
if (!this.NoDefault)
output.WriteLine(this.ClientID + "AutoSuggest.no_default = false;");
if (!this.FilterSuggestions)
output.WriteLine(this.ClientID
+ "AutoSuggest.filter_suggestions = false;");
if (!this.FillVisibile)
output.WriteLine(this.ClientID + "AutoSuggest.fill_visible = false;");
if (this.ReadOnly)
output.WriteLine(this.ClientID + "AutoSuggest.read_only = true;");
if (this.UseDropDownButton)
output.WriteLine(this.ClientID
+ "AutoSuggest.use_dropdown_button = true;");
foreach (Parameter item in this.Parameters)
{
if (item is ControlParameter)
{
Control control = this.Parent.FindControl(
((ControlParameter)item).ControlID);
if (control == null)
{
Control parent = this.Parent;
while (parent != null && control == null)
{
parent = parent.Parent;
control = parent.FindControl(
((ControlParameter)item).ControlID);
}
}
if (control != null)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['@" + control.ClientID + "', '"
+ item.Name + "'];");
}
else if (item is FormParameter)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['@" + ((FormParameter)item).FormField + "', '"
+ item.Name + "'];");
else if (item is QueryStringParameter)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['" + item.Name + "', '"
+ Page.Request.QueryString[
((QueryStringParameter)item).QueryStringField] + "'];");
else if (item is CookieParameter)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['" + item.Name + "', '"
+ Page.Request.Cookies[((CookieParameter)item).CookieName].Value
+ "'];");
else if (item is SessionParameter)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['" + item.Name + "', '"
+ Page.Session[((SessionParameter)item).SessionField].ToString()
+ "'];");
else if (item is ProfileParameter)
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['" + item.Name + "', '"
+ HttpContext.Current.Profile[
((ProfileParameter)item).PropertyName].ToString() + "'];");
else
output.WriteLine(this.ClientID + "AutoSuggest.parameters["
+ this.ClientID + "AutoSuggest.parameters.length]"
+ " = ['" + item.Name + "', '" + item.DefaultValue + "'];");
}
if (this.DownImagePath != "~/arrow-down.gif")
output.WriteLine(this.ClientID + "AutoSuggest.image[0] = "
+ ResolveUrl(this.DownImagePath));
if (this.DownHoverImagePath != "~/arrow-down-d.gif")
output.WriteLine(this.ClientID + "AutoSuggest.image[1] = "
+ ResolveUrl(this.DownHoverImagePath));
if (this.UpImagePath != "~/arrow-up.gif")
output.WriteLine(this.ClientID + "AutoSuggest.image[2] = "
+ ResolveUrl(this.UpImagePath));
if (this.UpHoverImagePath != "~/arrow-up-d.gif")
output.WriteLine(this.ClientID + "AutoSuggest.image[3] = "
+ ResolveUrl(this.UpHoverImagePath));
if (this.DropDownImagePath != "~/drop-down.png")
output.WriteLine(this.ClientID + "AutoSuggest.image[4] = "
+ ResolveUrl(this.DropDownImagePath));
if (this.DropDownHoverImagePath != "~/drop-down_h.png")
output.WriteLine(this.ClientID + "AutoSuggest.image[5] = "
+ ResolveUrl(this.DropDownHoverImagePath));
if (this.DropDownPressedImagePath != "~/drop-down_p.png")
output.WriteLine(this.ClientID + "AutoSuggest.image[6] = "
+ ResolveUrl(this.DropDownPressedImagePath));
output.Write("</script>");
}
}
Don't let the length of the method mislead you; had the JavaScript class not been implemented in an object orientated manner, the code could have been a lot more complicated. Rather than lose you by trying to explain everything that is happening in this method, I'll let the comments in the code do that for you. The different ways in which the HtmlTextWriter
output parameter is used is explained with the much shorter example of writing to the HTML served by the page in the Output Text Input section, and the same strategy of checking properties against their default values is used to prevent anything unnecessary from being output.
To make editing the AutoSuggestTextBox
and AutoSuggestListBase
property values as easy as possible, the controls take advantage of the many UITypeEditor
s that come with the .NET Framework. Of course, custom UITypeEditor
s could have been created, but that wasn't really necessary for any of the properties. It did, however, take a significant amount of hunting for the right editors to use for each task, so I'll go ahead and go over each of them here to put the information all in one place.
Most properties use the EditorAttribute
to specify how they are to be edited from the property grid in design mode, while others required additional attributes. Each UITypeEditor
and attribute used are listed below.
ImageUrlEditor
- Used with UrlPropertyAttribute
to allow selecting a file from the project to use as the URL for an image. Used for the suggestion navigation arrows and drop-down button image properties like in the example below.
[UrlProperty(), Editor(typeof(ImageUrlEditor), typeof(UITypeEditor))]
public string DownHoverImagePath
UrlEditor
- Used with UrlPropertyAttribute
to allow selecting a file from the project to use as a URL. Used for the ScriptPath
and AjaxUrl
properties like in the example below.
[UrlProperty(), Editor(typeof(UrlEditor), typeof(UITypeEditor))]
public string AjaxUrl
ColorEditor
- Used to allow setting named colors from a drop-down list and custom from a dialog. Used for color "Appearance" properties like in the example below.
[Editor(typeof(ColorEditor), typeof(UITypeEditor))]
public string ItemBackColor
DataFieldConverter
- Specified with the TypeConverterAttribute
and used with the NotifyParentPropertyAttribute
to allow selecting a column name available from the data source specified with the DataSourceID
property with a drop-down list. Used for the DataTextField
and DataValueField
properties like in the example below.
[NotifyParentProperty(true), TypeConverter(typeof(DataFieldConverter))]
public string DataTextField
PersistanceMode
- Specified with the PersistenceModeAttribute
and used to set whether the property is to be set in the ASP.NET source with a node or child element of the tag. Used for the Text
and Items
properties like in the example below.
[PersistenceMode(PersistenceMode.InnerProperty)]
public string Text
CategoryAttribute
and DescriptionAttribute
- Used to set how a property or event is to be categorized and described in the property grid. Used for all properties and events browsable by the property grid like in the example below.
[Category("Appearance"), Description("The text value of the control.")]
public string Text
DefaultValueAttribute
- Used to specify the default value of a property which does not require it to be specified in the tag (property value should be set to match the value specified here in the class' constructor). Used for nearly all properties like in the example below for setting the default value of a Color
property.
[DefaultValue(typeof(Color), "0xF00000")]
public Color SelectedItemForeColor
MergablePropertyAttribute
- Used to specify if a property can be edited in the property grid when multiple controls are selected. Used for the Parameters
and Items
properties like in the example below.
[MergableProperty(false)]
public ParameterCollection Parameters
BrowsableAttribute
- Used to specify if a property can be edited in the property grid. Used for the SelectedIndex
SelectedValue
and SelectedText
properties of the AutoSuggestTextBox
and several irrelevant properties of the AutoSuggestList
, like in the example below.
[Browsable(false)]
public int SelectedIndex
As you can see, there are quite a few attributes and UITypeEditor
s available from the .NET framework for you to exploit for adding designer support for your own components. There are plenty more design tools where the ones used by this library came from, which is the .NET Framework's System.Design and System.Drawing.Design assemblies.
Should the needs of your own component properties not already be implemented in the .NET Framework, there is always the option of developing your own. Custom editors can be created by inheriting from UITypeEditor
, and designers for editing components from an expandable menu in the designer can be created by inheriting from HtmlControlDesigner
. Adding advanced designer support of that nature wasn't really a priority when developing this library, but may be added and covered in future updates.
There really isn't a whole lot involved in regards to script registration in the ASP.NET 2.0 version of the library. Since the AutoSuggestTextBox
uses an inline script to initialize the control, there is a bit more that needs to be done for it to function properly within an ASP.NET 3.5 AJAX UpdatePanel
. For this reason, the library is available in two different assembly versions.
The only differences in these two versions are the Render
and OnPreRender
methods of the AutoSuggestTextBox
. For maintainability between the two versions, the AutoSuggestTextBox
class is declared as partial
, and the Render
and OnPreRender
methods are implemented in their own files for the two projects, and the rest of the files were added as linked items. There is a trick to use Eeflection for calling the required ScriptManager
methods for registering the script in ASP.NET 2.0, but that method proved to be a bust when using the library by referencing its assembly.
As mentioned, not a whole lot need be done to register the script with the ASP.NET 2.0 version of the library. The script file used by the AutoSuggestTextBox
is registered by overriding the control's OnPreRender
method as seen in the example code below.
public partial class AutoSuggestTextBox
{
protected override void OnPreRender(EventArgs e)
{
if (!Page.ClientScript.IsClientScriptBlockRegistered("autosuggest"))
{
string path = ResolveUrl(this.ScriptPath);
string script = "<script type=\"text/javascript\" src=\""
+ path + "\"></script>";
Page.ClientScript.RegisterClientScriptBlock(typeof(Page)
, "autosuggest", script);
}
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
RenderContents(writer);
if (!this.DesignMode)
{
RenderScript(writer);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedText"
, this.SelectedText);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedValue"
, this.SelectedValue);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedIndex"
, this.SelectedIndex.ToString());
}
}
}
In this method, the script file contain the autosuggest JavaScript class is registered using the Page
's ClientScript
property. Before the script is registered, a check is made to make sure it hasn't already been registered. The path to the script file is then resolved, concatenated into the script tag, and registered as a client script block. It should be noted that I could have used the RegisterClientScriptResource
method, but when an exception was thrown because the ScriptFile
was improperly set during testing didn't sit well with me, I decided against using it so that it would fail silently. In hind-sight, this might have been a controversial move, but if you don't like it, you know how to change it.
I included the Render
method in this example for comparison purposes against the ASP.NET 3.5 AJAX version and also to note another purpose of overriding the Render
method. By default, the Render
method of WebControl
will write a span element to the page with the id set to the value from the ClientID
property. Since that would create a conflict with the id of the input element output by the control, no call is made to the Render
method of the base class.
After the HTML input element and script has been written by the Render
method, the control registers three hidden fields with the values of the SelectedText
, SelectedValue
, and SelectedIndex
properties. The names of these fields are set to the value of the control's UniqueID
property concantenated with a string to indicate the property it represents. The autosuggest JavaScript object ensures that the values of these fields are set accordingly when an item is selected from the suggestion list.
As previously alluded to, if the AutoSuggestTextBox
is used within an ASP.NET 3.5 AJAX UpdatePanel
, the ScriptManager
is used to register the script file and inline script which initializes the instance of the control. You can see how this is accomplished with the example code below.
public partial class AutoSuggestTextBox
{
protected override void OnPreRender(EventArgs e)
{
string path = ResolveUrl(ScriptPath);
string script = "<script type=\"text/javascript\" src=\""
+ path + "\"></script>";
ScriptManager sm = ScriptManager.GetCurrent(this.Page);
if (sm != null && sm.IsInAsyncPostBack)
ScriptManager.RegisterClientScriptBlock(this
, typeof(AutoSuggestTextBox), "autosuggest"
, script, false);
else if (!Page.ClientScript.IsClientScriptBlockRegistered("autosuggest"))
Page.ClientScript.RegisterClientScriptBlock(typeof(Page)
, "autosuggest", script);
base.OnPreRender(e);
}
protected override void Render(HtmlTextWriter writer)
{
RenderContents(writer);
ScriptManager sm = ScriptManager.GetCurrent(this.Page);
if (sm != null && sm.IsInAsyncPostBack && !this.DesignMode)
{
StringWriter sw = new StringWriter();
RenderScript(new HtmlTextWriter(sw));
string script = sw.ToString();
ScriptManager.RegisterStartupScript(this
, typeof(AutoSuggestTextBox), UniqueID
, script, false);
ScriptManager.RegisterHiddenField(this
, this.UniqueID + "$SelectedText"
, this.SelectedText);
ScriptManager.RegisterHiddenField(this
, this.UniqueID + "$SelectedValue"
, this.SelectedValue);
ScriptManager.RegisterHiddenField(this
, this.UniqueID + "$SelectedIndex"
, this.SelectedIndex.ToString());
}
else if (!this.DesignMode)
{
RenderScript(writer);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedText"
, this.SelectedText);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedValue"
, this.SelectedValue);
Page.ClientScript.RegisterHiddenField(this.UniqueID + "$SelectedIndex"
, this.SelectedIndex.ToString());
}
}
}
As you can see in the OnPreRender
method of the example, the path and the script block for the JavaScript file are initialized in the same manner as in the ASP.NET 2.0 version except that it is done before checking if the script is already registered. This is done because the ScriptManager
does not implement a method for checking if a script has already been registered; I assume that is because that is done by the register method itself, but I did not research enough to confirm that.
Once the script string has been created, an instance of the ScriptManager
is retrieved by passing the control's page to the ScriptManager
's static GetCurrent
method. Then, the ScriptManger
instance is checked to determine if the page actually contains a ScriptManager
, and if so, if it is handling partial page rendering (an UpdatePanel
is in use). If so, the JavaScript file is registered with the ScriptManager
's static RegisterClientScriptBlock
method; otherwise, this is done in the same manner as in the ASP.NET 2.0 version.
The ScriptManager
is also used to register the script block written by the RenderScript
method with the Render
method. Since the UpdatePanel
injects its contents into the DOM without reloading the page, the script is treated like a plain text node by the browser and unaware that it is intended to be executed after being injected. Registering the inline script with the ScriptManager
causes it to handle ensuring the script is executed.
Before registering the script with the ScriptManager
, a check to ensure it is actually possible and necessary is made in the same manner as in the OnPreRender
method. The trick to get the script written by the RenderScript
method into a string is to pass in a new HtmlTextWriter
constructed using a StringWriter
instance that can be referenced after the method is executed. Then, the script is retrieved by calling the StringWriter
's ToString
method and registered by calling the ScriptManager
's RegisterStartupScript
method. Otherwise, if there is no need or no ScriptManager
to register with, the script is written in the same manner as in the ASP.NET 2.0 version.
Like in the ASP.NET 2.0 version, the Render
method also registers the hidden fields for the control. If the script needs to be registered by the ScriptManager
, the hidden fields also need be registered with the ScriptManager
for the control to function properly. Otherwise, the hidden fields are registered in the same manner as in the ASP.NET 2.0 version.
What makes the AutoSuggest ASP.NET Server Control Library a library and not just a server control is that it implements the AutoSuggestListPage
for outputting a suggestion list in the format expected by the AutoSuggestTextBox
. The AutoSuggestList
control was created to make configuring the AutoSuggestListPage
in design view possible.
Basically, all the AutoSuggestList
control does is inherit from the AutoSuggestListBase
control to provide data binding functionality and delimiter properties, hides the irrelevant properties from the property grid, and displays a box on the page in design view for selecting the control for editing. Since outputting HTML and hiding properties from the property grid are already covered in this article, I will not provide any code examples from this control.
The AutoSuggestListPage
contains a protected field for accessing the AutoSuggestList
's properties which are wrapped so they can be accessed either through the control or from the AutoSuggestListPage
's properties. The AutoSuggestList
is declared as null
so it is necessary that the control be available with the same ID as the field name from the ASPX page for the AutoSuggestListPage
. To ensure that the list is output in the correct format expected by the AutoSuggestTextBox
, the OnLoad
and Render
methods of the base Page
class are overridden as seen in the example code below.
protected override void OnLoad(System.EventArgs e)
{
this.Response.AddHeader("Content-Type", "text/xml");
base.OnLoad(e);
}
protected override void Render(HtmlTextWriter output)
{
if (!DesignMode)
{
output.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
output.WriteLine("<listdata>");
bool multiValue = false;
foreach (ListItem item in this.Items)
multiValue = (item.Text != item.Value);
if (multiValue)
{
for (int itemIndex = 0; itemIndex < this.Items.Count; itemIndex++)
{
if (itemIndex == 0)
output.Write(this.Items[itemIndex].Text
+ this.TextValueDelimiter
+ this.Items[itemIndex].Value);
else if (itemIndex != this.Items.Count - 1)
output.Write(this.ItemDelimiter
+ this.Items[itemIndex].Text
+ this.TextValueDelimiter
+ this.Items[itemIndex].Value);
else
output.Write(this.ItemDelimiter
+ this.Items[itemIndex].Text
+ this.TextValueDelimiter
+ this.Items[itemIndex].Value);
}
}
else
{
for (int itemIndex = 0; itemIndex < this.Items.Count; itemIndex++)
{
if (itemIndex == 0)
output.Write(this.Items[itemIndex].Text);
else if (itemIndex != this.Items.Count - 1)
output.Write(this.ItemDelimiter
+ this.Items[itemIndex].Text);
else
output.Write(this.ItemDelimiter
+ this.Items[itemIndex].Text);
}
}
output.Write(Environment.NewLine + "</listdata>");
}
else
base.Render(output);
}
All that happens in the OnLoad
method is a header is added to the response to set the content type to XML and the OnLoad
method of the base Page
is called. This is done to ensure the page is treated as an XML file by the browser so the JavaScript class can parse the list items correctly.
The Render
method is overridden to output the suggestion list with the items and delimiters specified with the AutoSuggestList
control and prevent the base Page
from rendering anything else. If in DesignMode
, the base Page
is rendered normally so that the AutoSuggestList
control can be selected for editing.
In the previous section, I alluded to the fact that the AutoSuggestListPage
depends on an AutoSuggestList
control being on the linked ASPX file for it to function properly. So, adding an AutoSuggestListPage
to a web project involves adding a new page, changing the class to inherit from AutoSuggestListPage
, and adding an AutoSuggestList
control to the page from which the list can be configured. Or, you could just use the template provided and add an AutoSuggestListPage
to your project already configured with the requirements above and go straight to configuring the list.
Creating the template was a fairly straightforward process. Templates can be taken as far as having their own wizards built for them, which wasn't really necessary considering the ease of configuration from design view. Basically, all that was required for creating this template was for an AutoSuggestListPage
to be implemented as described above, then exporting the item to a template using the wizard available from selecting the Visual Studio File menu's Export Template... menu item.
- September 20, 2009 - Version 1.0