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

AJAX 在这里 - 第 3 部分:自动完成文本框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (32投票s)

2005年5月2日

8分钟阅读

viewsIcon

498314

downloadIcon

3745

一个自定义的 AJAX - ASP.NET 控件。

引言

如果您一直在关注本系列文章,那么您知道在第 1 部分中,我们简要讨论了异步 JavaScript 和 XML,简称 AJAX。我们创建了 CallBackObject 来帮助简化回调的启动过程。在第 2 部分中,我们研究了如何将 CallBackObject 与 ASP.NET 控件和事件集成。现在,在第 3 部分,我们将把这些知识付诸实践,并利用我们迄今为止所学的一切来创建一个自定义 ASP.NET 控件。正如您可能猜到的,该控件就是自动完成文本框 (ACTB)。

在开始讲解精彩内容之前,我想说这个控件还有很大的改进空间。功能、设计时集成和易用性都可以得到极大的改善。这部分留给您!

好东西

在讨论创建控件之前,我想向您展示该控件的易用性。我们将从列表 1 和列表 2 (Actb.aspxActb.aspx.cs) 开始。在您平息了压倒性的兴奋之后,我们将检查 ACTB 列表 3 (AutoCompleteTextBox.js) 的 JavaScript 部分和 ASP.NET 部分列表 4 (AutoCompleteTextBox.cs)。

Actb.aspx

此页面非常简单。我们正在为用户设置一些输入字段。你们中的大多数人可能会注意到并非所有国家都使用邮政编码。我将其作为演示,说明如何根据其他字段的值来自定义自动完成文本框的结果。(稍后将详细介绍。)要使用我们的自定义控件,我们必须在页面中注册程序集并为其指定一个标签前缀。我已谦虚地使用了我的首字母缩写作为标签前缀,但您可以选择任何您想要的名称。我们还指定了控件可以找到的命名空间和程序集名称。

<%@ Register TagPrefix="wcp" Namespace="WCPierce.Web.UI.WebControls" 
           Assembly="WCPierce.Web" %>

在此之后,唯一有趣的 HTML 部分是 ACTB 的实际声明。

<wcp:AutoCompleteTextBox runat="server" id="actbCountry" 
  OnTextChanged="actbCountry_TextChanged" ListItemCssClass="ListItem"
  ListItemHoverCssClass="ListItemHover" />

此声明几乎与普通的 ASP TextBox 相同。唯一添加的属性是用于美化我们的下拉列表的 ListItemCssClassListItemHoverCssClass。这些 CSS 类定义在外部样式表 AutoCompleteTextBox.css 中。

Actb.aspx.cs

当我们检查代码隐藏文件时,事情开始变得稍微有趣一些。首先需要注意的是代码顶部的 using 语句。

. . .
using Microsoft.ApplicationBlocks.Data;

using WCPierce.Web;
using WCPierce.Web.UI.WebControls;

Microsoft 提供的 Data Application Block 用于简化一些数据库访问代码。其他两个 using 指令使我们能够访问 Auto Complete TextBox 和 CallBackHelper 类。

请注意我们在 HTML 中引用的 actbCountry_TextChanged 事件。此函数中的所有内容都包含在 try/catch 块中,并且任何错误都通过使用 CallBackHelper.HandleError 返回给客户端。

try
{
  //. . . 
}
catch(Exception ex)
{
  CallBackHelper.HandleError( ex );
}

这一点很重要,因为回调期间发生的错误可能非常难以调试(相信我)。有了这段代码和正确的 JavaScript,调试错误就变得轻而易举。

第一步是检查 txtZip 输入字段的内容。如果其中有值,actbCountry 将默认设置为“United States”(抱歉,其他使用邮政编码的国家)。

if( txtZip.Text.Length > 0 )
{
  CallBackHelper.Write( "United States" );
}

对于下一部分代码,重要的是要记住,每次用户在 ACTB 中按下按键时,此事件都会触发。因此,如果 Zip 字段为空,我们会获取用户当前在 Country 字段中输入的内容,例如,我们使用“U”,然后在数据库中搜索每个以字母“U”开头的国家。

AutoCompleteTextBox actb = s as AutoCompleteTextBox;
string str = String.Format("SELECT [Text] 
                            FROM Lists 
                            WHERE ListName='Country' 
                            AND [Text] LIKE '{0}%' 
                            ORDER BY [Text]", actb.Text);

所有匹配的国家(乌干达、乌克兰、乌拉圭等)都包含在 SqlDataReader 中。然后,我们将自动完成文本框的 DataSource 属性设置为 SqlDataReader。此过程与用于绑定到 DropDownListDataGrid 的过程相同。

SqlDataReader sdr = SqlHelper.ExecuteReader(@"Server=(local);
                                              Database=DotNetNuke;
                                              Integrated Security=SSPI;", 
                                              CommandType.Text, str);
actb.DataSource = sdr;
actb.DataTextField = "Text";
actb.BindData();

精明的读者会注意到这里唯一的区别是调用了 BindData()。通常,对于 DataBound 控件,您会调用 DataBind()。不幸的是,我无法使其正常工作。原因有点复杂,但如果您尝试在禁用了 ViewState 的 DataGrid 中使用自动完成文本框,您就可以看出原因。

就这样!我已经将所有繁重的工作封装在 ACTB 控件中。您现在可以像使用其他数据绑定控件一样使用它。请访问此网站,观看 ACTB 的演示。将 Zip 留空,然后在 Country 框中输入一个字母,瞧,您应该会看到一个带有匹配项的下拉列表,以及列表中第一个国家/地区的自动完成功能。太棒了!

AutoCompleteTextBox.js

在开始讨论这个小宝石之前,我需要感谢那些帮助我实现这个控件的伟大个人

来自上述两篇文章的概念加上 CallBackObject 产生了 ACTB。这一部分有很多代码,与其逐行讲解(而且我的手指快累了),我只讲解新的部分。

AutoCompleteTextBox.prototype.TextBox_KeyUp = function(oEvent)
{
  var iKeyCode = oEvent.keyCode;

  if( iKeyCode == 8 )
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    return;
  }
  else if( iKeyCode == 16 || iKeyCode == 20 )
  {
    this.DoAutoSuggest = true;
  }
  else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || 
                         (iKeyCode >= 112 && iKeyCode <= 123)) 
  {
    return;
  }
  else
  {
    this.DoAutoSuggest = true;
  }
  
  var txt = this.TextBox.value;
  if( txt.length > 0 )
  {
    this.Cbo.DoCallBack(this.TextBox.name, txt);
  }
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    this.Cbo.AbortCallBack();
  }
}

每当在 ACTB 中按下按键时,此事件就会触发。我们进行一些检查,查看按下了哪个键以及文本框中是否有任何数据。如果有,我们就调用 Cbo.DoCallBack。如您在第 1 部分中所知,这就是启动服务器端请求的操作。

当服务器端请求完成时,我们的方法 AutoCompleteTextBox.prototype.Cbo_Complete 将被执行,我们处理结果。

AutoCompleteTextBox.prototype.Cbo_Complete = function(responseText, responseXML)
{ 
  while ( this.Div.hasChildNodes() )
    this.Div.removeChild(this.Div.firstChild);
            
  // get all the matching strings from the server response
  var aStr = responseText.split('\n');
         
  // add each string to the popup-div
  var i, n = aStr.length;
 
  if( n > 0 )
  {
    for ( i = 0; i < n; i++ )
    {
      var oDiv = document.createElement('div');
      this.Div.appendChild(oDiv);
      try
      {
        oDiv.innerHTML    = aStr[i];
      }
      catch(e)
      {
        this.Cbo_Error('405','Error','Text returned from Call Back was invalid');
        return;
      }
      oDiv.noWrap       = true;
      oDiv.style.width  = '100%';
      oDiv.className    = this.ListItemClass;
      oDiv.onmousedown  = AutoCompleteTextBox.prototype.Div_MouseDown;
      oDiv.onmouseover  = AutoCompleteTextBox.prototype.Div_MouseOver;
      oDiv.onmouseout   = AutoCompleteTextBox.prototype.Div_MouseOut;
      oDiv.AutoCompleteTextBox = this;            
    }
    this.Div.style.display = 'block';
      
    if( this.DoAutoSuggest == true )
      this.AutoSuggest( aStr );
  }  
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display='none';
  }
}

从服务器返回的数据是以换行符分隔的国家/地区名称列表。在我们删除下拉列表中任何当前条目后,我们将返回值拆分为国家/地区名称数组。然后,我们遍历数组,将每个国家/地区添加到下拉列表中,分配一些事件,并设置任何样式。然后向用户显示填充的列表,并调用 AutoSuggest 方法,以执行输入预判功能,并将列表中的第一个条目放入文本框并选择正确的字符。

如果我在此部分一带而过,我很抱歉,但本节开头引用的文章解释了剩余的代码。

AutoCompleteTextBox.cs

我已尽力在控件本身的注释中做到详尽。我们将重点介绍控件的要点,而不是逐行讲解(而且我的手指快累了)。

首先需要注意的是,AutoComplete TextBox 继承自内置的 ASP TextBox

public class AutoCompleteTextBox : System.Web.UI.WebControls.TextBox

这为我们节省了大量工作。ACTB 有许多附加属性,用于指定一些 CSS 信息和用于客户端部分使用的 JavaScript 文件的路径。Render 方法是开始的地方。

protected override void Render(HtmlTextWriter output)
{
  string uId = this.UniqueID;
  string newUid = uId.Replace(":", "_");
  string divId = newUid + "_Div";
  string jsId = newUid + "_Js";

  StringBuilder acScript = new StringBuilder();
  acScript.Append("<script type='\"text/javascript\"'>");
  acScript.AppendFormat("var {0} = new AutoCompleteTextBox('{1}','{2}');
                         {0}.ListItemClass='{3}';
                         {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                         this.ListItemCssClass, this.ListItemHoverCssClass);
  acScript.Append("</script>");

  Page.RegisterStartupScript(newUid, acScript.ToString());

  base.Attributes.Add("AutoComplete", "False");
  base.Render(output);
  output.Write(String.Format("<DIV id={0}></DIV>", divId));
}

为了让客户端 JavaScript 生效,它需要引用将充当 ACTB 的文本框,以及将充当控件下拉部分的 <DIV> 标签。ASP.NET 控件有一个 ID,通常在您的代码中使用,还有一个 UniqueID,它是在页面级别控件的唯一标识符。有时 ControlIDUniqueID 相同,但在使用用户控件、服务器控件或 DataList 控件时,您会遇到麻烦。因此,我们获取对 ACTB UniqueID 的引用,并为我们的 <DIV> 标签以及动态 JavaScript 代码创建 ID。

  string uId = this.UniqueID;
  string newUid = uId.Replace(":", "_");
  string divId = newUid + "_Div";
  string jsId = newUid + "_Js";

接下来,我们动态创建客户端所需的 JavaScript,以创建自动完成文本框。我们使用 CSS 属性初始化 JavaScript 对象,并让 ASP.NET 使用 RegisterStartupScript 将其放置在页面上的正确位置。

  StringBuilder acScript = new StringBuilder();
  acScript.Append("<script type='\"text/javascript\"'>");
  acScript.AppendFormat("var {0} = new AutoCompleteTextBox('{1}','{2}');
                         {0}.ListItemClass='{3}';
                         {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                         this.ListItemCssClass, this.ListItemHoverCssClass);
  acScript.Append("</script>");

  Page.RegisterStartupScript(newUid, acScript.ToString());

最后,我们让我们的 base 类 (TextBox) 自身进行渲染,然后渲染下拉功能所需的 <DIV> 标签。

  base.Attributes.Add("AutoComplete", "False");
  base.Render(output);
  output.Write(String.Format("<DIV id={0}></DIV>", divId));

我们还重写了 OnTextChanged 事件。我们只希望在回调期间发生此事件,并且仅当我们的控件是事件的目标时。

protected override void OnTextChanged(EventArgs e)
{  
  if( Page.Request.Params["__EVENTTARGET"] == 
         this.UniqueID && CallBackHelper.IsCallBack )
  {
    base.OnTextChanged( e );
  }
}

最后需要解决的是 AutoPostBackDataBind。显然,如果启用了 AutoPostBack,我们将破坏 AJAX 的目的。DataBind 方法必须被重写以消除其功能。如果您中的任何一个人弄清楚如何使其工作,请告诉我。目前,开发人员必须调用 BindData

public override void DataBind()
{
  // Do Nothing
}

public override bool AutoPostBack
{
  get { return false; }
}

public virtual void BindData()
{
  this.OnDataBinding(EventArgs.Empty);
}

结论

最后,我们劳动的成果交付了一个简洁的小控件,为 ASP.NET 网站带来了新的功能。有很多代码需要消化,我鼓励您下载源代码并进行尝试。我希望我奠定了基础,并为未来的开发播下了想法的种子。这项技术潜力巨大,终于受到了应有的关注。

列表 1 - Actb.aspx

<%@ Register TagPrefix="wcp" Namespace="WCPierce.Web.UI.WebControls" 
Assembly="WCPierce.Web" %>
<%@ Page language="c#" Codebehind="Actb.aspx.cs" AutoEventWireup="false" 
Inherits="TestWeb.Actb" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
  <HEAD>
    <title>AutoCompleteTextBox Example</title>
    <meta name=vs_defaultClientScript content="JavaScript">
    <meta name=vs_targetSchema 
          content="http://schemas.microsoft.com/intellisense/ie5">
    <link href="Css/AutoCompleteTextBox.css" 
          type="text/css" rel="stylesheet" />
  </HEAD>
  <body>
    <form id="Form1" method="post" runat="server">
      <table>
        <tr>
          <td colspan="2">Please enter your informaiton below.</td>
        </tr>
        <tr>
          <td>Name:</td>
          <td><asp:TextBox Runat="server" ID="txtName" /></td>
        </tr>
        <tr>
          <td>Zip:</td>
          <td><asp:TextBox Runat="server" ID="txtZip" /></td>
        </tr>
        <tr>
          <td>Country:</td>
          <td>
            <wcp:AutoCompleteTextBox runat="server" id="actbCountry" 
              OnTextChanged="actbCountry_TextChanged" 
              ListItemCssClass="ListItem"
              ListItemHoverCssClass="ListItemHover" />
          </td>
        </tr>
      </table>
    </form>
  </body>
</HTML>

列表 2 - Actb.aspx.cs

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

using Microsoft.ApplicationBlocks.Data;

using WCPierce.Web;
using WCPierce.Web.UI.WebControls;

namespace TestWeb
{
  public class Actb : System.Web.UI.Page
  {
    protected System.Web.UI.WebControls.TextBox txtName;
    protected WCPierce.Web.UI.WebControls.AutoCompleteTextBox actbCountry;
    protected System.Web.UI.WebControls.TextBox txtZip;
  
    private void Page_Load(object sender, System.EventArgs e) { }

    protected void actbCountry_TextChanged(object s, EventArgs e)
    {
      try
      {
        if( txtZip.Text.Length > 0 )
        {
          CallBackHelper.Write( "United States" );
        }
        else
        {
          AutoCompleteTextBox actb = s as AutoCompleteTextBox;
          string str = String.Format("SELECT [Text] 
                                      FROM Lists 
                                      WHERE ListName='Country' 
                                      AND [Text] LIKE '{0}%' 
                                      ORDER BY [Text]", actb.Text);
          SqlDataReader sdr = SqlHelper.ExecuteReader(@"Server=(local);
                                             Database=DotNetNuke;
                                             Integrated Security=SSPI;", 
                                             CommandType.Text, str);
          actb.DataSource = sdr;
          actb.DataTextField = "Text";
          actb.BindData();
        }
      }
      catch(Exception ex)
      {
        CallBackHelper.HandleError( ex );
      }
    }

    #region Web Form Designer generated code
  }
}

列表 3 - AutoCompleteTextBox.js

function AutoCompleteTextBox(TextBoxId, DivId, DivClass)
{
  // initialize member variables
  var oThis = this;
  var oText = document.getElementById(TextBoxId);
  var oDiv  = document.getElementById(DivId);
  this.TextBox = oText;
  this.Div = oDiv;
    
  // CallBackObject + Event Handlers
  this.Cbo = new CallBackObject();
  this.Cbo.OnComplete = function(responseText,responseXML)
                      {oThis.Cbo_Complete(responseText,responseXML);};
  this.Cbo.OnError = function(status,statusText,responseText)
                      {oThis.Cbo_Error(status,statusText,responseText);};
            
  // attach handlers to the TextBox
  oText.AutoCompleteTextBox = this;
  oText.onkeyup = AutoCompleteTextBox.prototype.OnKeyUp;
  oText.onblur  = AutoCompleteTextBox.prototype.OnBlur;
 
  // align the drop down div
  var c = GetCoords(oText);
  var n = oText.style.pixelHeight;
  if( !n )
  { 
    n = 25;
  }
  else
  {
    n += 2;
  }
  oDiv.style.left = c.x;
  oDiv.style.top = c.y + n;
  oDiv.style.display = 'none';
  oDiv.style.position = 'absolute';
  
  // Set some default styles
  if( DivClass )
    oDiv.className = DivClass;
  else
  {
    oDiv.style.border = '1';
    oDiv.style.borderColor = 'black';
    oDiv.style.borderStyle = 'solid';
    oDiv.style.backgroundColor = 'white';
    oDiv.style.padding = '2';
  }
}

AutoCompleteTextBox.prototype.DoAutoSuggest = false;

AutoCompleteTextBox.prototype.ListItemClass = '';

AutoCompleteTextBox.prototype.ListItemHoverClass = '';

// TextBox OnBlur
AutoCompleteTextBox.prototype.OnBlur = function()
{
  this.AutoCompleteTextBox.TextBox_Blur();
}

AutoCompleteTextBox.prototype.TextBox_Blur = function()
{
  this.Div.style.display='none';
}

// TextBox OnKeyUp
AutoCompleteTextBox.prototype.OnKeyUp = function(oEvent)
{
  //check for the proper location of the event object
  if (!oEvent) 
  {
    oEvent = window.event;
  }    
  this.AutoCompleteTextBox.TextBox_KeyUp(oEvent);      
}

AutoCompleteTextBox.prototype.TextBox_KeyUp = function(oEvent)
{
  var iKeyCode = oEvent.keyCode;

  if( iKeyCode == 8 )
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    return;
  }
  else if( iKeyCode == 16 || iKeyCode == 20 )
  {
    this.DoAutoSuggest = true;
  }
  else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || 
          (iKeyCode >= 112 && iKeyCode <= 123)) 
  {
    return;
  }
  else
  {
    this.DoAutoSuggest = true;
  }
  
  var txt = this.TextBox.value;
  if( txt.length > 0 )
  {
    this.Cbo.DoCallBack(this.TextBox.name, txt);
  }
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display = 'none';
    this.Cbo.AbortCallBack();
  }
}

