Click here to Skip to main content
15,867,851 members
Articles / Web Development / HTML
Article

Custom AutoCompleteExtender with multiple word suggestions

Rate me:
Please Sign up or sign in to vote.
4.27/5 (4 votes)
13 Oct 2006CPOL2 min read 120K   1.3K   61   18
Implements an AutoCompleteExtender for multiple word suggestions and to change the control's style.

Sample Image - CustomAutoCompleteExt.gif

Introduction

By default, the AutoCompleteExtender shows results from the entire value of one text box. With my implementation, it is possible to search more than one word inside a text box divided by comma (or another character). Every time a comma is written, the list of suggestions appears for the new word.

At the moment, AutoCompleteExtender doesn’t support the style for the popup list. We’ll implement these properties during the modifications for multiple suggestions.

Inherit the AutoCompleteProperties

The first step is to create C# classes for the new control CustomAutoCompleteExtender. We want to declare a class called CustomAutoCompleteProperties that inherits from AutoCompleteProperties and adds the support for the “multiple word suggestions” and CSS styles properties. For “multiple word suggestions”, we need one property: SeparatorChar. With this property, we are able to divide word-to-word and open the suggestions’ list for the new word written.

C#
namespace CustomAtlas.Controls
{
    public class CustomAutoCompleteProperties : AutoCompleteProperties
    {
        public string SeparatorChar
        {
            get
            {
                object obj = base.ViewState["SeparatorChar"];
                if (obj != null) return (string)obj;
                else return ",";
            }
            set
            {
                base.ViewState["SeparatorChar"] = value;
                base.OnChanged(EventArgs.Empty);
            }
        }
        public string CssList
        {
            get
            {
                object obj = base.ViewState["CssList"];
                if (obj != null) return (string)obj;
                else return String.Empty;
            }
            set
            {
                base.ViewState["CssList"] = value;
                base.OnChanged(EventArgs.Empty);
            }
        }
        public string CssItem
        {
            get
            {
                object obj = base.ViewState["CssItem"];
                if (obj != null) return (string)obj;
                else return String.Empty;
            }
            set
            {
                base.ViewState["CssItem"] = value;
                base.OnChanged(EventArgs.Empty);
            }
        }
        public string CssHoverItem
        {
            get
            {
                object obj = base.ViewState["CssHoverItem"];
                if (obj != null) return (string)obj;
                else return String.Empty;
            }
            set
            {
                base.ViewState["CssHoverItem"] = value;
                base.OnChanged(EventArgs.Empty);
            }
        }
    }
}

The CssList, CssItem, and CssHoverItem are necessary to build the control’s style. CssList provides to draw the list’s box, and CssItem and CssHoverItem draw every item in the list.

Inherit the AutoCompleteExtender

First step done, we continue with the Extender. In this case, we inherit from the AutoCompleteExtender class and add new properties to the control:

