Click here to Skip to main content
Click here to Skip to main content

A Reusable WPF Autocomplete TextBox

, 5 Jan 2010
Rate this:
Please Sign up or sign in to vote.
A custom control based on a TextBox which allows autocompletion based on a custom filter from any items source.

Introduction

One of the sorely missed features in the WPF control arsenal is an auto-complete text box. In this article, I will create an auto-complete text box, which is similar to what you can find on the web, for example when typing into the Google search box.

I created this text box as a WPF custom control, thereby retaining the ability to style and template the control with maximum flexibility. Additionally I designed this control to be reusable, and to harness the full power of WPF with regards to dependency properties and data binding. The control is very easy to use, simply set the ItemsSource property, and set a Binding property to indicate which data field to use as the completion source. The actual filter is set as a delegate in the code-behind.

There are several road blocks I encountered on my way, and I will go into detail in the next section, but here they are summarized:

  • Exposing the important dependency properties of the list box
  • Focus issues, and how they affect the popup auto-close behavior, and completion list keyboard navigation
  • Evaluating a Binding object on a data item dynamically (from code)
  • Limiting the number of completions shown

In the discussion up ahead, I will not spam the article with code. I will give short examples when necessary, and describe the logic in English. There are two reasons for this, first, there's a lot of code and secondly, I wish to be able to correct and change the code style without having to update the article too much.

Using the Code

I will start by demonstrating a simple use case for the control, to show how easy it is to use it. Here's a minimalistic XAML code that uses an auto-complete text box using the U.S. national weather service observation stations' RSS feed.

Here's how it looks while typing, with keyboard navigation into the completion list:

And here it is after the selection (pressed Enter):

And the code:

<Window x:Class="Test.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:actb="clr-namespace:Aviad.WPF.Controls;assembly=Aviad.WPF.Controls"
    Title="Window1" Height="300" Width="600">
    <Window.Resources>
        <XmlDataProvider x:Key="xml" 
	Source=http://www.nws.noaa.gov/xml/current_obs/index.xml 
	DataChanged="XmlDataProvider_DataChanged"/>
        <DataTemplate x:Key="TheItemTemplate">
            <Border BorderBrush="Salmon" BorderThickness="2" CornerRadius="5">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition/>
                        <RowDefinition/>
                        <RowDefinition/>
                    </Grid.RowDefinitions>
                    <TextBlock Text="ID:  "/>
                    <TextBlock Grid.Column="1" Text="{Binding XPath=station_id}"/>
                    <TextBlock Grid.Row="1" Text="Name:  "/>
                    <TextBlock Grid.Column="1" Grid.Row="1" 
			Text="{Binding XPath=station_name}"/>
                    <TextBlock Grid.Row="2" Text="RSS URL:  "/>
                    <TextBlock Grid.Column="1" Grid.Row="2" 
			Text="{Binding XPath=rss_url}"/>
                </Grid>
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <TextBlock x:Name="StatusLabel" Text="Loading Data..." Margin="20,20,0,0"/>
        <actb:AutoCompleteTextBox 
            x:Name="actb" 
            Margin="20,40,20,0"
            VerticalAlignment="Top" 
            ItemsSource="{Binding Source={StaticResource xml}, XPath=//station}" 
            ItemTemplate="{StaticResource TheItemTemplate}"
            Binding="{Binding XPath=station_id}" 
            MaxCompletions="10"/>
    </Grid>
</Window>

And here is the code-behind which sets the filter up:

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        actb.Filter = Filter;
    }
 
    private bool Filter(object obj, string text)
    {
        XmlElement element = (XmlElement)obj;
        string StationName = 
		element.SelectSingleNode("station_name").InnerText.ToLower();
        if (StationName.Contains(text.ToLower())) return true;
        return false;
    }
 
    private void XmlDataProvider_DataChanged(object sender, EventArgs e)
    {
        StatusLabel.Text = "Xml Data Loaded.";
    }
}

Note how the filter uses the station_name element to filter the auto-completion list, and the XAML specifies that the station_id is extracted from the selected entry. Run the sample and try it out, remember to wait for the XML data to load, it's not small.

In the last section of this article I will show an advanced use case for this text box, using the Google Suggest API.

The Implementation

