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.
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:
namespace CustomAtlas.Controls
{
public class CustomAutoCompleteExtender : AutoCompleteExtender
{
protected override void RenderScript(
Microsoft.Web.Script.ScriptTextWriter writer, Control targetControl)
{
CustomAutoCompleteProperties cacp =
(CustomAutoCompleteProperties)
base.GetTargetProperties(targetControl);
if ((cacp != null) && cacp.Enabled)
{
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");
}
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");
}
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");
}
}
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:
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;
}
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);
}
var completionListStyle = _completionListElement.style;
if ( _cssList != '' )
{
_completionListElement.className = _cssList;
}
else
{
completionListStyle.backgroundColor = 'window';
completionListStyle.color = 'windowtext';
completionListStyle.border = 'solid 1px buttonshadow';
completionListStyle.cursor = 'default';
}
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;
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';
}
}
}
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 )
{
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 )
{
var pos = currentValue.lastIndexOf(_separatorChar);
pos++;
currentValue = currentValue.substring(0, pos) + text;
}
else
{
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:
var text = this.control.element.value;
if ( text.lastIndexOf(_separatorChar) > -1 )
{
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:
<%@ 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.