C#
namespace CustomAtlas.Controls
{
    public class CustomAutoCompleteExtender : AutoCompleteExtender
    {
        protected override void RenderScript(
          Microsoft.Web.Script.ScriptTextWriter writer, Control targetControl)
        {
            // get our CustomAutoCompleteProperties
            CustomAutoCompleteProperties cacp = 
                (CustomAutoCompleteProperties)
                 base.GetTargetProperties(targetControl);
            if ((cacp != null) && cacp.Enabled)
            {
                // check if the ServicePath is set
                string _ServicePath = cacp.ServicePath;
                if (_ServicePath == String.Empty)
                {
                    _ServicePath = this.ServicePath;
                }
                if (_ServicePath == String.Empty)
                {
                    throw new InvalidOperationException("The ServicePath " + 
                                  "must be set for AutoCompleteBehavior");
                }
                // check if the ServiceMethod is set
                string _ServiceMethod = cacp.ServiceMethod;
                if (_ServiceMethod == String.Empty)
                {
                    _ServiceMethod = this.ServiceMethod;
                }
                if (_ServiceMethod == String.Empty)
                {
                    throw new InvalidOperationException("The ServiceMethod " + 
                                    "must be set for AutoCompleteBehavior");
                }
                // search for the completion list control if an ID was supplied
                Control c = null;
                string drp = this.DropDownPanelID;
                if (drp != String.Empty)
                {
                    c = this.NamingContainer.FindControl(drp);
                    if (c == null)
                    {
                        throw new InvalidOperationException("The specified " + 
                                       "DropDownPanelID is not a valid ID");
                    }
                }
                // write the Atlas markup on page
                writer.WriteStartElement("autoComplete");
                writer.WriteAttributeString("serviceURL", 
                    base.ResolveClientUrl(_ServicePath));
                writer.WriteAttributeString("serviceMethod", _ServiceMethod);
                if (c != null)
                  writer.WriteAttributeString("completionList", c.ClientID);
                writer.WriteAttributeString("minimumPrefixLength",
                             cacp.MinimumPrefixLength.ToString());
                writer.WriteAttributeString("separatorChar", 
                                        cacp.SeparatorChar);
                writer.WriteAttributeString("cssList", cacp.CssList);
                writer.WriteAttributeString("cssItem", cacp.CssItem);
                writer.WriteAttributeString("cssHoverItem", 
                                        cacp.CssHoverItem);
                writer.WriteEndElement();
            }
        }
    }
}

Implementing the custom AutoCompleteBehavior

Now that the control is ready, we have only to manage the client-side code to send the right value to the Web Service (the comma-separated word) and to apply our custom CSS style.

Searching for the AutoCompleteBehavior class in the Atlas.js file, we can copy it and register our custom class:

