5.25.2006

Atlas: Smart Auto Completion

Auto Completion without a Web Service



To get the latest version that works with Beta 2, please visit my new blog:
http://weblogs.asp.net/infinitiesloop/



If you haven't heard of the Atlas project yet, this article may not be for you. But even then, reading this may give you a clue as to the capabilities of Atlas. To quote the Atlas site,

"Atlas" is a free framework for building a new generation of richer, more interactive, highly personalized standards based Web applications.

One of the innovations Atlas brings to the table is its level of integration with the server side. It is because of this integration that makes it possible for you to take advantage of Atlas' advanced client side features without leaving the comforts of the server side of the world.

But despite this client/server integration, Atlas is still a truly client side framework, one that can be integrated with any server side back end, not just ASP.NET. Its design must take that into consideration, because everything it does must (and should) be a purely client side game. But if you are definitely using Atlas with ASP.NET, it would be nice if you could rely on deeper integration into the rich asp.net server side world. And that is what this article is all about.

Using the Auto Complete Extender


First, lets go over how the built-in stuff works. The built-in Atlas AutoCompleteExtender adds intelligent auto completion behavior to a textbox. Here's how you set one up:

First thing you need is a script manager:

<atlas:ScriptManager ID="ScriptManager1" runat="server" />
And then place a textbox and attach an AutoCompleteExtender to it:

<asp:TextBox id="txt1" runat="server"/>

<atlas:AutoCompleteExtender id="ext1" runat="server">
<atlas:AutoCompleteExtenderProperties Enabled="true"
ServicePath="SuggestionService.asmx"
ServiceMethod="GetSuggestions" TargetControlID="txt1" />
</atlas:AutoCompleteExtender>
The behavior uses the given ServicePath and ServiceMethod properties to call the specified Web Service whenever suggestions are needed. The web service, written by you, can query a database or do whatever it needs to do in order to calculate the suggestions. Here is an example of a web service that provides the necessary method signature:

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class SuggestionService : System.Web.Services.WebService
{
public SuggestionService()
{
}
[WebMethod]
public string[] GetSuggestions(string prefixText, int count)
{
return new string[] { "abc", "def", "ghi" };
}
}
The prefixText parameter is the text the user has entered thus far, and the count parameter is the maximum number of suggestions you should return. And just like that, we have auto-completion!


Auto Completion via a Web Service

Going one step further


It's the greatest thing since the variable geometry nacelle. But, there are some limitations to this approach. Because it must call a web service, the logic for determining the suggestions is isolated from the page that contains the textbox. That means you do not have access to any state information that is available on the page. In many cases that may not be an issue. It may even be good design to isolate that logic into a web service. But because you lack that state information, you are forced to configure the web service via global means such as the web.config. For example, a connection string. What if a developer wants auto completion for different database than the one the rest of the application is using? What if the suggestions that you want to show depend on the value of another control on the page, such as a date range that should limit the search results of the query? Or maybe you have a "product category" dropdown and the textbox should only suggest results for products that are in that category? What if you are a control developer (like me) and you want to create a custom textbox control that has auto completion built into it -- how will you effectively package together your custom control and the web service? Why should I have to create a web service to provide auto completion at all?

Why can't I wield the power of auto-completion all within the page itself?

Yes... Let's do that. Presenting the SmartAutoCompleteExtender and the SmartTextBox. In creating these two controls I had a strategy in mind:

  1. Create an Auto Complete extender that uses a CALLBACK rather than a web service. This will allow all the logic to calculate the suggestions to live on the same page.

  2. Support a Web Service too, for flexibility.

  3. Utilize the existing auto complete behavior script, so I don't have to write complex JavaScript.

  4. Raise an Event in the Extender control when suggestions are needed. The event model will allow users of the extender to provide suggestions easily within the page and multiple listeners could potentially work together to create suggestions from different sources.

  5. Create a TextBox with a built-in Auto Complete extender, so users of that control can provide auto completion without even knowing what Atlas is


This being my first custom extender for Atlas, I did run into a few problems. But in less than a day I was able to piece these controls together. If I had to do it again, already having learned all the lessons that I did, I could probably do it on the order of a few hours. This is a testament to the extensibility of Atlas. More on that later.

First, let us look at the SmartAutoCompleteExtender. To use it I must register my custom control assembly and namespace with the obligatory Register directive at the top:

<%@ Register Assembly="Infinity.Web" Namespace="Infinity.Web.UI.WebControls" TagPrefix="i88" %>
i88 is a nice tag prefix I like to use. Now lets replace the built in atlas extender with the smart one:

<i88:SmartAutoCompleteExtender Enabled="true" id="ext1" runat="server">
<i88:SmartAutoCompleteProperties
TargetControlID="txt1" />
</i88:SmartAutoCompleteExtender>
Notice there's no ServicePath or ServiceMethod specified. This extender uses an event on the server side instead. But if you wanted to use a web service, it supports that too:

<i88:SmartAutoCompleteExtender Enabled="true" id="ext1" runat="server">
<i88:SmartAutoCompleteProperties
TargetControlID="txt1"
Mode="WebService"
ServicePath="SuggestionService.asmx"
ServiceMethod="GetSuggestions" />
</i88:SmartAutoCompleteExtender>
But, we're more interested in the event. Just to keep these code lines shorter (screen real estate is prime on this blog .. I need a better medium!), create a namespace alias in your code behind:

using I88=Infinity.Web.UI.WebControls;
Then you can hook into the event on the extender like so:

protected override void OnInit(EventArgs e)
{
this.ext1.AutoCompleteCallback += new I88.EventHandler(ext1_AutoCompleteCallback);
base.OnInit(e);
}
AutoCompleteCallback is the event that fires when the client side needs suggestions. Now for the event handler itself:

void ext1_AutoCompleteCallback(object sender, I88.AutoCompleteEventArgs args)
{
int count = args.Count;
string prefix = args.Prefix;

args.CompletionItems = new string[] { prefix + "A", prefix + "B", prefix + "C" };
}
The event argument passed to the event handler contains three properties:

  1. Count - The maximum number of suggestions that should be returned

  2. Prefix - The characters entered so far into the textbox

  3. CompletionItems - a string array of suggestions to be set by you.
Just for demonstration and to prove it is working, we take the prefix and create three suggestions by adding "A", "B" and "C" to it. Here we go:


Look ma, no web service!

Excellent. The beauty of this is that it really was not very hard to do. I was able to reuse the existing javascript behavior that is defined for the built-in AutoCompleteExtender. The custom behavior javascript simply creates an instance of the built in behavior and lets it do all the dirty work. All that was required is to hook into the timer object it uses by replacing its event handler with my own, and replacing existing methods with your own is easy to do with javascript. To do the actual callback, it uses asp.net's built-in javascript function for that, too: WebForm_DoCallback. By passing it the ID of the extender control, and implementing ICallbackEventHandler in the extender control, its easy to raise the auto complete event.

Going one step further, again


Indeed why stop there... wouldn't it be nice if you didn't even need to create an extender? Why can't auto complete be a built-in feature of the textbox itself? Man you sure are demanding. Fine...

Get rid of the extender declaration. Then replace the TextBox with, well... the i88:SmartTextBox:

<i88:SmartTextBox ID="txt1" runat="server" EnableAutoComplete="true" />
Don't forget the EnableAutoComplete="true" attribute, because by default it is disabled. You might wonder why I didn't call this control "AutoCompleteTextBox" instead. Well because I don't believe in naming controls (or classes) after their features. The SmartTextBox might one day be extended to support many other advanced features, of which Auto Completion would be just one. Just planning ahead a little. "Smart" may not be the best name either, but its the best I could come up with ok? :)

Internally, the SmartTextBox creates a SmartAutoComplete extender for you. It also surfaces a property that allows you to have access to all the same properties the extender does. So once again you can use a web service or a callback, whatever floats your boat:

<i88:SmartTextBox ID="txt1" runat="server" EnableAutoComplete="true"
AutoComplete-Mode="WebService"
AutoComplete-ServicePath="SuggestionService.asmx"
AutoComplete-ServiceMethod="GetSuggestions" />
But as before, we're only interested in the callback mode for this article. The SmartTextBox also surfaces the event itself -- so if all you knew about was the SmartTextBox control, you could quickly and easily add custom auto completion to it, without ever hearing the word "Atlas" muttered. Change the event handler like so:

protected override void OnInit(EventArgs e)
{
this.txt1.AutoCompleteCallback += new I88.EventHandler(txt1_AutoCompleteCallback);
base.OnInit(e);
}

void txt1_AutoCompleteCallback(object sender, I88.AutoCompleteEventArgs args)
{
int count = args.Count;
string prefix = args.Prefix;

args.CompletionItems = new string[] { prefix + "A", prefix + "B", prefix + "C" };
}
And put it into action... remember to type at least 3 characters, because the default minimum length is 3. That is a property you can change if you like.


Look ma, no web service AND no Extender!

Yes I cheated -- this screenshot is exactly the same one as before. But that's because it looks exactly the same anyway. Even the 'view source' on the page looks exactly the same. The only difference is the declaration in the form.

Download the source code here!

I should also mention that the project type is the Web Application project option introduced by Microsoft, described in detail and available for download here. If you don't have it already, you should. It will be baked in on the first service pack for VS2005 anyway. If you'd rather not, then you'll have to rebuild the project file, or perhaps you can change the project type guid so it's a class library project instead. Sorry for the confusion.

Use the source however you like... change it, trash it, claim it as your own, I don't mind :) All I ask is you send some friends over to this tiny corner of the internet. I enjoy having readers. :)

Happy coding!

UPDATE 06/08/2006:

Someone pointed out a bug with the project on the asp.net forums. Click here to read the post. I have re-uploaded a new project with the fix.

I also added a new property to the extender: EnableCache (boolean). You see, the built in behavior has a nice little performance feature built into it. Once it requests suggestions for prefix "abc", it remembers them in a cache variable on the client. So if you type in "abcd", wait for the auto complete to occur again, then delete the "d", the behavior is able to recall the suggestions it already calculated without calling the webservice or performing another callback. But this presents a problem in callback mode if you try to use this custom extender in the ways it is advertised, where the suggestions may depend on the state of other controls on the page. The state of those controls may change, and the cache of suggestions will be wrong. And so it was simple to add the EnableCache property. Normally you should leave this option alone. Only disable the cache if you know you might give different suggestions for the same prefix text, because the values are dependant on other controls.

And finally, I have also included a sample web application that uses the SmartTextBox in callback mode. It also contains a RadioButtonList, and the suggestion items vary based on the selected value of the list.

Thanks to ethos42 for the bug report!
Download the Source Code here!

UPDATE 06/10/2006:

Unfortunately for me but fortunate for you guys, jjradke pointed out another bug. The event will fail to fire if you place the textbox within a naming container (for example within a template column of a datagrid, among other things). The bug was due to it using the ClientID instead of the UniqueID... which has been fixed!! If you already downloaded the source you can easily patch your copy by changing this line (approximately line 90 in the SmartAutoCompleteExtender.cs file)

writer.WriteAttributeString("id", this.ClientID);

To this:

writer.WriteAttributeString("id", this.UniqueID);

Thanks for the bug report!

UPDATE: 08/24/2006:

I'm sorry to have put you guys through downloading from FilePlanet. The link eventually broke, but thankfully I've found a more suitable home for the source code. The download links should work once again.