AutoCompleteTextBox.prototype.Cbo_Complete = 
                      function(responseText, responseXML)
{ 
  while ( this.Div.hasChildNodes() )
    this.Div.removeChild(this.Div.firstChild);
            
  // get all the matching strings from the server response
  var aStr = responseText.split('\n');
         
  // add each string to the popup-div
  var i, n = aStr.length;
 
  if( n > 0 )
  {
    for ( i = 0; i < n; i++ )
    {
      var oDiv = document.createElement('div');
      this.Div.appendChild(oDiv);
      try
      {
        oDiv.innerHTML    = aStr[i];
      }
      catch(e)
      {
        this.Cbo_Error('405','Error', 
                'Text returned from Call Back was invalid');
        return;
      }
      oDiv.noWrap       = true;
      oDiv.style.width  = '100%';
      oDiv.className    = this.ListItemClass;
      oDiv.onmousedown  = AutoCompleteTextBox.prototype.Div_MouseDown;
      oDiv.onmouseover  = AutoCompleteTextBox.prototype.Div_MouseOver;
      oDiv.onmouseout   = AutoCompleteTextBox.prototype.Div_MouseOut;
      oDiv.AutoCompleteTextBox = this;            
    }
    this.Div.style.display = 'block';
      
    if( this.DoAutoSuggest == true )
      this.AutoSuggest( aStr );
  }  
  else
  {
    this.Div.innerHTML = '';
    this.Div.style.display='none';
  }
}

AutoCompleteTextBox.prototype.Cbo_Error = 
                    function(status, statusText, responseText)
{
  alert('CallBackObject Error: status=' + status + '\nstatusText=' + 
                                  statusText + '\n' + responseText);
}

AutoCompleteTextBox.prototype.Div_MouseDown = function()
{
  this.AutoCompleteTextBox.TextBox.value = this.innerHTML;
}

AutoCompleteTextBox.prototype.Div_MouseOver = function()
{
  if( this.AutoCompleteTextBox.ListItemHoverClass.length > 0 )
    this.className = this.AutoCompleteTextBox.ListItemHoverClass;
  else
  {
    this.style.backgroundColor = 'black';
    this.style.color = 'white';
  }
}

AutoCompleteTextBox.prototype.Div_MouseOut = function()
{
  if( this.AutoCompleteTextBox.ListItemClass.length > 0 )
    this.className = this.AutoCompleteTextBox.ListItemClass;
  else
  {
    this.style.backgroundColor = 'white';
    this.style.color = 'black';
  }
}

AutoCompleteTextBox.prototype.AutoSuggest = 
                             function(aSuggestions /*:array*/) 
{
  if (aSuggestions.length > 0) 
  {
    this.TypeAhead(aSuggestions[0]);
  }
}

AutoCompleteTextBox.prototype.TypeAhead = 
                            function( sSuggestion /*:string*/)
{
  if( this.TextBox.createTextRange || this.TextBox.setSelectionRange)
  {
    var iLen = this.TextBox.value.length; 
    this.TextBox.value = sSuggestion; 
    this.SelectRange(iLen, sSuggestion.length);
  }
}

AutoCompleteTextBox.prototype.SelectRange = 
                         function (iStart /*:int*/, iLength /*:int*/) 
{
  //use text ranges for Internet Explorer
  if (this.TextBox.createTextRange) 
  {
    var oRange = this.TextBox.createTextRange(); 
    oRange.moveStart("character", iStart); 
    oRange.moveEnd("character", iLength - this.TextBox.value.length);      
    oRange.select();
   
  //use setSelectionRange() for Mozilla
  } 
  else if (this.TextBox.setSelectionRange) 
  {
      this.TextBox.setSelectionRange(iStart, iLength);
  }     

  //set focus back to the textbox
  this.TextBox.focus();      
}
             
function GetCoords(obj /*:object*/) 
{   
  var newObj = new Object();
  newObj.x = obj.offsetLeft;
  newObj.y = obj.offsetTop;
  theParent = obj.offsetParent;
  while(theParent != null)
  {
    newObj.y += theParent.offsetTop;
    newObj.x += theParent.offsetLeft;
    theParent = theParent.offsetParent;
  }
  
  return newObj;
}

列表 4 - AutoCompleteTextBox.cs

using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;

using WCPierce.Web.UI;

[assembly:TagPrefix("WCPierce.Web.UI.WebControls", "wcp")]
namespace WCPierce.Web.UI.WebControls
{
  /// <SUMMARY>
  /// AutoCompleteTextBox is similar to the WinForm 
  /// ComboBox control.  As the 
  /// user types into the box, the enter is 
  /// "auto-completed" based on values 
  /// databound to the TextBox by the developer.
  /// </SUMMARY>
  [DefaultProperty("Text"), 
       ToolboxData("<{0}:AutoCompleteTextBox runat=server>")]
  public class AutoCompleteTextBox : System.Web.UI.WebControls.TextBox
  {
    #region Member Variables

    /// <SUMMARY>
    /// The (relative) path to the AutoCompleteTextBox JavaScript file.
    /// </SUMMARY>
    private string _scriptPath = string.Empty;

    /// <SUMMARY>
    /// For using databinding with your AutoCompleteTextBox
    /// </SUMMARY>
    private object _dataSource = null;

    /// <SUMMARY>
    /// Data returned to the client is in 
    /// the form of "entry"-newline-
    /// "entry"-newline...If you wanted to get 
    /// cute, we could return in an XML
    ///  format.
    /// </SUMMARY>
    private static readonly string _FormatString = "{0}\n";