JavaScript
Type.registerNamespace('Custom.UI');
Custom.UI.AutoCompleteBehavior = function() {
    Custom.UI.AutoCompleteBehavior.initializeBase(this);
    
    var _appURL;
    var _serviceURL;
    var _serviceMethod;
    var _separatorChar = ',';
    var _minimumPrefixLength = 3;
    var _cssList;
    var _cssItem;
    var _cssHoverItem;
    var _completionSetCount = 10;
    var _completionInterval = 1000;
    var _completionListElement;
    var _popupBehavior;
    
    var _timer;
    var _cache;
    var _currentPrefix;
    var _selectIndex;
    
    var _focusHandler;
    var _blurHandler;
    var _keyDownHandler;
    var _mouseDownHandler;
    var _mouseUpHandler;
    var _mouseOverHandler;
    var _tickHandler;
    
    this.get_appURL = function() {
        return _appURL;
    }
    this.set_appURL = function(value) {
        _appURL = value;
    }
    this.get_completionInterval = function() {
        return _completionInterval;
    }
    this.set_completionInterval = function(value) {
        _completionInterval = value;
    }
    
    this.get_completionList = function() {
        return _completionListElement;
    }
    this.set_completionList = function(value) {
        _completionListElement = value;
    }
    
    this.get_completionSetCount = function() {
        return _completionSetCount;
    }
    this.set_completionSetCount = function(value) {
        _completionSetCount = value;
    }
    
    this.get_minimumPrefixLength = function() {
        return _minimumPrefixLength;
    }
    this.set_minimumPrefixLength = function(value) {
        _minimumPrefixLength = value;
    }
    
    this.get_separatorChar = function() {
        return _separatorChar;
    }
    this.set_separatorChar = function(value) {
        _separatorChar = value;
    }
    
    this.get_serviceMethod = function() {
        return _serviceMethod;
    }
    this.set_serviceMethod = function(value) {
        _serviceMethod = value;
    }
    
    this.get_serviceURL = function() {
        return _serviceURL;
    }
    this.set_serviceURL = function(value) {
        _serviceURL = value;
    }
    
    /* styles */
    this.get_cssList = function() {
        return _cssList;
    }
    this.set_cssList = function(value) {
        _cssList = value;
    }
    this.get_cssItem = function() {
        return _cssItem;
    }
    this.set_cssItem = function(value) {
        _cssItem = value;
    }
    this.get_cssHoverItem = function() {
        return _cssHoverItem;
    }
    this.set_cssHoverItem = function(value) {
        _cssHoverItem = value;
    }


    this.dispose = function() {
        if (_timer) {
            _timer.tick.remove(_tickHandler);
            _timer.dispose();
        }
        
        var element = this.control.element;
        element.detachEvent('onfocus', _focusHandler);
        element.detachEvent('onblur', _blurHandler);
        element.detachEvent('onkeydown', _keyDownHandler);
        
        _completionListElement.detachEvent('onmousedown', _mouseDownHandler);
        _completionListElement.detachEvent('onmouseup', _mouseUpHandler);
        _completionListElement.detachEvent('onmouseover', _mouseOverHandler);
        
        _tickHandler = null;
        _focusHandler = null;
        _blurHandler = null;
        _keyDownHandler = null;
        _mouseDownHandler = null;
        _mouseUpHandler = null;
        _mouseOverHandler = null;
        Sys.UI.AutoCompleteBehavior.callBaseMethod(this, 'dispose');
    }
    this.getDescriptor = function() {
        var td = Custom.UI.AutoCompleteBehavior.callBaseMethod(this, 
                                                   'getDescriptor');
        td.addProperty('completionInterval', Number);
        td.addProperty('completionList', Object, false, 
                         Sys.Attributes.Element, true);
        td.addProperty('completionSetCount', Number);
        td.addProperty('minimumPrefixLength', Number);
        td.addProperty('separatorChar', String);
        td.addProperty('cssList', String);
        td.addProperty('cssItem', String);
        td.addProperty('cssHoverItem', String);
        td.addProperty('serviceMethod', String);
        td.addProperty('serviceURL', String);
        td.addProperty('appURL', String);
        return td;
    }
    
    this.initialize = function() {
        Custom.UI.AutoCompleteBehavior.callBaseMethod(this, 'initialize');
        _tickHandler = Function.createDelegate(this, this._onTimerTick);
        _focusHandler = Function.createDelegate(this, this._onGotFocus);
        _blurHandler = Function.createDelegate(this, this._onLostFocus);
        _keyDownHandler = Function.createDelegate(this, this._onKeyDown);
        _mouseDownHandler = Function.createDelegate(this, 
                                  this._onListMouseDown);
        _mouseUpHandler = Function.createDelegate(this, this._onListMouseUp);
        _mouseOverHandler = Function.createDelegate(this, 
                                  this._onListMouseOver);
        
        _timer = new Sys.Timer();
        _timer.set_interval(_completionInterval);
        _timer.tick.add(_tickHandler);
        
        var element = this.control.element;
        element.autocomplete = "off";
        element.attachEvent('onfocus', _focusHandler);
        element.attachEvent('onblur', _blurHandler);
        element.attachEvent('onkeydown', _keyDownHandler);
        
        var elementBounds = Sys.UI.Control.getBounds(element);
        
        if (!_completionListElement) {
            _completionListElement = document.createElement('DIV');
            document.body.appendChild(_completionListElement);
        }
        
        // apply styles
        var completionListStyle = _completionListElement.style;
        if ( _cssList != '' ) 
        {
            _completionListElement.className = _cssList;
        } 
        else 
        {
            completionListStyle.backgroundColor = 'window';
            completionListStyle.color = 'windowtext';
            completionListStyle.border = 'solid 1px buttonshadow';
            completionListStyle.cursor = 'default';
        }
        // default styles
        completionListStyle.unselectable = 'unselectable';
        completionListStyle.overflow = 'hidden';
        completionListStyle.visibility = 'hidden';
        completionListStyle.width = (elementBounds.width - 2) + 'px';
        
        _completionListElement.attachEvent('onmousedown', _mouseDownHandler);
        _completionListElement.attachEvent('onmouseup', _mouseUpHandler);
        _completionListElement.attachEvent('onmouseover', _mouseOverHandler);
        document.body.appendChild(_completionListElement);
        var popupControl = new Sys.UI.Control(_completionListElement);
        _popupBehavior = new Sys.UI.PopupBehavior();
        _popupBehavior.set_parentElement(element);
        _popupBehavior.set_positioningMode(Sys.UI.PositioningMode.BottomLeft);
        popupControl.get_behaviors().add(_popupBehavior);
        _popupBehavior.initialize();
        popupControl.initialize();
    }
    
    this._hideCompletionList = function() {
        _popupBehavior.hide();
        _completionListElement.innerHTML = '';
        _selectIndex = -1;
    }
    
    this._highlightItem = function(item) {
        var children = _completionListElement.childNodes;
        // non-selecteditems
        for (var i = 0; i < children.length; i++) {
            var child = children[i];
            if (child != item) {
                if ( _cssItem != '' ) 
                {
                    child.className = _cssItem;
                }
                else
                {
                    child.style.backgroundColor = 'window';
                    child.style.color = 'windowtext';
                }
            }
        }
        // selected item
        if ( _cssHoverItem != '' ) 
        {
            item.className = _cssHoverItem;
        }
        else 
        {
            item.style.backgroundColor = 'highlight';
            item.style.color = 'highlighttext';
        }
    }
    
    this._onListMouseDown = function() {
        if (window.event.srcElement != _completionListElement) {
            this._setText(window.event.srcElement.firstChild.nodeValue);
        }
    }
    
    this._onListMouseUp = function() {
        this.control.focus();
    }
    
    this._onListMouseOver = function() {
        var item = window.event.srcElement;
        _selectIndex = -1;
        this._highlightItem(item);
    }
    this._onGotFocus = function() {
        _timer.set_enabled(true);
    }
    
    this._onKeyDown = function() {
        var e = window.event;
        if (e.keyCode == 27) {
            this._hideCompletionList();
            e.returnValue = false;
        }
        else if (e.keyCode == Sys.UI.Key.Up) {
            if (_selectIndex > 0) {
                _selectIndex--;
                this._highlightItem(
                  _completionListElement.childNodes[_selectIndex]);
                e.returnValue = false;
            }
        }
        else if (e.keyCode == Sys.UI.Key.Down) {
            if (_selectIndex < (_completionListElement.childNodes.length - 1)) {
                _selectIndex++;
                this._highlightItem(
                  _completionListElement.childNodes[_selectIndex]);
                e.returnValue = false;
            }
        }
        else if (e.keyCode == Sys.UI.Key.Return) {
            if (_selectIndex != -1) {
                this._setText(_completionListElement.childNodes[_selectIndex].
                                                        firstChild.nodeValue);
                e.returnValue = false;
            }
        }
        
        if (e.keyCode != Sys.UI.Key.Tab) {
            _timer.set_enabled(true);
        }
    }
    
    this._onLostFocus = function() {
        _timer.set_enabled(false);
        this._hideCompletionList();
    }
    
    function _onMethodComplete(result, response, context) {
        var acBehavior = context[0];
        var prefixText = context[1];
        acBehavior._update(prefixText, result,  true);
    }
    
    this._onTimerTick = function(sender, eventArgs) {
        if (_serviceURL && _serviceMethod) {
        
            var text = this.control.element.value;
            
            if ( text.lastIndexOf(_separatorChar) > -1 ) 
            {
                // found separator char in the text
                var pos = text.lastIndexOf(_separatorChar);
                pos++;
                text = text.substring(pos, (text.length));
                text = text.trim();
            }
            
            if (text.trim().length < _minimumPrefixLength) {
                this._update('', null,  false);
                return;
            }
            
            if (_currentPrefix != text) {
                _currentPrefix = text;
                if (_cache && _cache[text]) {
                    this._update(text, _cache[text],  false);
                    return;
                }
                
                Sys.Net.ServiceMethod.invoke(_serviceURL, _serviceMethod, 
                  _appURL, { prefixText : _currentPrefix, count: 
                  _completionSetCount }, _onMethodComplete, null, 
                  null, null, [ this, text ]);
            }
        }
    }
    
    this._setText = function(text) {
        _timer.set_enabled(false);
        _currentPrefix = text;
        if (Sys.UI.TextBox.isInstanceOfType(this.control)) {
            this.control.set_text(text);
        }
        else {
            var currentValue = this.control.element.value;
            if ( currentValue.lastIndexOf(_separatorChar) > -1 ) 
            {
                // found separator char in the text
                var pos = currentValue.lastIndexOf(_separatorChar);
                pos++;
                currentValue = currentValue.substring(0, pos) + text;
            } 
            else 
            {
                // no separator char found
                currentValue = text;
            }
            this.control.element.value = currentValue;
        }
        this._hideCompletionList();
    }
    
    this._update = function(prefixText, completionItems, cacheResults) {
        if (cacheResults) {
            if (!_cache) {
                _cache = { };
            }
            _cache[prefixText] = completionItems;
        }
        _completionListElement.innerHTML = '';
        _selectIndex = -1;
        if (completionItems && completionItems.length) {
            for (var i = 0; i < completionItems.length; i++) {
                var itemElement = document.createElement('div');
                itemElement.appendChild(
                     document.createTextNode(completionItems[i]));
                itemElement.__item = '';
                if ( _cssItem != '' ) 
                {
                    itemElement.className = _cssItem;
                }
                else
                {                
                    var itemElementStyle = itemElement.style;
                    itemElementStyle.padding = '1px';
                    itemElementStyle.textAlign = 'left';
                    itemElementStyle.textOverflow = 'ellipsis';
                    itemElementStyle.backgroundColor = 'window';
                    itemElementStyle.color = 'windowtext';
                }                
                _completionListElement.appendChild(itemElement);
            }
            _popupBehavior.show();
        }
        else {
            _popupBehavior.hide();
        }
    }
}
Custom.UI.AutoCompleteBehavior.registerSealedClass(
          'Custom.UI.AutoCompleteBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('script', 'autoComplete', 
                           Custom.UI.AutoCompleteBehavior);

