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

Custom AutoCompleteExtender with multiple word suggestions

4.27/5 (4 votes)
13 Oct 2006CPOL2 min read 1   1.3K  
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)