    /// <SUMMARY>
    /// If a ScriptPath isn't specified, check the 
    /// web.config file for the 
    /// following key.
    /// </SUMMARY>
    private static readonly string _ScriptPath = 
                           "AutoCompleteTextBox.ScriptPath";

    /// <SUMMARY>
    /// CSS Class name for the list item of the dropdownlist.
    /// </SUMMARY>
    private string _listItemCssClass = string.Empty;

    /// <SUMMARY>
    /// CSS Class name for the "hover" effect of 
    /// the list item of the dropdownlist.
    /// </SUMMARY>
    private string _listItemHoverCssClass = string.Empty;

    #endregion

    #region Public Properties

    /// <SUMMARY>
    /// The path to the AutoComplete.js file.  
    /// If you leave it blank, it will
    /// automatically look in the web.config 
    /// for the value under the key
    /// "AutoCompleteTextBox.ScriptPath". 
    /// Should be a path relative to the 
    /// application root i.e. "~\scripts\AutoCompleteTextBox.js".
    /// </SUMMARY>
    public string ScriptPath
    {
      get 
      { 
        if( _scriptPath != string.Empty )
          return ResolveUrl(_scriptPath); 

        try
        {
          return 
            ResolveUrl(System.Configuration.ConfigurationSettings.AppSettings[
                                             AutoCompleteTextBox._ScriptPath]);
        }
        catch 
        { 
          return null;
        }
      }
      set { _scriptPath = value; }
    }

    /// <SUMMARY>
    /// CSS Class name for the list item of the dropdownlist.
    /// </SUMMARY>
    public string ListItemCssClass
    {
      get { return _listItemCssClass; }
      set { _listItemCssClass = value; }
    }

    /// <SUMMARY>
    /// CSS Class name for the "hover" effect of the 
    /// list item of the dropdownlist.
    /// </SUMMARY>
    public string ListItemHoverCssClass
    {
      get { return _listItemHoverCssClass; }
      set { _listItemHoverCssClass = value; }
    }

    /// <SUMMARY>
    /// For use with databinding.
    /// </SUMMARY>
    [Bindable(true), 
      DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
     DefaultValue((string) null)]
    public virtual object DataSource
    {
      get
      {
        return _dataSource;
      }
      set
      {
        if (((value != null) && !(value is IListSource)) && 
                                !(value is IEnumerable))
        {
          throw new ArgumentException("Invalid_DataSource_Type: " + 
                                                      this.ID);
        }
        _dataSource = value;
      }
    }

    /// <SUMMARY>
    /// For use with databinding.
    /// </SUMMARY>
    public virtual string DataTextField
    {
      get 
      { 
        object o = this.ViewState["DataTextField"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataTextField"] = value;
      }
    }

    /// <SUMMARY>
    /// For use with databinding.
    /// </SUMMARY>
    public virtual string DataTextFormatString
    {
      get
      {
        object o = this.ViewState["DataTextFormatString"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataTextFormatString"] = value;
      }
    }

    /// <SUMMARY>
    /// For use with databinding.
    /// </SUMMARY>
    [DefaultValue("")]
    public virtual string DataMember
    {
      get
      {
        object o = this.ViewState["DataMember"];
        if (o != null)
        {
          return (string)o;
        }
        return string.Empty;
      }
      set
      {
        this.ViewState["DataMember"] = value;
      }
    }

    #endregion

    #region Overrides

    /// <SUMMARY>
    /// Render this control to the output parameter specified.
    /// </SUMMARY>
    /// <PARAM name="output">The HTML writer 
    /// to write out to.</PARAM>
    protected override void Render(HtmlTextWriter output)
    {
      string uId = this.UniqueID;
      string newUid = uId.Replace(":", "_");
      string divId = newUid + "_Div";
      string jsId = newUid + "_Js";

      StringBuilder acScript = new StringBuilder();
      acScript.Append("<script type='\"text/javascript\"'>");
      acScript.AppendFormat("var {0} = 
                  new AutoCompleteTextBox('{1}','{2}');
                  {0}.ListItemClass='{3}';
                  {0}.ListItemHoverClass='{4}';", jsId, newUid, divId, 
                  this.ListItemCssClass, this.ListItemHoverCssClass);
      acScript.Append("</script>");

      Page.RegisterStartupScript(newUid, acScript.ToString());

      base.Attributes.Add("AutoComplete", "False");
      base.Render(output);
      output.Write(String.Format("<DIV id={0}></DIV>", divId));
    }

    /// <SUMMARY>
    /// Register our common scripts and do default PreRendering.
    /// </SUMMARY>
    /// <PARAM name="e"></PARAM>
    protected override void OnPreRender(EventArgs e)
    {
      this._RegisterCommonScripts();
      base.OnPreRender(e);
    }

    /// <SUMMARY>
    /// Only fire the OnTextChanged event if 
    /// this control is the target and it
    /// is a Call Back
    /// </SUMMARY>
    /// <PARAM name="e"></PARAM>
    protected override void OnTextChanged(EventArgs e)
    {  
      if( Page.Request.Params["__EVENTTARGET"] == 
          this.UniqueID && CallBackHelper.IsCallBack )
      {
        base.OnTextChanged( e );
      }
    }

    /// <SUMMARY>
    /// The original idea was to have the 
    /// Auto Complete Text Box behave like a
    /// normal Data Bindable control.  
    /// But, alas, I couldn't figure that out.
    /// Thank you M$ for the databinding code.
    /// </SUMMARY>
    /// <PARAM name="e"></PARAM>
    protected override void OnDataBinding(EventArgs e)
    {
      base.OnDataBinding(e);

      IEnumerable ie = 
          DataSourceHelper.GetResolvedDataSource(this.DataSource, 
                                                this.DataMember);
      StringBuilder sb = new StringBuilder();

      if( ie != null )
      {
        bool useTextField = false;
        bool useFormatString = false;
        string textField = DataTextField;
        string formatString = DataTextFormatString;
        if( textField.Length != 0 )
        {
          useTextField = true;
        }
        if( formatString.Length != 0 )
        {
          useFormatString = true;
        }
        foreach( object o in ie )
        {
          if( useTextField )
          {
            if( textField.Length > 0)
            {
              sb.AppendFormat(AutoCompleteTextBox._FormatString, 
                DataBinder.GetPropertyValue(o, textField, formatString));
            }
          }
          else
          {
            if( useFormatString )
            {
              sb.AppendFormat( AutoCompleteTextBox._FormatString, 
                                string.Format(formatString, o) );
            }
            else
            {
              sb.AppendFormat(AutoCompleteTextBox._FormatString, 
                                                  o.ToString());
            }
          }  // useTextField
        }  // foreach
      } // ie != null

      // Remove trailing '\n'
      if( sb.Length > 1 )
        sb.Remove(sb.Length-1, 1);


      CallBackHelper.Write( sb.ToString() );
    }


    /// <SUMMARY>
    /// Perhaps in the future, I will figure out 
    /// how to make this work.  Before
    /// you email me with the answer please try 
    /// using an AutoCompleteTextBox in
    /// a DataGrid with ViewState disabled.
    /// </SUMMARY>
    public override void DataBind()
    {
      // Do Nothing
    }

    /// <SUMMARY>
    /// What's the point if the developer turns on AutoPostBack?
    /// </SUMMARY>
    public override bool AutoPostBack
    {
      get { return false; }
    }


    #endregion

    #region Public Methods

    /// <SUMMARY>
    /// For now, Developer's must call this method to bind to their 
    /// Auto Complete Text Box.
    /// </SUMMARY>
    public virtual void BindData()
    {
      this.OnDataBinding(EventArgs.Empty);
    }

    #endregion

    #region Helper Methods

    /// <SUMMARY>
    /// Add a reference to the JavaScript, but only once per page.
    /// </SUMMARY>
    private void _RegisterCommonScripts()
    {
      if (!this.Page.IsClientScriptBlockRegistered("AutoCompleteTextBox"))
      {
        StringBuilder script = new StringBuilder();
        script.AppendFormat("<script src="{0}" type=text/javascript></script>", 
                                                             this.ScriptPath);
        this.Page.RegisterClientScriptBlock("AutoCompleteTextBox", 
                                                           script.ToString());
      }
    }

    #endregion
  }
}

历史

  • 2005-05-02
    • 初始发布。
© . All rights reserved.