First, we have to add the four properties created in the Extender class. Now, we are able to use the properties’ values of the control placed in the .aspx page container.

Searching inside the code, there is a function _onTimerTick used to show the list after a time delay. In this function, we’ll intercept the value to send to the Web Service and change it as we want:

JavaScript
var text = this.control.element.value;
if ( text.lastIndexOf(_separatorChar) > -1 ) 
{
 // found separator char in the text, choosing the right word
 var pos = text.lastIndexOf(_separatorChar);
 pos++;
 text = text.substring(pos, (text.length));
text = text.trim();
}

Now, when the user types a value in the text box, the AutoCompleteBehavior verifies the presence of the separator char. If present, the text sent to the Web Service is the last word found and not the entire value of the text box.

Like a standard, I use to save .js code into scriptLibrary folder.

Let’s try it

The situation: AutoCompleteBehavior.js saved into the scriptLibrary folder, CustomAutoCompleteProperties.cs and CustomAutoCompleteExtender.cs saved into the App_Code folder... we are ready to try it.

Create a new .aspx file, and add a reference to the CustomAutoCompleteExtender class, and place the controls in the page:

ASP.NET
<%@ Page Language="C#" AutoEventWireup="true" 
             CodeFile="Default.aspx.cs" Inherits="_Default" %>
