Build an Anthem.NET [AJAX] Autosuggest Textbox

by Peter A. Bromberg, Ph.D.

Peter Bromberg

Remote Scripting (which is now referred to inaccurately as "AJAX" mostly because of a bunch of marketing hype) was invented by Microsoft around 1998, and was first notably used in a commercial application by the MS Exchange team in Outlook Web Access to make the Outlook Web application "look and feel" more like the desktop Outlook. The level of maturity of OWA was incredible, but it was only recently that browsers besides Internet Explorer got religion and implemented the XMLHttpWebRequest feature natively. That's why the big buzz over "AJAX", with many developers not even realizing that it has actually been around since last century!



Everybody just seems to love to "bash Microsoft", these days, but the fact of the matter is that the Microsoft people do one hell of a lot of innovation. Microsoft has been doing "Web 2.0" since 1998. One thing they don't always do well is to capitalize on their innovations -- often others do, and take all the glory. "AJAX" is a good example -- with Microsoft "appearing" to have to play catch-up on technology that they invented!

I became interested in the Remote Scripting concept not long after, and what one could call a "breakthrough" implementation was first done by Brent Ashley in 2000 with his "JSRS" offering that was 100% Javascript. In fact, the first article I believe I ever wrote on the subject was one about using cookies for small amounts of data here. That was back in 2001. If you search eggheadcafe.com on the phrase "Remote Scripting", you will find a number of articles about using the technique, in various incarnations, for a variety of diffferent functions.

One of the clarion implementations of Remote Scripting that woke up a lot of web developers was the application by the Google people of their "Suggest" search beta. An Autosuggest control is basically an HTML textbox (INPUT) element with a bunch of dynamic DIV tags below it that will present a list of matching selections based on the text that has been typed into the textbox. Every time you type another character, the search is narrowed down. Additional Javascript provides a mechanism for scrolling through this list and selecting a desired choice, which is then automatically entered into the textbox for you.

This was hailed as the greatest thing since Barney Rubble invented the electronic ignition system 45,000 years ago, and buzzwords flew. Gosh! it was so -- uhh, "Web 2.0"! The Google people didn't even use the acronym "AJAX" until very recently, simple referring to their creation as "Javascript".

The new ATLAS framework has an Autosuggest feature built in, but in this article I will focus on the Anthem.NET framework, reworking some very nice code that was originally submitted to the Anthem.NET Sourceforge repository as a suggested patch by J.C. Murphy. I've changed around the offering in order to make it more "self - contained" and moved a lot of brand new data access code and related properties directly into the control. The sample control is presented here in both ASP.NET 1.1 and ASP.NET 2.0 versions, along with the entire source tree so that interested users can look at everything in Anthem.NET.

First, a word about "AJAX" libraries for the .NET platform. The ASP.NET platform revolves around the concept of a stateful Page class that has objects, ViewState and so on that one may want or need to interact with at the server side. The new ATLAS framework is designed to preserve this model and allow access to it while still offering the flexibility to customize or simplify the model.

Unfortunately, not all implementations of "AJAX" for ASP.NET preserve this important concept. Some offerings, like Schwartz' "AJAX.NET", rely on external handlers that must be registered in the web.config thereby breaking the stateful Page model. The only two "mature" Frameworks I"ve found that appear to preserve the stateful page concept are Anthem.NET, originated by Jason Diamond, and MagicAjax, another notable offering. Both of these are 100% open-source, Anthem.NET having a lot more activity and contributors, and both are available on Sourceforge.net. Both are also extremely easy to use, and I have used both of them with great success. I'm not belittling AJAX.NET at all, I just think developers should understand this important difference when they are going to make a choice of which library to use.

Other developers have plunged into this area, and another implementation that I like called "wwHoverPanel" was done by my buddy and fellow MVP Rick Strahl here. Rick strives for a simplified coding model in his approach.

