65.9K
CodeProject 正在变化。 阅读更多。
Home

具有多词建议的自定义 AutoCompleteExtender

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.27/5 (4投票s)

2006年10月13日

CPOL

2分钟阅读

viewsIcon

122738

downloadIcon

1302

实现一个 AutoCompleteExtender,用于多词建议并更改控件的样式。

Sample Image - CustomAutoCompleteExt.gif

引言

默认情况下,AutoCompleteExtender 显示来自一个文本框的整个值的结果。通过我的实现,可以在一个用逗号(或其他字符)分隔的文本框中搜索多个单词。每次写入逗号时,都会为新单词显示建议列表。

目前,AutoCompleteExtender 不支持弹出列表的样式。我们将在修改多词建议时实现这些属性。

继承 AutoCompleteProperties

第一步是为新的控件 CustomAutoCompleteExtender 创建 C# 类。我们想要声明一个名为 CustomAutoCompleteProperties 的类,该类继承自 AutoCompleteProperties 并添加对“多词建议”和 CSS 样式属性的支持。对于“多词建议”,我们需要一个属性:SeparatorChar。通过此属性,我们可以逐字分隔,并为编写的新单词打开建议列表。

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);
            }
        }
    }
}

CssListCssItemCssHoverItem 对于构建控件的样式是必需的。CssList 用于绘制列表框,而 CssItemCssHoverItem 用于绘制列表中的每个项目。

继承 AutoCompleteExtender

完成第一步后,我们继续使用 Extender。在这种情况下,我们继承自 AutoCompleteExtender 类,并向控件添加新属性

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();
            }
        }
    }
}

实现自定义 AutoCompleteBehavior

现在控件已准备就绪,我们只需要管理客户端代码,将正确的值发送到 Web 服务(逗号分隔的单词)并应用我们的自定义 CSS 样式。

Atlas.js 文件中搜索 AutoCompleteBehavior 类,我们可以复制它并注册我们的自定义类

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);

首先,我们必须添加在 Extender 类中创建的四个属性。现在,我们可以使用放置在 .aspx 页面容器中的控件的属性值。

在代码中搜索,有一个函数 _onTimerTick 用于在延迟一段时间后显示列表。在这个函数中,我们将拦截要发送到 Web 服务的值,并根据需要更改它

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();
}

现在,当用户在文本框中键入值时,AutoCompleteBehavior 会验证分隔符字符是否存在。如果存在,则发送到 Web 服务的文本是找到的最后一个单词,而不是文本框的整个值。

像标准一样,我习惯将 .js 代码保存在 scriptLibrary 文件夹中。

让我们试一试

情况:AutoCompleteBehavior.js 保存在 scriptLibrary 文件夹中,CustomAutoCompleteProperties.csCustomAutoCompleteExtender.cs 保存在 App_Code 文件夹中...我们准备好试一试了。

创建一个新的 .aspx 文件,并添加对 CustomAutoCompleteExtender 类的引用,并将控件放置在页面中

<%@ 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>

调用一个演示 Web 服务(返回与编写的相同值 10 次),输入一个单词,然后输入逗号(逗号是默认字符),并以一个新单词开头,将显示一个带有新项目建议的列表。

希望这会有所帮助。

© . All rights reserved.