<%@ Register Namespace="CustomAtlas.Controls" TagPrefix="customAtlas" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 
     "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>CustomAutoCompleteExtender</title>
    <link href="StyleSheet.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
        <atlas:ScriptManager ID="scriptManager" runat="server">
            <Scripts>
                <atlas:ScriptReference ScriptName="Custom" />
                <atlas:ScriptReference 
                   Path="scriptLibrary/CustomAutoCompleteBehavior.js" />
            </Scripts>
        </atlas:ScriptManager>
        
        <div>
            <asp:TextBox ID="txtSuggestions" 
                     runat="server"></asp:TextBox>
            <customAtlas:CustomAutoCompleteExtender 
                   ID="CustomAutoCompleteExtender1" runat="server">
                <customAtlas:CustomAutoCompleteProperties
                                 TargetControlID="txtSuggestions"
                                 ServicePath="WebServiceDemo.asmx"
                                 ServiceMethod="GetSuggestions"
                                 MinimumPrefixLength="1"
                                 SeparatorChar=","
                                 CssList="autoCompleteList"
                                 CssItem="autoCompleteItem" 
                                 CssHoverItem="autoCompleteHoverItem"
                                 Enabled="true" />
            </customAtlas:CustomAutoCompleteExtender>       
        </div>
    </form>