Conceptually, an auto-complete text box is a text box and a list box in one, but since .NET doesn't allow for multiple inheritance, I had to choose which part is the more prominent one. I chose the TextBox as my base class, and the ListBox control is added as a part in the control template. Additionally, there is the feature which causes the completion list to pop in and out of view when appropriate, and to do that I used a Popup control.

Exposing Dependency Properties

Pushing the list box into the template made several important properties inaccessible, namely:

  • ItemsSource
  • ItemTemplate
  • ItemContainerStyle
  • ItemTemplateSelector

Therefore, to begin with, I exposed these properties in my control. I used the AddOwner method of the appropriate DependencyProperty objects which are part of the ItemsControl class. Here's how it's done for one property, it's similar to the other two.

public static readonly DependencyProperty ItemsSourceProperty =
    ItemsControl.ItemsSourceProperty.AddOwner(
        typeof(AutoCompleteTextBox), 
        new UIPropertyMetadata(null, OnItemsSourceChanged));

Notice that I am assigning a callback method to each of the properties, in the callback method I "forward" the property to the internal list box (with some additional logic).

Next I provide three more properties:

  • Binding - A dependency property which holds the binding used to extract the text to use for auto-completion
  • MaxCompletions - A dependency property for limiting the number of completion results shown
  • Filter - A basic property which holds the callback delegate for filtering the items collection based on the text entered

The auto-complete logic works as follows: For each character typed, the collection view of the list box is refiltered, if at least one match is found, the popup is opened and the list is displayed. A completion entry may be selected by clicking or by using the keyboard. Once a completion entry is chosen, the text obtained using the Binding property from the selected item is put into the text box content, and the popup is closed.

The auto-completion completes successfully in one of the following cases:

  • The user clicks on an item in the list.
  • The user navigates the list using the arrow keys and presses the Return/Enter key or the Tab key on an item.

If the user chose a completion entry by using the keyboard and pressing Tab, the focus also moves to the next control in the tab order of the window.

The auto-completion is aborted in one of the following cases:

  • The user clicks anywhere outside the control area.
  • The user hits Escape.
  • The focus is on the text box itself and the user hits Tab.

Evaluating the Binding

Those of you who are familiar with the previous version of my article, know that I used a dirty hack to transfer the binding from the Binding property of the text box to the data object that is retrieved from the completion list. I recently discovered that I was doing this the wrong way by trying to set the binding on a dummy DependencyObject, and by using a FrameworkElement instead, I was able to use the existing binding and take advantage of the behavior where the default source of the binding is to the DataContext property.

// Retrieve the Binding object from the control.
var originalBinding = BindingOperations.GetBinding(this, BindingProperty);
if (originalBinding == null) return;
 
// Set the dummy's DataContext to our selected object.
dummy.DataContext = obj;
 
// Apply the binding to the dummy FrameworkElement.
BindingOperations.SetBinding(dummy, TextProperty, originalBinding);
 
// Get the binding's resulting value.
Text = dummy.GetValue(TextProperty).ToString();

The 'hack' version is still present in the source download comments, for those who are interested.

Limiting the Completions List

Sometimes the items collection used as the source for completion is very large and consists of thousands of entries. If we do not limit the completions list in some way, we would get a big performance hit on the first few characters we type, because of the time it takes the WPF framework to generate all the visuals that populate the list. We could iterate manually over the collection view and generate a list for display, but this will cause a new list to be generated for every key press. The better way would be to somehow cause the collection view to only expose its first (n) elements.

This is where I had to get creative. To accomplish that, I created a class called LimitedListCollectionView which inherits from ListCollectionView, and bounds size the collection it exposes according to some parameter. Then I overrode the Count property, and the MoveCurrentToNext/Last/Previous/Position methods:

public override int Count { get { return Math.Min(base.Count, Limit); } }
 
public override bool MoveCurrentToLast()
{
    return base.MoveCurrentToPosition(Count - 1);
}
 
public override bool MoveCurrentToNext()
{
    if (base.CurrentPosition == Count - 1)
        return base.MoveCurrentToPosition(base.Count);
    else 
        return base.MoveCurrentToNext();
}
 
public override bool MoveCurrentToPrevious()
{
    if (base.IsCurrentAfterLast)
        return base.MoveCurrentToPosition(Count - 1);
    else
        return base.MoveCurrentToPrevious();
}
 
public override bool MoveCurrentToPosition(int position)
{
    if (position < Count)
        return base.MoveCurrentToPosition(position);
    else
        return base.MoveCurrentToPosition(base.Count);
}
 
#region IEnumerable Members
 
IEnumerator IEnumerable.GetEnumerator()
{
    do
    {
        yield return CurrentItem;
    } while (MoveCurrentToNext());
}
 
#endregion

Notice the intricacies there, I allow the caller to iterate on the first Limit elements, once he tries to move past them, he's transported to the end of the collection.

Other Tricks

There are some other tricks I used in order to make this control truly feel like it's supposed to, and not like an ugly hack. One of those tricks is having the list selection indicator follow the mouse as it moves. Another is trapping the down arrow keypress and transferring focus to the list box to enable keyboard navigation. Additionally I hooked many input events to properly handle the auto-closing of the completion list in case of lost focus, keypresses, etc. I won't copy/paste all the code here. If you are interested, you may look in the source files.

Advanced Use Case - Google Search Box

In this section I will show step by step how to build a google search box, with search suggestions as you type. I will start by defining a "view model". The view model will serve as the backing store for all the data bindings in this example.

The view model defines three properties:

  • QueryText - The backing store for the user's text.
  • QueryCollection - The backing store for the completion list.
  • WaitMessage - Used to tell the user that a query is running in the background.

The QueryCollection property performs a blocking web request to Google's servers, and may take a long time to complete, we bind to it in an asynchronous way, using a PriorityBinding. The WaitMessage property is used as the "fast" property in that same PriorityBinding to notify the user of the pending query. An ItemTemplateSelector is used to select the appropriate template for the case of the wait message.

public class ViewModel : INotifyPropertyChanged
{
    private List<string> _WaitMessage = new List<string>() { "Please Wait..." };
    public IEnumerable WaitMessage { get { return _WaitMessage; } }
 
    private string _QueryText;
    public string QueryText
    {
        get { return _QueryText; }
        set
        {
            if (_QueryText != value)
            {
                _QueryText = value;
                OnPropertyChanged("QueryText");
                _QueryCollection = null;
                OnPropertyChanged("QueryCollection");
            }
        }
    }
 
    public IEnumerable _QueryCollection = null;
    public IEnumerable QueryCollection
    {
        get
        {
            QueryGoogle(QueryText);
            return _QueryCollection;
        }
    }
 
    private void QueryGoogle(string SearchTerm)
    {
        string sanitized = HttpUtility.HtmlEncode(SearchTerm);
        string url = @"http://google.com/complete/search?output=toolbar&q=" + sanitized;
        WebRequest httpWebRequest = HttpWebRequest.Create(url);
        var webResponse = httpWebRequest.GetResponse();
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(webResponse.GetResponseStream());
        var result = xmlDoc.SelectNodes("//CompleteSuggestion");
        _QueryCollection = result;
    }
 
    #region INotifyPropertyChanged Members
 
    public event PropertyChangedEventHandler PropertyChanged;
 
    #endregion
 
    protected void OnPropertyChanged(string prop)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(prop));
    }
}

Next we will create a CollectionViewSource that will hold the results of the query. Since we want the results to be dynamic, we will bind the Source property of the CollectionViewSoruce to the QueryCollection property of the view model. This is where we utilize the PriorityBinding binding class, since we know that the QueryCollection property is a "slow" operation, we put it together with a "fast" property in a PriorityBinding object. The WPF framework will use the "fast" binding until the "slow" one finally provides a value, and then it will notify everyone that the value has changed. Note that the "slow" binding is marked with IsAsync="True"; this causes it to be run in the background, and is required for the PriorityBinding to function correctly.

<CollectionViewSource x:Key="xml">
    <CollectionViewSource.Source>
        <PriorityBinding>
            <Binding Source="{StaticResource vm}"
                     Path="QueryCollection"
                     IsAsync="True"/>
            <Binding Source="{StaticResource vm}" Path="WaitMessage"/>
        </PriorityBinding>
    </CollectionViewSource.Source>
</CollectionViewSource>

Next we declare our auto-complete text box, and properly bind it.

<actb:AutoCompleteTextBox 
    Text=
"{Binding Source={StaticResource vm}, Path=QueryText, UpdateSourceTrigger=PropertyChanged}"
    ItemsSource="{Binding Source={StaticResource xml}}" 
    ItemTemplateSelector="{StaticResource TemplateSelector}"
    Binding="{Binding XPath=suggestion/@data}" 
    MaxCompletions="5"/>

Note that we bind the ItemsSource property to our CollectionViewSource, and our Text property to the view model's QueryText property. It's also important to set the update source trigger properly so that the list updates as we type.

Next in line is the visual templating. Since our completion list can be populated with two types of objects: An XML node list, or a simple list of strings in the case of the wait message, we need to prepare two templates, and to use a template selector. Here are the templates:

<DataTemplate x:Key="TheItemTemplate">
    <Border BorderBrush="Salmon" BorderThickness="2" CornerRadius="5">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <TextBlock Text="Suggestion:  "/>
            <TextBlock Grid.Column="1" 
                       Text="{Binding XPath=suggestion/@data}"/>
            <TextBlock Grid.Row="1" Text="Results:  "/>
            <TextBlock Grid.Column="1" 
                       Grid.Row="1" 
                       Text="{Binding XPath=num_queries/@int}"/>
        </Grid>
    </Border>
</DataTemplate>
<DataTemplate x:Key="WaitTemplate">
    <TextBlock Text="{Binding}" Background="SlateBlue"/>
</DataTemplate>

And here's the template selector definition:

public class MyDataTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(
        object item,
        DependencyObject container)
    {
        Window wnd = Application.Current.MainWindow;
        if (item is string)
            return wnd.FindResource("WaitTemplate") as DataTemplate;
        else
            return wnd.FindResource("TheItemTemplate") as DataTemplate;
    }
}

Viola, we're done. No code-behind is needed for the window, since we don't need to assign any special filtering logic to the completion list (we take everything returned by Google).

That's it, enjoy.

History

  • 25th November, 2009: Initial post
  • 27th November, 2009: Google search example added
  • 5th January, 2010: Improved code regarding Binding reapplication.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Aviad P.
Software Developer (Senior)
Israel Israel
Software developer since 1984, currently specializing in C# and .NET.

Comments and Discussions

 
QuestionTips: Binding property is mandatory. Completion list appears when TextBox is modified programmatically. PinmemberQwertie17-Jan-14 14:49 
GeneralMy vote of 2 PinmemberBruce-luu16-Oct-13 21:04 
GeneralRe: My vote of 2 PinmemberFrancesc Castells14-Nov-13 4:38 
QuestionCant get it to work with multibind PinmemberCurtis Moostoos22-Aug-13 5:09 
GeneralMy vote of 5 PinmemberMember 830115426-May-13 22:59 
QuestionDelete using X icon PinmemberMember 78993517-Feb-13 7:43 
BugA Reusable WPF Autocomplete TextBox Issue Pinmembermanvindar singh3-Jan-13 21:04 
QuestionCan someone do a code-behind version? [modified] PinmemberJerry Weltman18-May-12 9:12 
GeneralMy Vote Of 5 PinmemberShahin Khorshidnia24-Apr-12 4:53 
QuestionDesign time NullReferenceException PinmemberMorten Nilsen6-Mar-12 22:47 
AnswerRe: Design time NullReferenceException PinmemberMorten Nilsen7-Mar-12 0:41 
Questiongreat sample... help needed Pinmemberchj1242-Feb-12 9:24 
Questiongreat article! PinmemberMember 47736115-Jan-12 2:36 
SuggestionFocus issue? PinmemberMickey Mousoff1-Jan-12 8:48 
GeneralRe: Focus issue? [modified] PinmemberSerdar YILMAZ6-Apr-13 23:49 
QuestionIs a blocking query required? Async callback won't update itemssource PinmemberMike Hodnick10-Aug-11 4:26 
GeneralToda Ach Sheli !!! Pinmemberantraxant18-Apr-11 6:31 
GeneralIssue Pinmemberbrianduper15-Dec-10 17:19 
GeneralUse with JSON Webservice PinmemberPlew13-Dec-10 4:18 
GeneralAdded Features PinmemberJmiktutt29-Oct-10 9:22 
GeneralSuggest Append PinmemberJmiktutt29-Oct-10 9:39 
GeneralSelected Item PinmemberJmiktutt29-Oct-10 10:15 
GeneralVisual Studio Designer Error Pinmembercalvinwillman5-Oct-10 17:54 
GeneralItemsSource is null Pinmemberpeter.morlion8-Sep-10 8:02 
GeneralRe: ItemsSource is null Pinmemberpeter.morlion13-Sep-10 8:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140827.1 | Last Updated 5 Jan 2010
Article Copyright 2009 by Aviad P.
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid