Tuesday, December 4, 2007

AutoCompleteExtender, key-value issue and beyond ...

Here is the issue - an ASP.NET application and a database table consisting of unknown number of records, 50 or 50000. Then, the interface control which should allow the user to pick one of the items. The DropDownList does not work here - although there's nothing wrong in putting 50 items into the DropDownList, the list with 50000 elements is rather a bad idea.

My first and almost obvious choice was the AutoCompleteExtender from the ASP.NET Ajax Toolkit. After few tech studies, I've realized how many drawbacks this control has:

  1. it's not obvious how to assign values to items put into the drop part of the extender. New versions of the Toolkit seems to target this issue (look at this blog entry from Phani Raj). It seems that the value is available at client side and after you get it, you are on your own. It means you and the javascript on a date.
  2. suppose the user picks a correct entry from the list and then modifies the textbox value so it does not correspond to any of available items. Guess what? The suggestion from the article above does not work. The hidden textbox still holds the correct value selected from the list. Cyril Durand notes this issue in his blog entry here.
  3. unfortunately, even Cyril's trick does not work when the user just types the correct value into the textbox and does not pick anything directly from the list.

Why it is important to have the value of an item available? Well, suppose that the dropdown holds a list of all towns in your country. There are surely many towns with the same name. Instead of putting just a name into the drop down list, you'd rather format items in a custom way so that the user is able to pick the correct item from the list. Now, the page posts back and you get a literal which is surely a name of a town but formatted in a custom way. How are you supposed to retrieve it's database identity? God one knows.

However, the major issue of the AutoCompleteExtender is the need to type something into the textbox while in fact the user could have no idea what he/she should type. In the "list of towns" example - suppose I know the country, I know the region and I know that there are 50 towns in that region but have no idea of their names. If I am to see the complete list, I will surely know what item to pick. However, the AutoCompleteExtender will only show me 10 first items.

Yet another example - a drop down list to pick users. Suppose I know his age, his gender, his address, his company but I completely forgot his name. How am I to pick the user from the list using the AutoCompleteExtender?

This is where my idea comples to play - instead of the AutoCompleteExtender the user will see a plain textbox with small button next to it. Clicking the button will spawn a new browser window with rich user interface to define a custom filter and a paged GridView showing a list of items matching current filter criteria. After user is able to narrow the list so that the correct item can be picked, the new window will return both name and the value (database identity) of the item to the parent window.

So the issue now is - how are we supposed to build a communication link between two browser windows so that when the user picks an item in a child window, the information of the selection will appear in a parent window?

Let's start by putting two textboxes in a parent window.

   1: <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
   2: <input id="Hidden1" type="hidden" runat="server" />

The first one will receive the item's name, the second one will receive item's value.

Let's also put a html button on the form which will be responsible for spanning a new window:

   1: <input type="button" id="Button1" runat="server" value="wybierz" />

The button will raise local browser event and a new window will be shown to the user where he/she will be able to pick the item from the list.

   1: public partial class _Default : 
   2:   System.Web.UI.Page 
   3: {
   4:     protected void Page_Load(object sender, EventArgs e)
   5:     {
   6:         Button1.Attributes["onclick"] = 
   7:             string.Format( "showWindow( 'Child.aspx', '{0}', '{1}' );", 
   8:                  TextBox1.ClientID, Hidden1.ClientID );
   9:     }

The showWindow is a local javascript method to raise the new window:

   1: <script language="javascript" type="text/javascript">
   2: function showWindow(URL, controlID, targetControlID)
   3: {
   4:   noweOkno = window.open( URL + '?controlID='+controlID+'&targetControlID=' + 
   5:      targetControlID, '_blank', 
   6:      'menubar=no, toolbar=no, location=no, scrollbars=no, resizable=no, ' +
   7:      'status=no, width=640, height=480, left=30, top=30')
   8:   noweOkno.focus();
   9: }
  10: </script>    

The Child.aspx window (which will ultimately return both the name and the value of the item) can be arbitrarily complicated. The only interesting issue here is that I pass two parameters in the QueryString - the controlID parameter holds the name of a parent window control which will receive the name of the item and the targetControlID parameter holds the name of a parent window control which will receive the value of the item.

No matter what you do in the Child.aspx window, ultimately the user presses the OK button which should pass both name and the value of picked item to the parent window.

   1: class ChildWindow {
   2:   protected void OK_Click( object sender, EventArgs e )
   3:   {
   4:     string Script =
   5:         string.Format( 
   6:     "window.opener.document.getElementById( '{0}' ).value = '{1}';" +
   7:     "window.opener.document.getElementById( '{2}' ).value = '{3}';" +
   8:     "window.close();", 
   9:     this.Request.QueryString["controlID"], NameToBeReturnedBack, 
  10:     this.Request.QueryString["targetControlID"], ValueToBeReturnedBack );
  12:     this.ClientScript.RegisterStartupScript( typeof( ChildWindow ), "close", "<script>"+Script+"</script>" );
  13:   }

This snippet does the magic trick. Using window.opener I refer to the parent window from the child window and this script sets both the name textbox and the hidden value textbox to proper values. After this the child window is immediately closed.

And that's it. In the parent window you can ask for the item's value stored in the hidden textbox:

   1: string SelectedValue = Hidden1.Value.ToString();
A simple "todo" task: the triple - TextBox, hidden TextBox and the button bringing the child window could be put into a web control which could then just expose ChildPageURL and SelectedValue properties. I will implement such user control in few days and write another entry to present the code.


emir said...

thanks for this great article. really appreciate it.... keep it going

Ivan said...

I ran in to this situation today as well. I had the bright idea to add a datasource control that actually looked up what the user entered to return an ID that represented it.

This value was then to be stored in a hidden field.

Apparantly, there's a problem with having an UpdatePanel inside of a FormView; even if all of your controls LOOK like the have the proper values inside of them, when you use an Add command (Click an insert button), the values passed to the DataSource control are nullified/reset to their default values.

I think I'm going to use your approach, and get rid of the UpdatePanel completely (at least not surround my Bound controls with it).

Got the custom control done yet? Or do you need some help with creating the control?

Wiktor Zychla said...

ivan, please take a look here



Aim said...

Hi, very nice article.

You are looking for something that AutocompleteExtender is not even designed for. One of main ideas is to make it as simple as possible (ehm...)

RRave said...

Dear Sir,

I have a launched new web site for .NET programming resources. www.codegain.com. I would like to invite to the codegain.com as author and supporter. I hope you will joins with us soon.

Thank You
Founder www.codegain.com