</body>
</html>

Call a demo Web Service (returns the same value written 10 times), digit one word, then comma (comma is the default char), and starting with a new word, a list with suggestions for a new item is shown.

Hope this will be helpful.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer HTML.it
Italy Italy
Simple C# Developer.

Comments and Discussions

 
GeneralThehttp://www.treehousesystems.com/CustomAutoComplete_MSFTAJAXRelease1.0.zip link doesn't work any more. Pin
roeih17-Aug-09 21:08
roeih17-Aug-09 21:08 
GeneralAssertion Failed Pin
kavyamark17-Aug-09 7:19
kavyamark17-Aug-09 7:19 
QuestionThe string parameter 'url' cannot be null or empty. Parameter name: url [modified] Pin
Rizwan Javed27-Jun-09 1:49
Rizwan Javed27-Jun-09 1:49 
Generalproblem with the language used Pin
navenchary1-May-09 0:13
navenchary1-May-09 0:13 
Generalkey-value pair retrieval Pin
thas0228-Mar-09 0:51
thas0228-Mar-09 0:51 
Questionis it possible in windows application ? Pin
Janardhan Mysore18-Sep-08 2:45
Janardhan Mysore18-Sep-08 2:45 
GeneralThanks Pin
Summer_son3-Nov-07 8:41
Summer_son3-Nov-07 8:41 
GeneralAutocomplete Extender Pin
sudhaconn16-Apr-07 19:52
sudhaconn16-Apr-07 19:52 
GeneralRe: Autocomplete Extender Pin
pagerss13-Oct-07 3:25
pagerss13-Oct-07 3:25 
Questionproblem of CustomAutoCompleteProperties Pin
vero19654-Apr-07 0:25
vero19654-Apr-07 0:25 
GeneralCustomAutoComplete for Opera Pin
e0ne16-Feb-07 5:10
e0ne16-Feb-07 5:10 
GeneralChanges for Final Release of Microsoft Ajax Extensions Pin
malsmith30-Jan-07 6:07
malsmith30-Jan-07 6:07 
GeneralRe: Changes for Final Release of Microsoft Ajax Extensions Pin
vivekthangaswamy5-Jun-07 19:20
professionalvivekthangaswamy5-Jun-07 19:20 
GeneralUnknown error Pin
kissz7-Dec-06 21:15
kissz7-Dec-06 21:15 
QuestionAutoCompleteExtender is missing in ASP.NET Ajax 1.0 Pin
Anand Morbia27-Nov-06 1:32
Anand Morbia27-Nov-06 1:32 
AnswerRe: AutoCompleteExtender is missing in ASP.NET Ajax 1.0 Pin
BBeary6-Dec-06 0:56
BBeary6-Dec-06 0:56 
GeneralUsing a codebehind method inseat a webservice Pin
rm800018-Oct-06 2:43
rm800018-Oct-06 2:43 
AnswerRe: Using a codebehind method inseat a webservice Pin
Gianni Marzaloni (ZofM)19-Oct-06 5:31
Gianni Marzaloni (ZofM)19-Oct-06 5:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.