Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Using and Developing an AutoSuggest ASP.NET Server Control Library

4.89/5 (24 votes)
20 Sep 2009Zlib37 min read 92.6K   2.1K  
An article on using an auto-suggest text-box/drop-down list ASP.NET server control library which can be populated inline or using AJAX, and how it was developed.

AutoSuggestTextBox in action.

Table of Contents

Introduction

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.

Background

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 DropDownLists 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.

Using the Code

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.

Set AutoSuggestTextBox Script and Image Paths

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.

Set AutoSuggestTextBox Appearance

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.

TextBox Appearance

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.

Suggestion List Appearance

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.

Set AutoSuggestTextBox Behavior

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.

General Settings

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.

Multiselection

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.

Client Scripting

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:

JavaScript
function(index, control)
{
  /*alert('Selected key: "' + control.keywords[index]
     + '" with value: "' + control.values[index] + '"');*/
}

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.

Text-Box and Drop-Down Mode

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.

Populating Suggestion Lists

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.

Manual List Population

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:

XML
<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>

Data Bound Population

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.

Programmatic List Population

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:

C#
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;
        }
    }
}

AJAX List Population

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:

XML
<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.

AutoSuggest ASP.NET Server Control Library Reference

AutoSuggest Namespace

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.

Image 2

AutoSuggestListBase

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.

AutoSuggestList

The AutoSuggestList class inherits from AutoSuggestListBase to be used as a control to configure AutoSuggestListPages 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.

AutoSuggestListPage

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.

AutoSuggestTextBox

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.
  • 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.

How it Works

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 AutoSuggestListPages to web projects with the New Item dialog.

Output Text Input

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.

C#
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.

Use ViewState

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.

C#
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.

Implement IPostBackDataHandler

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.

C#
#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.

C#
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 Bound ListControl Behavior

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.

C#
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.

AutoSuggest JavaScript Class

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.

Output AutoSuggest Script Calls

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.

C#
protected void RenderScript(HtmlTextWriter output)
{
    if (this.Visible)
    {
        // Checking if the Items property's text and value properties differ
        // to determine if both values need to be set for each JavaScript Array
        // item needs to store both values.
        bool multiValue = false;
        foreach (ListItem item in this.Items)
        {
            multiValue = (item.Text != item.Value);
            if (multiValue)
                break;
        }
        // Opening script tag.
        output.WriteBeginTag("script");
        output.WriteAttribute("type", "text/javascript");
        output.WriteLine(">");
        // Declaring and instantiating the suggestion array if no AJAX
        // URL specified.
        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("];");
        }
        // Declaring and instantiating the autosuggest object.
        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
                + ");");

        // Setting the autosuggest object properties.
        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;");
        // Adding each parameters to the array with names and values
        // based on parameter type.
        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 + "'];");
        }
        // Setting the autosuggest object image paths.
        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.

Designer Functionality

To make editing the AutoSuggestTextBox and AutoSuggestListBase property values as easy as possible, the controls take advantage of the many UITypeEditors that come with the .NET Framework. Of course, custom UITypeEditors 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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [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.
  • C#
    [Browsable(false)]
    public int SelectedIndex //...

As you can see, there are quite a few attributes and UITypeEditors 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.

Script Registration

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.

ASP.NET 2.0

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.

C#
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.

ASP.NET 3.5 AJAX

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.

C#
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.

AutoSuggestListPage for AJAX Output

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.

C#
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.

Item Template for AutoSuggestListPage

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.

History

  • September 20, 2009 - Version 1.0
    • Initial release.

License

This article, along with any associated source code and files, is licensed under The zlib/libpng License