While working on a project recently it occurred to me that I would want to use an AutoSuggest type textbox in the app, and so, being the proponent of "Don't reinvent the wheel" that I am, I looked around. There are a few decent offerings of "standalone" AutoSuggest textboxes, most notably this one called "autosuggestbox", but I was reasonably sure that I was going to use either Anthem.NET or MagicAjax for other features of my site, so I spent some time at Sourceforge with Anthem.NET.

One of the great features of Anthem.NET is that it has a real "provider" model that allows developers to create custom "AJAX" (Remote Scripting) - enabled controls by simply deriving from the base Anthem.NET Control class. Jason Diamond's unique approach to this is brilliant, writing "Anthem.NET Control Markers" at the beginning and end of the control, which get parsed by the framework automatically. Murphy did a lot of the necessary work with his Anthem.NET "AutoSuggest" and implemented a bunch of Javascript to handle the rendering of the dynamic list of suggestions. However, I was not happy with his external requirement for the data source, because it not only required you to get the data from within the code for the hosting page, but it also assumed that you would get the data one time and every time a user typed in a new character in the textbox, it would simply filter on that data again. That's not extensible, you really need an arrangment where a brand new query can be made each time the user types in a character. That's expecially true if you have a lot of data - you don't want to load all of it in one go, it is simply an inefficient waste of resources.

So, what I did was to move all the data access directly into the control itself. Now, you can set a connection string, and a SQL Statement with a filter parameter as properties of the control. This makes it much more flexible and creates a control that is 100% "self-contained".

Let's take a brief look at the Anthem.NET Control model and see how this was done. In my work, I attempted to change as little as possible of J.C.'s existing work so that it would be easier to "keep in synch". This is the ASP.NET 1.1 version of the control:

using System;

using System.ComponentModel;

using System.Data;

using System.IO;

using System.Text;

using System.Web;

using System.Web.UI;

using System.Web.UI.HtmlControls;

using System.Web.UI.WebControls;

using System.Reflection ;

using System.Data.SqlClient;

#if V2

//[assembly: WebResource("Anthem.TextBox.js", "text/javascript")]

#endif

namespace Anthem

{

    public class AutoSuggestTextBox : TextBox, Anthem.IUpdatableControl

    {

       private bool _allowEditing = true;
       // if AllowEditing is false, user will not be able to key a value that's not suggested

        private Anthem.Label _errorLabel;    // Label to hold error text

        private string _errorLabelName;   
        // if set, textbox will use the label named to write error message on AlloEdit

        private const string parentTagName = "span";

        private object _dataSource;

        private DataView _dataView;

 

        public bool AllowEditing

        {

            get

            {

                return _allowEditing;

            }

            set

            {

                _allowEditing = value;

            }

        }

 

        public string ErrorLabelName

        {

            get

            {

                return _errorLabelName;

            }

            set

            {

                _errorLabelName = value;

            }

        }

 

        public AutoSuggestTextBox()

        {

        }

 

        public object DataSource

        {

            get

            {

                return _dataSource;

            }

            set

            {

                _dataSource = value;

            }

        }

 

 

    private string _connectionString;

    [Bindable(true),Browsable(true), Category("Data")]

    public string ConnectionString

    {

        get { return _connectionString;}

        set {_connectionString=value;}

    }

 

    private bool _isStoredProcedure;

    [Bindable(true),Browsable(true) , Category("Data")]

    public bool IsStoredProcedure

    {

        get {return _isStoredProcedure;}

        set {_isStoredProcedure = value;}

    }

 

    private string _commandText;

    [Bindable(true),Browsable(true), Category("Data") ]  

    public string CommandText

    {

        get{return _commandText;}

        set{_commandText=value;}

    }

 

 

        [Anthem.Method]

        public String OnKeyUp()

