|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionIf you’ve been following along with this article series then you know in Part 1 we talked briefly about Asynchronous JavaScript And XML or AJAX for short. We created the Before we get to the good stuff I want to say there is a lot of room for improvement for this control. Functionality, design-time integration, and ease-of-use can all be drastically improved. I leave that to you! The Good StuffBefore we talk about creating the control, I want to show you just how easy it is to use the control. We’ll start with Listings 1 and 2 (Actb.aspx and Actb.aspx.cs). After you calm down from your overwhelming excitement, we will examine the JavaScript portion of the ACTB Listing 3 (AutoCompleteTextBox.js) and the ASP.NET portion Listing 4 (AutoCompleteTextBox.cs). Actb.aspxThis page is pretty straightforward. We are setting up some input fields for the user. Most of you will probably note that not all countries use a Zip code. I put this in as a demonstration of how you might customize the results of the Auto Complete TextBox based on values from other fields. (more on this later.) To use our custom control we have to register the assembly with the page and give it a tag prefix. I’ve modestly used my initials as the tag prefix but you can choose any prefix you desire. We also specify the namespace and assembly name where the control(s) can be found. <%@ Register TagPrefix="wcp" Namespace="WCPierce.Web.UI.WebControls"
Assembly="WCPierce.Web" %>
After that, the only interesting piece of HTML is the actual declaration of the <wcp:AutoCompleteTextBox runat="server" id="actbCountry"
OnTextChanged="actbCountry_TextChanged" ListItemCssClass="ListItem"
ListItemHoverCssClass="ListItemHover" />
The declaration is nearly identical to that of a normal ASP Actb.aspx.csThings begin to get slightly more interesting when we examine the code-behind file. The first thing to note is the . . .
using Microsoft.ApplicationBlocks.Data;
using WCPierce.Web;
using WCPierce.Web.UI.WebControls;
The Data Application Block provided by Microsoft is used to simplify some database access code. The other two Note the try
{
//. . .
}
catch(Exception ex)
{
CallBackHelper.HandleError( ex );
}
This is important because errors that occur during a Call Back can be very difficult to debug (trust me on this). With this code in place and the proper JavaScript, it makes debugging errors a breeze. The first step is to examine the contents of the if( txtZip.Text.Length > 0 )
{
CallBackHelper.Write( "United States" );
}
For the next part of the code it is important to remember that this event is firing every time the user presses a key while in the 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);
All of the matching countries (Uganda, Ukraine, Uruguay, etc.) are returned in a SqlDataReader sdr = SqlHelper.ExecuteReader(@"Server=(local);
Database=DotNetNuke;
Integrated Security=SSPI;",
CommandType.Text, str);
actb.DataSource = sdr;
actb.DataTextField = "Text";
actb.BindData();
Savvy readers will notice the only difference here is the call to That’s it! I’ve encapsulated all the hard work in the AutoCompleteTextBox.jsBefore we start discussing this little gem, I need to recognize the great individuals who helped me make this control a reality:
The concepts from those two articles plus the 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();
}
}
Whenever a key is pressed in the When the server side request completes, our method 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';
}
}
The data returned from the server is a new-line delimited list of country names. After we remove any current entries from the drop down list, we split the return value into an array of country names. Then, we loop through the array and add each county to the drop down list, assign some events, and set any styles. The populated list is then displayed to the user, and we call the I apologize if I breezed over this portion but the articles referenced at the beginning of this section explain the remainder of the code. AutoCompleteTextBox.csI tried to be thorough in my commenting of the control itself. We will cover the highlights of the control in lieu of taking you through line by line (plus my fingers are getting tired). The first thing to note is that AutoComplete TextBox inherits from the built-in ASP public class AutoCompleteTextBox : System.Web.UI.WebControls.TextBox
This saves us a tremendous amount of work. 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));
}
In order for the client side JavaScript to work, it needs a reference to the TextBox that will be acting as the string uId = this.UniqueID;
string newUid = uId.Replace(":", "_");
string divId = newUid + "_Div";
string jsId = newUid + "_Js";
Next we dynamically create the JavaScript needed on the client side to create the Auto Complete TextBox. We initialize the JavaScript object with CSS properties and have ASP.NET put it in the proper place on the page for us with 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());
Finally, we have our base.Attributes.Add("AutoComplete", "False");
base.Render(output);
output.Write(String.Format("<DIV id={0}></DIV>", divId));
We also override the protected override void OnTextChanged(EventArgs e)
{
if( Page.Request.Params["__EVENTTARGET"] ==
this.UniqueID && CallBackHelper.IsCallBack )
{
base.OnTextChanged( e );
}
}
The last few things to address are public override void DataBind()
{
// Do Nothing
}
public override bool AutoPostBack
{
get { return false; }
}
public virtual void BindData()
{
this.OnDataBinding(EventArgs.Empty);
}
ConclusionFinally the fruits of our labor have delivered a neat little control that brings some new functionality to the ASP.NET websites. There is a lot of code to digest and I encourage you to download the source and play around. I hope I laid the ground work and planted some ideas for future development. There is a lot of potential for this technology and it is finally getting the attention it deserves. Listing 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>
Listing 2 – Actb.aspx.csusing 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
}
}
Listing 3 – AutoCompleteTextBox.jsfunction 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;
}
Listing 4 – AutoCompleteTextBox.csusing 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>
History
| |||||||||||||||||||||||||||||||||||