24 November 2008
This is a comprehensive developer's guide to using AutoCompleteBox. With the initial Silverlight Toolkit release, we focused on providing API reference documentation. This is "the missing guide" that covers the full breadth of the control.
Along with the next toolkit release, I will release guides on the ISelectionAdapter interface and more involved customizations that people have been asking me about.
A customized auto complete box: custom item template; re-templated control for additional adornment; custom population events used for custom highlighting behaviors.
One of the new controls in the Silverlight Toolkit is the AutoCompleteBox control. It is a control that is composed of a text box for text entry, a rich set of properties for customization and item display, data binding support, and all the necessary logic to provide suggestions and completion.
Today, this control is in the Preview quality band of the toolkit, so it may have a moderate number of breaking API or behavior changes in response to customer feedback. This post is accurate for the December 2008 Silverlight Toolkit and November 2008 releases, with the few differences and changes indicated inline.
Our control is designed with the majority of developers in mind, so it covers a great number of scenarios, plus extensibility points and the ability to easily re-template the control to meet your needs. Inspired by many other auto complete controls, this implementation breaks new ground with its true data binding support, built-in set of search filters, and rich data template support.
You should know about some of its limitations: this control does not implement the "delimiter" feature, and is not designed to be drop-in compatible with an editable ComboBox. It also cannot restrict the user to selecting only the known items at this time, without some additional coding work. We're going to look towards customer feedback to help shape the eventual release of this control. We do envision bringing this control to WPF eventually, so look for that!
Covered in this guide:
To use AutoCompleteBox, add a reference to your Silverlight Application project. You should reference the Microsoft.Windows.Controls.dll binary from your Siverlight Toolkit download. You will also need to add the "controls" prefix declaration for the toolkit. Make sure that your page declaration includes the "controls" namespace:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls"> <!-- Page goes here --> </UserControl>
To add the control to your page, simply add a new instance of AutoCompleteBox in XAML:
<controls:AutoCompleteBox x:Name="autoComplete1" />
Typically, you'll want to provide an x:Name for the control, too. The sample project includes a few XAML-only AutoCompleteBox examples, but most of the time you'll also be interacting with the control at least a little bit through code.
The properties that you will find yourself setting most often to customize the experience you provide your end users are: ItemsSource, IsTextCompletionEnabled, MinimumPrefixLength, and MinimumPopulateDelay. These properties are covered in a few sections below.
The text that is displayed in the AutoCompleteBox's nested text box will always be available through the Text dependency property of the control. The SelectedItem property will either be null, or set to the selected item instance.
You can experience the AutoCompleteBox control today by checking out the live samples hosted here. Also,
The AutoCompleteBox for Silverlight offers more or less the same experience as an AJAX auto complete implementation, with the added benefit of supporting collections of rich business objects, rich UI-based templating for items, and the ability to substitute a standard list box control with a custom selection control. Since it re-uses the built-in Silverlight text box, you also get additional support for IME support that has traditionally been a difficult issue for auto complete implementations in JavaScript.
Some of the important features worth calling out are:
The toolkit's offline CHM help file, included with every toolkit download, shows each and every property.
ItemsSource
The items source is an IEnumerable that should contain a collection of the items you would like to have available for the control.
IsTextCompletionEnabled
This property will perform the regular search on your items for a suggestion, and if any items are found, the first (the "best match") will be selected, update the text box's string value, and the rest of the text will be highlighted so that your end user can either continue typing, or press backspace, to remove the suggestion.
Default value: False (December release), True (November release)
This is similar to the WPF ComboBox's IsTextSearchEnabled property, or the YUI AutoComplete's TypeAhead property.
MinimumPopulateDelay
This is the amount of time, in milliseconds, that elapses after the user types and the population event is fired. The default value is 0, but when connecting to external web services, you'll want to use this to gate request time by making it a larger value, such as 100 or 200.
Keep in mind that the auto complete will feel a little slower in these scenarios: you have the minimum populate delay, the request to the server, network latency, the response, parsing the response, and updating the items before any suggestions will be shown.
MinimumPrefixLength
This is the minimum number of characters that must be entered before the control looks for suggestions. The default value is 1, but a lot of classic AJAX auto complete controls use 3 as the default value, so this is a common customization property.
You may want to dynamically change this value based on the number of items you have available in your index or collection, since a prefix length of "1" with the complete English dictionary would have a massive performance impact, and not be helpful to most users.
Setting a value of -1 is permitted, and will ensure that suggestions are never provide - effectively disabling the auto complete functionality of the control.
SearchMode
The control exposes a 'search mode' property that represents the filter used on items on the client side. The default search mode is 'StartsWith,' and represents a typical auto complete control's filter.
If you'd like to change the filter to a different string comparison, build a filter to check several properties on a business object, or otherwise customize the experience, you can. You'll also probably want to simply set the SearchMode to "None" if you perform all the filtering in a server-side index or web service.
The default "StartsWith" value simply checks that each item's string value starts with the user's search string. If it does match, then the item will appear as a suggestion.
"Contains" is another oft-used property, and it checks that the user's search string appears anywhere in an item's string representation. For details on how a string value is found, take a look farther down at the data binding topic.
The AutoCompleteBox control is similar to other items controls in Silverlight, such as ComboBox, in that you set or bind the ItemsSource to a collection of items. The control then takes care of providing the filtering algorithm and walking through the items of the collection at the appropriate time.
Although most users will set the ItemsSource one time, there are scenarios where you may be hooking up to a web service, searching through an index, or otherwise only interested in updating the items when necessary: in those situations, you may update ItemsSource each time.
The control maintains a view of the suggestion data behind the scenes. You can get a read-only collection of that view in any Populated event handler.
Built-in ObservableCollection support was not available in original November release.
ObservableCollection support has been checked into the internal development branch and will be released in the December Toolkit.
To work around this for now, whenever changing an observable collection, you can just swap null and the collection: so set ItemsSource to null briefly, and then back to the observable collection - the cached items and the view will be updated.
How the text value of an item is determined
For performance reasons, there is no binding expression for the text value. This is one of the differences between this control and a combo box - typically there is a DisplayMemberPath that helps with that association. Since every key press could theoretically require the control to walk through the entire collection of items, more efficient means are used for getting the text to show in the text box control.
1. Converter property: If there is an IValueConverter set on the control, it is used to convert an item to a string value.
2. ToString method: If there is no value converter, or the converter returns null, then the ToString of the item is called. For simple types, like integers and strings, no change is necessary. For complex business objects, WCF proxy types, etc., you will want to override the ToString to provide a meaningful text value.
This control is not designed as a ComboBox replacement, but does offer similarities. By re-templating, for instance, you can set the actual DisplayMemberPath value on the nested list box. However, it is much easier to just create a new data template for the items to be displayed.
To mimic the behavior of DisplayMemberPath, simply create a new data template for the items, and place a ContentPresenter or TextBlock in it. If binding to a "FullName" property, for instance, here would be the equivalent template:
<controls:AutoCompleteBox x:Name="MyControl"> <controls:AutoCompleteBox.ItemTemplate> <DataTemplate> <StackPanel> <ContentPresenter Content="{Binding FullName}" /> </StackPanel> </DataTemplate> </controls:AutoCompleteBox.ItemTemplate> </controls:AutoCompleteBox>
For more advanced visualizations of data, you can place any set of UI elements and bindings in this template. Here is an example that shows a stock ticker, company name, and exchange market. This example has a few hard-coded strings to save you from having to see the highlighting code. For more information on actually implementing some of this functionality, check out this previous post.
<controls:AutoCompleteBox Width="315" x:Name="MyAutoCompleteControl" IsTextCompletionEnabled="False" Style="{StaticResource MyAutoComplete}"> <controls:AutoCompleteBox.ItemTemplate> <DataTemplate> <Grid Width="600"> <Grid.ColumnDefinitions> <ColumnDefinition Width="150" /> <ColumnDefinition Width="350" /> <ColumnDefinition Width="100" /> </Grid.ColumnDefinitions> <myapp:HighlightingTextBlock HighlightText="Micro" HighlightBrush="Black" HighlightFontWeight="Bold" Text="{Binding TickerSymbol}" /> <myapp:HighlightingTextBlock Grid.Column="1" HighlightBrush="Black" HighlightFontWeight="Bold" HighlightText="Micro" Text="{Binding Title}" /> <TextBlock HorizontalAlignment="Right" Foreground="Gray" Text="{Binding Exchange}" Grid.Column="2" /> </Grid> </DataTemplate> </controls:AutoCompleteBox.ItemTemplate> </controls:AutoCompleteBox>
The AutoCompleteSearchMode enum contains these entries:
"None" disables filtering and so all items in the items source will appear in the suggestions drop down.
"Custom" signifies that a custom text or item filter is in use. You must set the SearchMode to 'Custom' or else any text or item filter will be ignored.
This concept deserves a complete guide of its own. The custom filters are designed for coders who would rather write their own text handling logic, or work directly with the item instances, in determining whether items are actual suggestions.
Text handling will be useful if you're building a search application and want to build some kind of AND/OR logic into the box: if you are writing a photo search box for Flickr, you might want to parse and tokenize the text in the AutoCompleteBox, then perform an AND search over a web service for that.
When using your own ItemFilter or TextFilter, remember to set the SearchMode to Custom.
The filter is called with every item in the items source collection, whenever population needs to happen (to go find suggestions).
Most developers will use the ItemFilter: it is effectively a predicate that has two parameters: the first is the current string value of the text box that the user is typing into; the second is the item (of type object).
Here's a sample item filter, set in the Loaded event of my page:
MyAutoCompleteControl.ItemsSource = Stocks; MyAutoCompleteControl.SearchMode = AutoCompleteSearchMode.Custom; MyAutoCompleteControl.ItemFilter = (search, item) => { Stock stock = item as Stock; if (stock != null) { search = search.ToUpper(); return (stock.TickerSymbol.ToUpper().Contains(search) || stock.Title.ToUpper().Contains(search) || stock.Exchange.ToUpper().Contains(search)); } else { return false; } };
The code can get messy when working with many auto complete box instances and related data types. I personally recommend adding an item filter static method, or extension method, to your client-side business objects and/or project.
Here's the same code that goes in Loaded of my page, to use the filter:
MyAutoCompleteControl.ItemsSource = Stocks; MyAutoCompleteControl.SearchMode = AutoCompleteSearchMode.Custom; MyAutoCompleteControl.ItemFilter = (txt, i) => Stock.IsAutoCompleteSuggestion(txt, i);
And a simplified version of Stock.cs, used to represent a company, that contains the actual filter method. The code isn't great, and will fall flat on its face if any of the properties are null, but it demonstrates the concept:
public class Stock { public Stock(string symbol, string title, string exchange) { TickerSymbol = symbol; Title = title; Exchange = exchange; } public string TickerSymbol { get; set; } public string Title { get; set; } public string Exchange { get; set; } public static bool IsAutoCompleteSuggestion(string search, object item) { Stock stock = item as Stock; if (stock != null) { search = search.ToUpper(); return (stock.TickerSymbol.ToUpper().Contains(search) || stock.Title.ToUpper().Contains(search) || stock.Exchange.ToUpper().Contains(search)); } else { return false; } } public override string ToString() { return TickerSymbol; } }
This is a great feature because it means that you can provide an experience where you can search many of the properties of your business objects for the search string: first name, last name, full name, airport name, airport code, stock ticker symbol, car model, you name it - you can do it. The filter just returns true/false to indicate whether the item exists.
A text filter is a little more simple. The parameters of it are both string values: the search string, and then the string representation of an item. The filter is called once for every item in the items source collection. Again, the string value is determined by first trying the Converter, and then the ToString.
Several events are exposed that can open up the control to hooking up to custom data sources and services, building your own logic and suggestion filtering algorithm, or otherwise working around any limitations in the platform.
Suggestion population events: Populating, Populated
These events let you perform your own search logic, cancel the standard behavior optionally, and then call PopulateComplete when finished. The web service example was covered in the original Introducing AutoCompleteBox post.
Populating:
Called whenever the control is ready to search for suggestions, this is your opportunity to intercept the standard filtering and manage the items source first.
If you have the data available immediately, before your handler returns, you can simply change the ItemsSource property of the AutoCompleteBox control right here, no other changes necessary.
But if you’re going to perform an asynchronous operation (background thread calculations; call a web service), you need to set the Cancel property of the event arguments to True. This will cancel the built-in AutoComplete filtering and effectively tell the control to hold off until you are ready.
To continue the suggestion process, you need to then call the PopulateComplete method. In your web service completion event, you’ll want to place this call.
Populated:
This is called once suggestions are found and ready to be displayed. The Data property of the event arguments gives you read-only access to the view that will be provided to the selection control.
Drop down events
These events are exposed for the drop down, and may give you a chance to look through the visual tree to perform some fancier workarounds and effects. The Opening and Closing events can be canceled.
DropDownOpening
DropDownOpened
DropDownClosing
DropDownClosed
Note that the state of the control and suggestion requests may be impacted by cancellation of the drop down events, and lead to undesirable results.
In the JavaScript world, typically all auto complete controls connect straight to a service on a web server. In Silverlight, with the presence of rich client-side business objects, the auto complete control assumes that you instead use the Visual Studio tooling support for web services, or create your own web service plumbing for these types of servers. In Silverlight, working with JSON is super easy thanks to the System.Json client library that is included with the Silverlight 2 SDK.
A JSON + PHP web service was connected in this post near the bottom. You can connect to services using whatever you like: I've seen excellent SOAP, JSON, POX, and even traditional REST services like the complete Flickr API and the new Live Search 2.0 APIs work great with AutoCompleteBox from the Silverlight Toolkit.
Here, again, is the point where the asynchronous web service call is made by the control:
private void OnLoaded(object sender, RoutedEventArgs e) { AutoComplete1.IsTextCompletionEnabled = false; // Server does the filtering AutoComplete1.SearchMode = AutoCompleteSearchMode.None; AutoComplete1.Populating += (s, args) => { args.Cancel = true; WebClient wc = new WebClient(); string prefix = HttpUtility.UrlEncode(args.Parameter); Uri service = new Uri("http://www.jwpc.com/services/suggest/?prefix=" + prefix); wc.DownloadStringCompleted += DownloadStringCompleted; wc.DownloadStringAsync(service, s); }; }
And here's the JSON parsing of the result:
private void DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e) { AutoCompleteBox acb = e.UserState as AutoCompleteBox; if (acb != null && e.Error == null && !e.Cancelled && !string.IsNullOrEmpty(e.Result)) { List<string> suggestions = new List<string>(); JsonArray ja = JsonArray.Parse(e.Result) as JsonArray; foreach (JsonPrimitive suggestion in ja) { suggestions.Add(suggestion.ToString()); } if (suggestions.Count > 0) { acb.ItemsSource = suggestions; acb.PopulateComplete(); } } }
The control is typically composed of a text box and a popup of some sort. Though not a core scenario that we test, in theory you can re-template the control without a popup. Tim Heuer has also templated AutoCompleteBox to act like an editable ComboBox, for instance.
Template parts
The AutoCompleteBox implementation looks for and uses the following template parts:
It also exposes these styles:
On top of the standard Visual State Manager (VSM) states, you will also find:
The default template uses these states to fade the popup in and out.
And, you can re-template to bind to anything you like, add display member bindings, use different controls in the popup (like a TreeView, DataGrid, movie player, etc.). To use your own control inside the drop down, you'll need to implement the ISelectionAdapter interface or write your own wrapper/adapter for the control to interact with AutoCompleteBox.
Default style & control template
Here is the default template used for the control, feel free to use this as a starting point for re-templating. It includes a text box, the list box, and the VSM glue to get the popup to fade in and out:
<Style TargetType="controls:AutoCompleteBox"> <Setter Property="IsTabStop" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="controls:AutoCompleteBox"> <Grid Margin="{TemplateBinding Padding}" Background="{TemplateBinding Background}"> <TextBox IsTabStop="True" x:Name="Text" Style="{TemplateBinding TextBoxStyle}" Margin="0" /> <Popup x:Name="Popup"> <Border x:Name="PopupBorder" HorizontalAlignment="Stretch" Opacity="0.0" BorderThickness="0" CornerRadius="3"> <Border.RenderTransform> <TranslateTransform X="1" Y="1" /> </Border.RenderTransform> <Border.Background> <SolidColorBrush Color="#11000000" /> </Border.Background> <Border HorizontalAlignment="Stretch" Opacity="1.0" Padding="0" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" CornerRadius="3"> <Border.RenderTransform> <TransformGroup> <TranslateTransform X="-1" Y="-1" /> </TransformGroup> </Border.RenderTransform> <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFDDDDDD" Offset="0"/> <GradientStop Color="#AADDDDDD" Offset="1"/> </LinearGradientBrush> </Border.Background> <ListBox x:Name="SelectionAdapter" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" ItemContainerStyle="{TemplateBinding ItemContainerStyle}" ItemTemplate="{TemplateBinding ItemTemplate}" /> </Border> </Border> </Popup> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="PopupStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0:0:0.1" To="PopupOpened" /> <VisualTransition GeneratedDuration="0:0:0.2" To="PopupClosed" /> </VisualStateGroup.Transitions> <VisualState x:Name="PopupOpened"> <Storyboard> <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="1.0" /> </Storyboard> </VisualState> <VisualState x:Name="PopupClosed"> <Storyboard> <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="0.0" /> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style>
You can easily retemplate the control to add additional controls and functionality to the popup/drop-down used for suggestions. Simply retemplate and add your content alongside the list box! You'll want to do some validation of the experience, since clicking on or interacting with controls inside the popup may remove focus from the auto complete box control and disable and further typing functionality for the user.
Here's a partial sample adornment used from a stock ticker lookup. Important: this is effectively the inside of the popup, inside the final border of the template listed above - it is not a complete listing. It also contains hard-coded text strings to skip the search text updating for the adorned text block and hyperlinks...
<StackPanel> <ListBox x:Name="SelectionAdapter" BorderThickness="0" BorderBrush="Transparent" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" ItemContainerStyle="{TemplateBinding ItemContainerStyle}" ItemTemplate="{TemplateBinding ItemTemplate}" /> <StackPanel Background="White"> <TextBlock Margin="5" Foreground="#FF0066FF" FontWeight="Bold">Chart all results for "Micro"</TextBlock> <StackPanel> <StackPanel.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="#FFFFFFFF" Offset="0"/> <GradientStop Color="#FFEEEEEE" Offset="0.277"/> <GradientStop Color="#FFEEEEEE" Offset="1"/> </LinearGradientBrush> </StackPanel.Background> <StackPanel Margin="5" Orientation="Horizontal"> <TextBlock> <Run FontWeight="Bold">Tip: </Run> <Run>If you haven't sold it, you haven't lost it. </Run> </TextBlock> <HyperlinkButton Margin="8, 0, 0, 0" Content="Learn more..." Foreground="#FF0066FF" /> </StackPanel> </StackPanel> </StackPanel> </StackPanel>
The December toolkit release added a few new features for this control, including a small breaking change and different default value. Take note when updating applications.
There were additionally some minor bug fixes. As the control exits the Preview quality band, such simple churn should cease.
The Silverlight Toolkit is released under the Microsoft Public License (Ms-PL). I'm not a lawyer, but within the license rights, you can pretty do just about anything you want. Awesome!
You'll find the latest version of the Silverlight Toolkit on the associated CodePlex site. And, as always, check out the interactive sample application that features all of the controls.
On the site you will also be able to review and log feature requests, bugs, and vote on existing issues. Please feel free to discuss the control on the dedicated Silverlight.net forum.
If you have any feature requests or bug reports, please file them here. And, as the developer of the control, I welcome your general feedback as well. After the holiday break, I will post about ISelectionAdapter and whatever you ask me to post about!
If you're in the US, have a great Thanksgiving holiday!
Jeff Wilcox
AutoCompleteBox Developer
Silverlight Toolkit
Jeff Wilcox is a Software Engineer at Microsoft in the Open Source Programs Office (OSPO), helping Microsoft engineers use, contribute to and release open source at scale.