        {

            StringBuilder sb = new StringBuilder();     

            BindData( this.Text);

            for (int i = 0; i < _dataView.Count; i++)

            {

                sb.Append(_dataView[i][0]);

                sb.Append("\n");

            }

            // Remove trailing '\n'

            if (sb.Length > 1)

            {

                sb.Remove(sb.Length - 1, 1);

                if (ErrorLabelName != null)

                {

                    _errorLabel.Text = "";

                }

            }

            else if (!AllowEditing)

            {

                if (ErrorLabelName != null)

                {

                    _errorLabel.Text = "You must choose a value from the dropdown";

                }

            }

            return sb.ToString();

        }

 

        private void GetData (string filter)

        {

            SqlConnection conn = new SqlConnection(this._connectionString );

            SqlCommand cmd = new SqlCommand(this._commandText,conn);

 

            if(this._isStoredProcedure)

            {

                cmd.Parameters.Add(new SqlParameter("@filter",filter));

                cmd.CommandType=CommandType.StoredProcedure;

            }

            else

            {

                cmd.CommandText=cmd.CommandText.Replace("@filter",filter);

                cmd.CommandType=CommandType.Text;

            }

            SqlDataAdapter da = new SqlDataAdapter(cmd);

 

            DataSet ds = new DataSet();

            da.Fill(ds);

                this._dataSource =ds;

        }

 

        // this method expects a dataset to be assigned to _dataSource

        public void BindData(string filter)

        {

            GetData(filter);

            DataSet ds = new DataSet();

            DataView dv = new DataView();

            ds = (DataSet)_dataSource;

            dv = ds.Tables[0].DefaultView;

            _dataView = dv;

        }

 

        protected override void AddAttributesToRender(HtmlTextWriter writer)

        {

            string uId = this.UniqueID;

            string newUid = uId.Replace(":", "_");

            string ifrId = newUid + "_Iframe";

            string divId = newUid + "_Div";

            string jsId = newUid + "_Js";

            Anthem.Manager.AddScriptAttribute(this, "AutoComplete", "off");

            Anthem.Manager.AddScriptAttribute(this, "onkeyup", jsId + ".OnKeyUp(event)");

            Anthem.Manager.AddScriptAttribute(this, "onblur", jsId + ".OnBlur(event)");

            base.AddAttributesToRender(writer);

        }

 

    protected override void OnPreRender(EventArgs e)

        {

#if V2

this.Page.ClientScript.RegisterClientScriptResource(typeof(Anthem.AutoSuggestTextBox), "Anthem.TextBox.js");

#else

Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Anthem.TextBox.js");

StreamReader sr = new StreamReader(stream);

this.Page.RegisterClientScriptBlock("Anthem.TextBox.js", "<script language='javascript' type='text/javascript'>" + sr.ReadToEnd() + "</script>");

#endif

            base.OnPreRender(e);

        }

 

        public override bool AutoPostBack

        {

            get { return false; }

        }

 

        #region Common Anthem control code (DO NOT EDIT!)

        public override bool Visible

        {

            get { return Anthem.Manager.GetControlVisible(ViewState); }

            set { Anthem.Manager.SetControlVisible(ViewState, value); }

        }

 

        protected override void OnLoad(EventArgs e)

        {

            base.OnLoad(e);

 

            if (_errorLabelName != null)

            {

                foreach (Control c in Page.Controls)

                {

                    foreach (Control childc in c.Controls)

                    {

                        if (childc.ID == _errorLabelName)

                        {

                            _errorLabel = (Anthem.Label)childc;

                        }

                    }

                }

            }

            Anthem.Manager.Register(this);

        }

 

#if V2

        public override void RenderControl(HtmlTextWriter writer)

        {

            base.Visible = true;

            base.RenderControl(writer);

        }

#endif

        protected string getCreateScript()

        {

            string uId = this.UniqueID;

            string ifrId = uId + "_Iframe";

            string divId = uId + "_Div";

            string jsId = uId + "_Js";

            string allowEd = _allowEditing.ToString();

            return String.Format("createAutoSuggest({0}, '{1}', '{2}', '{3}', '{4}');", jsId, uId, divId, allowEd, ifrId);

        }

