Tuesday, December 18, 2007

AutoCompleteExtender and key-value issue continued...

Two weeks ago I wrote an entry on the AutoCompleteExtender issues. I've proposed completely different approach where a new window is spawned on demand from which the user can select the item from the paged list. In my approach instead of guessing what to type into the TextBox the user sees the complete list with a rich filter so that even if the list contains thousands of elements at the beginning the filter allows to narrow the list to a reasonable amount of elements and finally pick up the right one.

I've also promised that the loose idea expressed with a bit of javascript will finally materialize as the UserControl. So here it is:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.ComponentModel;
 
namespace Custom.Web.UI.Controls
{
    public class SelectableTextBox : WebControl, INamingContainer
    {
        public SelectableTextBox() { }
 
        private TextBox KeyTextBox;
        private HiddenField ValueTextBox;
 
        #region Static helper
        const string ClientReturnScript =
        "window.opener.document.getElementById( '{0}' ).value = '{1}';\r\n" +
        "window.opener.document.getElementById( '{2}' ).value = '{3}';\r\n" +
        "window.close();";
        public static void ReturnKeyAndValue( Page Page, string Key, string Value )
        {
            string Script = 
                string.Format( 
                    ClientReturnScript, 
                    Page.Request.QueryString["keyControlID"], 
                    Key, 
                    Page.Request.QueryString["valueControlID"], 
                    Value );
 
            Page.ClientScript.RegisterStartupScript( 
                typeof( SelectableTextBox ), 
                "clientreturnscript", "<script>" + Script + "</script>" );
        }
        #endregion
 
        #region Properties
 
        private string clientUrl;
        [Browsable( true ), Category( "Action" )]
        public string ClientUrl
        {
            get
            {
                return TildaHandler( clientUrl ); 
            }
            set
            {
                clientUrl = value;
            }
        }
 
        private int clientWindowWidth = 800;
        [Browsable(true), Category( "Appearance" )]
        public int ClientWindowWidth
        {
            get
            {
                return clientWindowWidth;
            }
            set
            {
                clientWindowWidth = value;
            }
        }
 
        private int clientWindowHeight = 600;
        [Browsable(true), Category( "Appearance" )]
        public int ClientWindowHeight
        {
            get
            {
                return clientWindowHeight;
            }
            set
            {
                clientWindowHeight = value;
            }
        }
 
        private int clientWindowLeft = 100;
        [Browsable( true ), Category( "Appearance" )]
        public int ClientWindowLeft
        {
            get
            {
                return clientWindowLeft;
            }
            set
            {
                clientWindowLeft = value;
            }
        }
 
        private int clientWindowTop = 100;
        [Browsable( true ), Category( "Appearance" )]
        public int ClientWindowTop
        {
            get
            {
                return clientWindowTop;
            }
            set
            {
                clientWindowTop = value;
            }
        }
 
        private ButtonType buttonType;
        [Browsable( true ), Category( "Appearance" )]
        public ButtonType ButtonType
        {
            get
            {
                EnsureChildControls();
                return buttonType;
            }
            set
            {
                EnsureChildControls();
                buttonType = value;
            }
        }
 
        #region Button
        private string buttonCaption;
        [Browsable( true ), Category( "Appearance" )]
        public string ButtonCaption
        {
            get
            {
                EnsureChildControls();
                return buttonCaption;
            }
            set
            {
                EnsureChildControls();
                buttonCaption = value;
            }
        }
        #endregion
 
        #region Image Button
        private string imageUrl;
        [Browsable(true), Category( "Appearance" )]
        public string ImageUrl
        {
            get
            {
                EnsureChildControls();
 
                return TildaHandler( imageUrl );
            }
            set
            {
                EnsureChildControls();
                imageUrl = value;
            }
        }
        #endregion
 
        #endregion
 
        #region Client properties
 
        [Browsable( false )]
        public string Text
        {
            get
            {
                return KeyTextBox.Text;
            }
            set
            {
                KeyTextBox.Text = value;
            }
        }
 
        [Browsable( false )]
        public string SelectedValue
        {
            get
            {
                if ( !string.IsNullOrEmpty( ValueTextBox.Value ) )
                    return ValueTextBox.Value;
 
                return null;
            }
            set
            {
                ValueTextBox.Value = value;
            }
        }
 
        public override ControlCollection Controls
        {
            get
            {
                EnsureChildControls();
                return base.Controls;
            }
        }
        #endregion
 
        protected override void CreateChildControls()
        {
            KeyTextBox = new TextBox();
            KeyTextBox.ID = "KeyTextBox";
            KeyTextBox.ReadOnly = true;
            KeyTextBox.Width = new Unit( 100, UnitType.Percentage );
 
            ValueTextBox = new HiddenField();
            ValueTextBox.ID = "ValueTextBox";
 
            this.Controls.Add( KeyTextBox );
            this.Controls.Add( ValueTextBox );
        }
 
        string Script =
        "<script>\r\n" +
        "function showWindowForSelectableControl(address, width, height, left, " +
        "top, keyControlID, valueControlID) \r\n" +
        "{ \r\n " +
        "  noweOkno = window.open( address + '?keyControlID='+keyControlID+'&valueControlID=' + " +
        "valueControlID, '_blank', 'menubar=no, toolbar=no, location=no, scrollbars=yes, " +
        "resizable=yes, status=no, width='+width+', height='+height+', left='+left+', top='+top) \r\n" +
        "  noweOkno.focus(); \r\n" +
        "}\r\n</script>";
        protected override void OnLoad( EventArgs e )
        {
            this.Page.ClientScript.RegisterStartupScript( 
                typeof( SelectableTextBox ), "showWindow", Script );
        }
 
        protected override void Render( HtmlTextWriter writer )
        {
            AddAttributesToRender( writer );
 
            // writer.AddAttribute( HtmlTextWriterAttribute.Style, this.Style.Value );
            writer.AddAttribute( HtmlTextWriterAttribute.Width, this.Width.ToString() );
            if ( !string.IsNullOrEmpty( this.CssClass ) )
                writer.AddAttribute( HtmlTextWriterAttribute.Class, this.CssClass );
 
            writer.RenderBeginTag( HtmlTextWriterTag.Div );
 
            writer.WriteBeginTag( "table" );
            writer.WriteAttribute( "border", "0" );
            writer.WriteAttribute( "cellpadding", "0" );
            writer.WriteAttribute( "cellspacing", "0" );
            writer.WriteAttribute( "width", "100%" );
            writer.Write( ">" );
 
            writer.WriteBeginTag( "tr" );
            writer.WriteAttribute( "width", "100%" );
            writer.Write( ">" );
            writer.WriteBeginTag( "td" );
            writer.WriteAttribute( "width", "100%" );
            writer.Write( ">" );
 
            KeyTextBox.RenderControl( writer );
            ValueTextBox.RenderControl( writer );
 
            writer.WriteEndTag( "td" );
            writer.WriteBeginTag( "td" );
            writer.Write( ">" );
 
            string ClientScript =
                string.Format( "showWindowForSelectableControl( '{0}', {1}, {2}, {3}, {4}, '{5}', '{6}' )",
                    ClientUrl,
                    ClientWindowWidth, ClientWindowHeight,
                    ClientWindowLeft, ClientWindowTop,
                    KeyTextBox.ClientID, ValueTextBox.ClientID );
 
            switch ( this.ButtonType )
            {
                case ButtonType.Image:
                    writer.WriteBeginTag( "image" );
                    writer.WriteAttribute( "alt", string.Empty );
                    writer.WriteAttribute( "src", ImageUrl );
                    writer.WriteAttribute( "onclick", ClientScript );
                    writer.WriteAttribute( "style", "cursor: pointer" );
                    writer.Write( ">" );
                    writer.WriteEndTag( "image" );
                    break;
                case ButtonType.Link:
                    writer.WriteBeginTag( "a" );
                    writer.WriteAttribute( "href", "#" );
                    writer.WriteAttribute( "onclick", ClientScript );
                    writer.Write( ">" );
                    writer.Write( ButtonCaption );
                    writer.WriteEndTag( "a" );
                    break;
                default:
                    writer.WriteBeginTag( "button" );
                    writer.WriteAttribute( "onclick", ClientScript );
                    writer.Write( ">" );
                    writer.Write( ButtonCaption );
                    writer.WriteEndTag( "button" );
                    break;
            }
 
            writer.WriteEndTag( "td" );
            writer.WriteEndTag( "tr" );
            writer.WriteEndTag( "table" );
 
            writer.RenderEndTag();
        }
 
        string TildaHandler( string Url )
        {
            return base.ResolveUrl( Url );
        }
    }
}

The control acts like a TextBox but no user input is allowed. Instead a button is provided aside of the control and clicking the button opens new browser window which navigates to ClientUrl Url. Example usage:



<mctl:SelectableTextBox ID="CityInsert" runat="server" ButtonType="Image"
    ImageUrl="~/Resources/item_detail.gif" ClientUrl="~/Selector/City_Selector.aspx"
    Width="60%" />

A helper method is provided to return the value from the new browser window - to return the value to calling window you just write:



SelectableTextBox.ReturnKeyAndValue( 
    (Page)HttpContext.Current.Handler, 
    Key, Value );

After the value is returned to the calling window both Key and Value can be acceseed with:



string Key   = CityInsert.Text;
string Value = CityInsert.SelectedValue;

Both properties are bindable.


Please feel free to use the component and drop a note if you find it useful.