        protected override void Render(HtmlTextWriter writer)

        {

            string uId = this.UniqueID;

            string newUid = uId.Replace(":", "_");

            string ifrId = newUid + "_Iframe";

            string divId = newUid + "_Div";

            string jsId = newUid + "_Js";

            string allowEd = _allowEditing.ToString();

#if !V2

            bool DesignMode = this.Context == null;

#endif

            StringBuilder acScript = new StringBuilder();

            acScript.Append("<script type=\"text/javascript\">");

            acScript.AppendFormat("var {0} = new ASTextBox('{1}','{2}','{4}');{0}.AllowEditing = '{3}';", jsId, newUid, divId, allowEd, ifrId);

            acScript.Append("</script>");

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

            if (Visible)

            {

                if (!DesignMode)

                {

                    // parentTagName must be defined as a private const string field in this class.

                    Anthem.Manager.WriteBeginControlMarker(writer, parentTagName, this);

                }

                base.Render(writer);

                if (!DesignMode)

                {

                    Anthem.Manager.WriteEndControlMarker(writer, parentTagName, this);

                    writer.Write(String.Format("<iframe id={1}></iframe><div id={0}></div>", divId, ifrId));

                }

            }

        }

        #endregion

    }

}

The key feature of Anthem.Net is the [Anthem.Method] attribute that automatically takes care of the serialization of method calls from the client - side (via XmlHttp) to the stateful ASP.NET Page / Control server side, and marshals the results back to the client to update the page or control. The method call is actually executed right inside the same page (or control) class, providing the unique access to the stateful Page model that I talked about earlier. In the case of an AutoComplete / AutoSuggest control, that method would be the OnKeyUp method, which triggers a new database call and repopulates the _dataSource property. This is done in my "GetData(string filter)" method, which uses SQLClient, but you could easily change this to use OleDb which would provide much more database flexibility.

Once the _DataSource has been custom populated, the rest of the code takes over and creates the "Suggest List" via the injected Javascript from the calls to:

Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Anthem.TextBox.js");

StreamReader sr = new StreamReader(stream);

this.Page.RegisterClientScriptBlock("Anthem.TextBox.js", "<script language='javascript' type='text/javascript'>" + sr.ReadToEnd() + "</script>");

The properties that are required are easily set in the Properties Window for the control. Here is an example:

You can enter a custom SQL Statement (or the name of a stored procedure), a connection string, and choose "IsStoredProcedure" as true or false. For a sproc, the input parameter is "@filter", but beyond that, you can make the sproc do whatever you want as long as the returned column values are aliased as "name". The control takes care of the rest, and this allows some flexibility with both the filter and the SQL (for example, you could have a JOIN, if necessary). The control expects the returned column values to be aliased as "name". The ASP.NET 2.0 version is virtually identical to the above; any differences are handled by the conditional compilation directives.

Here is an example display after typing "fr" into the Textbox:

In closing, let me say that I haven't spent a lot of time on this, so if you have a suggestion or sample code, post a notice on the little discussion board at the bottom of this article, and, time permitting, I'll try to feature any enhancements or changes and update the article. J.C. Murphy really did most of the work, to his credit - all I did was "take it up a notch".

The downloadable solution has the COMPLETE Anthem.Net source tree including the code for the AutoSuggest TextBox already built in, and sample pages to show the use of the control in both the 2003 and 2005 Examples sections. You should only need to change the connection string if you want to test it out on the Northwind "Customers" table, and make sure that the "Anthem-Examples-2003" folder is an IIS Application. The 2005 examples web application folder uses the FileSystem model, so an IIS Vroot isn't necessary. Enjoy!

Download the combined Visual Studio.NET 2003 and 2005 Solution for this article



Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: