WPF Auto-Complete Control

Posted by: Mahesh Sabnis , on 9/4/2010, in Category WPF
Views: 57588
Abstract: The WPF ComboBox lacks the auto-complete feature and in this article, we will develop our own WPF user control that behaves like the combobox, and supports auto-complete.
The WPF ComboBox lacks the auto-complete feature and in this article, we will develop our own WPF user control that resembles the combobox, and supports auto-complete.
Note: There are multiple ways of adding the auto-complete feature to the ComboBox control, like extending the existing ComboBox and then implementing the auto-complete feature. However for this article, we are creating our own user control that resembles the ComboBox, but with its own look and feel. Feel free to change the code to suit your requirement.
Step 1: Create a WPF application and add a user control to it. Name this as ‘AutoCompleteTextBox.xaml’.
Step 2: Write the following Xaml code in it:
<UserControl x:Class="WPF_Combobox_AutoComplete.AutoCompleteTextBox"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             mc:Ignorable="d"
             d:DesignHeight="140" d:DesignWidth="248" x:Name="rootControl">
    <Grid Height="120" Width="247">
        <TextBox Height="23" HorizontalAlignment="Left" Margin="88,49,0,0"
                 Name="txtAutoComplete" VerticalAlignment="Top" Width="120"
                 Text="{Binding ElementName=rootControl,Path=TextToChanged}"/>
        <ListBox Height="100" HorizontalAlignment="Left"
                 Margin="88,71,0,0" Name="lstData"
                 VerticalAlignment="Top" Width="120"
                 ItemsSource="{Binding ElementName=rootControl,Path=DataSource}"
                 UseLayoutRounding="True" IsTextSearchEnabled="False" />
    </Grid>
</UserControl>
 
The ListBox ‘lstData’ in the above code will be used to display the ‘possible filtered names’ based on what the user has already entered in the TextBox txtAutoComplete. 
Step 3: In this user control, I have added the following Dependency Properties.
C#
public string TextToChanged
{
get { return (string)GetValue(TextToChangedProperty); }
      set { SetValue(TextToChangedProperty, value); }
}
 
// Using a DependencyProperty as the backing store for TextToChanged. // This enables animation, styling, binding, etc...
public static readonly DependencyProperty TextToChangedProperty =
DependencyProperty.Register("TextToChanged", typeof(string),  typeof(AutoCompleteTextBox));
 
public IEnumerable DataSource
{
get { return (IEnumerable)GetValue(DataSourceProperty); }
      set { SetValue(DataSourceProperty, value); }
}
 
// Using a DependencyProperty as the backing store for DataSource. // This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register("DataSource", typeof(IEnumerable), typeof(AutoCompleteTextBox));
 
VB.NET (Converted Code)
Public Property TextToChanged() As String
Get
      Return CStr(GetValue(TextToChangedProperty))
End Get
       Set(ByVal value As String)
             SetValue(TextToChangedProperty, value)
       End Set
End Property
 
' Using a DependencyProperty as the backing store for TextToChanged. // This enables animation, styling, binding, etc...
Public Shared ReadOnly TextToChangedProperty As DependencyProperty = DependencyProperty.Register("TextToChanged", GetType(String), GetType(AutoCompleteTextBox))
 
Public Property DataSource() As IEnumerable
Get
      Return CType(GetValue(DataSourceProperty), IEnumerable)
End Get
       Set(ByVal value As IEnumerable)
             SetValue(DataSourceProperty, value)
       End Set
End Property
 
' Using a DependencyProperty as the backing store for DataSource. // This enables animation, styling, binding, etc...
Public Shared ReadOnly DataSourceProperty As DependencyProperty = DependencyProperty.Register("DataSource", GetType(IEnumerable), GetType(AutoCompleteTextBox))
 
The property ‘TextToChanged’ is used to store the text entered in the TextBox ‘txtAutoComplete’. The property ‘DataSource’ is used to bind with the ListBox ‘lstData’. This property holds all the values to be filtered.
 
Step 4: The following Routed Event is declared in the user control which will be raised when the TextChanged event of the TextBox ‘txtAutoComplete’ is handled.
C#
public event RoutedEventHandler TextBoxContentChanged
{
    add { AddHandler(TextBoxContentChangedEvent, value); }
    remove { RemoveHandler(TextBoxContentChangedEvent, value); }
}
 
public static readonly RoutedEvent TextBoxContentChangedEvent =
EventManager.RegisterRoutedEvent("TextBoxContentChanged", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoCompleteTextBox));
 
 
VB.NET (Converted Code)
Public Custom Event TextBoxContentChanged As RoutedEventHandler
      AddHandler(ByVal value As RoutedEventHandler)
            MyBase.AddHandler(TextBoxContentChangedEvent, value)
      End AddHandler
 
      RemoveHandler(ByVal value As RoutedEventHandler)
            MyBase.RemoveHandler(TextBoxContentChangedEvent, value)
      End RemoveHandler
 
      RaiseEvent(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs)
      End RaiseEvent
End Event
 
Public Shared ReadOnly TextBoxContentChangedEvent As RoutedEvent = EventManager.RegisterRoutedEvent("TextBoxContentChanged", RoutingStrategy.Bubble, GetType(RoutedEventHandler), GetType(AutoCompleteTextBox))
 
Step 5: The above routed event is raised in the TextChanged event of the ‘txtAutoComplete’  shown below. Also the text entered in ‘txtAutoComplete’ TextBox is stored in the dependency property ‘TextToChanged’ as shown below:
C#
public AutoCompleteTextBox()
{
     InitializeComponent();
txtAutoComplete.TextChanged += new TextChangedEventHandler(txtAutoComplete_TextChanged);
}
 
void txtAutoComplete_TextChanged(object sender, TextChangedEventArgs e)
{
     e.Handled = true;
     TextToChanged = txtAutoComplete.Text;
     RoutedEventArgs args = new RoutedEventArgs(TextBoxContentChangedEvent);
     RaiseEvent(args);
}
 
VB.NET (Converted Code)
Public Sub New()
      InitializeComponent()
      AddHandler txtAutoComplete.TextChanged, AddressOf txtAutoComplete_TextChanged
End Sub
 
Private Sub txtAutoComplete_TextChanged(ByVal sender As Object, ByVal e As TextChangedEventArgs)
      e.Handled = True
      TextToChanged = txtAutoComplete.Text
      Dim args As New RoutedEventArgs(TextBoxContentChangedEvent)
      MyBase.RaiseEvent(args)
End Sub
 
Step 6: Open MainPage.xaml and register the user control created in the previous steps, as shown below:
<Window x:Class="WPF_Combobox_AutoComplete.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:src="clr-namespace:WPF_Combobox_AutoComplete"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <Grid>
        <src:AutoCompleteTextBox x:Name="txtAutoCompleteTextBox" TextBoxContentChanged="txtAutoCompleteTextBox_TextBoxContentChanged"></src:AutoCompleteTextBox>
    </Grid>
</Window>
 
Step 7: In the MainPage.Xaml.cs or .vb, I have written the following code in ‘Windows_Loaded’ and the method ‘ListData()’ where the List<string> object is loaded with some default strings.
C#
List<string> lstNames;
 
private void Window_Loaded(object sender, RoutedEventArgs e)
{
     lstNames = new List<string>();
         
}
 
private void ListData()
{
            lstNames.Add("");
            lstNames.Add("Ajay");
            lstNames.Add("Rahul");
            lstNames.Add("Rajesh");
            lstNames.Add("Ravi");
            lstNames.Add("Suprotim");
            lstNames.Add("Ajit");
            lstNames.Add("Suraj");
            lstNames.Add("Vikram");
            lstNames.Add("Vikas");
            lstNames.Add("Pravin");
            lstNames.Add("Suprabhat");
            txtAutoCompleteTextBox.DataSource = lstNames; ;
}
 
VB.NET (Converted Code)
Private lstNames As List(Of String)
 
Private Sub Window_Loaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
       lstNames = New List(Of String)()
 
End Sub
 
Private Sub ListData()
                  lstNames.Add("")
                  lstNames.Add("Ajay")
                  lstNames.Add("Rahul")
                  lstNames.Add("Rajesh")
                  lstNames.Add("Ravi")
                  lstNames.Add("Suprotim")
                  lstNames.Add("Ajit")
                  lstNames.Add("Suraj")
                  lstNames.Add("Vikram")
                  lstNames.Add("Vikas")
                  lstNames.Add("Pravin")
                  lstNames.Add("Suprabhat")
                  txtAutoCompleteTextBox.DataSource = lstNames
 
End Sub
Step 8: In the MainPage.Xaml.cs or.vb, the event ‘TextBoxContentChanged’ event declared in the user control, is handled with the following code:
C#
private void txtAutoCompleteTextBox_TextBoxContentChanged(object sender, RoutedEventArgs e)
{
       ListData();
       string s = txtAutoCompleteTextBox.TextToChanged;
       var filter = (from name in lstNames
                     where (name.Contains(s))
                     select name).ToList<string>();
       foreach (var item in filter)
       {
           if (item.Equals(s))
            {
                  txtAutoCompleteTextBox.TextToChanged = s;
            }
       }
 
       txtAutoCompleteTextBox.DataSource = filter;
 
       if (s == string.Empty)
       {
             filter.Clear();
             txtAutoCompleteTextBox.DataSource = filter;
       }
}
 
VB.NET
Private Sub txtAutoCompleteTextBox_TextBoxContentChanged(ByVal sender As Object, ByVal e As RoutedEventArgs)
         ListData()
         Dim s As String = txtAutoCompleteTextBox.TextToChanged
         Dim filter = (
             From name In lstNames
             Where (name.Contains(s))
             Select name).ToList(Of String)()
         For Each item In filter
               If item.Equals(s) Then
                         txtAutoCompleteTextBox.TextToChanged = s
               End If
         Next item
 
         txtAutoCompleteTextBox.DataSource = filter
 
         If s = String.Empty Then
                   filter.Clear()
                   txtAutoCompleteTextBox.DataSource = filter
         End If
End Sub
The above code calls the ‘ListData()’ method and based upon the text entered in the textbox, the LINQ query retrieves the matched string from the List<string> ‘lstData’.
Step 9: Run the application and the following result will be displayed:
Result
Note: This user control is just a demonstration of how auto-complete can be implemented in your custom control. The control has to be perfected in terms of UI and functionality. So feel free to do so.
The entire source code of this article can be downloaded over here
Give a +1 to this article if you think it was well written. Thanks!
Recommended Articles
Mahesh is having 10 years of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions). Follow him on twitter @maheshdotnet


Page copy protected against web site content infringement by Copyscape


User Feedback
Comment posted by Stefan on Tuesday, January 4, 2011 3:22 AM
This is a nice one, but what about keyboard handling? like key up or key down and scrolling the list?
Comment posted by RĂ©mi Desbiens on Friday, January 21, 2011 10:12 AM
I would like convert code c# in VB NET, Merci beaucoup (thank'you)

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace Wpf.Util
{
    public class GridViewSort
    {
        #region Attached properties

        public static ICommand GetCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(CommandProperty);
        }

        public static void SetCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(CommandProperty, value);
        }

        // Using a DependencyProperty as the backing store for Command.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached(
                "Command",
                typeof(ICommand),
                typeof(GridViewSort),
                new UIPropertyMetadata(
                    null,
                    (o, e) =>
                    {
                        ItemsControl listView = o as ItemsControl;
                        if (listView != null)
                        {
                            if (!GetAutoSort(listView)) // Don't change click handler if AutoSort enabled
                            {
                                if (e.OldValue != null && e.NewValue == null)
                                {
                                    listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                                if (e.OldValue == null && e.NewValue != null)
                                {
                                    listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                            }
                        }
                    }
                )
            );

        public static bool GetAutoSort(DependencyObject obj)
        {
            return (bool)obj.GetValue(AutoSortProperty);
        }

        public static void SetAutoSort(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoSortProperty, value);
        }

        // Using a DependencyProperty as the backing store for AutoSort.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoSortProperty =
            DependencyProperty.RegisterAttached(
                "AutoSort",
                typeof(bool),
                typeof(GridViewSort),
                new UIPropertyMetadata(
                    false,
                    (o, e) =>
                    {
                        ListView listView = o as ListView;
                        if (listView != null)
                        {
                            if (GetCommand(listView) == null) // Don't change click handler if a command is set
                            {
                                bool oldValue = (bool)e.OldValue;
                                bool newValue = (bool)e.NewValue;
                                if (oldValue && !newValue)
                                {
                                    listView.RemoveHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                                if (!oldValue && newValue)
                                {
                                    listView.AddHandler(GridViewColumnHeader.ClickEvent, new RoutedEventHandler(ColumnHeader_Click));
                                }
                            }
                        }
                    }
                )
            );

        public static string GetPropertyName(DependencyObject obj)
        {
            return (string)obj.GetValue(PropertyNameProperty);
        }

        public static void SetPropertyName(DependencyObject obj, string value)
        {
            obj.SetValue(PropertyNameProperty, value);
        }

        // Using a DependencyProperty as the backing store for PropertyName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.RegisterAttached(
                "PropertyName",
                typeof(string),
                typeof(GridViewSort),
                new UIPropertyMetadata(null)
            );

        #endregion

        #region Column header click event handler

        private static void ColumnHeader_Click(object sender, RoutedEventArgs e)
        {
            GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
            if (headerClicked != null)
            {
                string propertyName = GetPropertyName(headerClicked.Column);
                if (!string.IsNullOrEmpty(propertyName))
                {
                    ListView listView = GetAncestor<ListView>(headerClicked);
                    if (listView != null)
                    {
                        ICommand command = GetCommand(listView);
                        if (command != null)
                        {
                            if (command.CanExecute(propertyName))
                            {
                                command.Execute(propertyName);
                            }
                        }
                        else if (GetAutoSort(listView))
                        {
                            ApplySort(listView.Items, propertyName);
                        }
                    }
                }
            }
        }

        #endregion

        #region Helper methods

        public static T GetAncestor<T>(DependencyObject reference) where T : DependencyObject
        {
            DependencyObject parent = VisualTreeHelper.GetParent(reference);
            while (!(parent is T))
            {
                parent = VisualTreeHelper.GetParent(parent);
            }
            if (parent != null)
                return (T)parent;
            else
                return null;
        }

        public static void ApplySort(ICollectionView view, string propertyName)
        {
            ListSortDirection direction = ListSortDirection.Ascending;
            if (view.SortDescriptions.Count > 0)
            {
                SortDescription currentSort = view.SortDescriptions[0];
                if (currentSort.PropertyName == propertyName)
                {
                    if (currentSort.Direction == ListSortDirection.Ascending)
                        direction = ListSortDirection.Descending;
                    else
                        direction = ListSortDirection.Ascending;
                }
                view.SortDescriptions.Clear();
            }
            if (!string.IsNullOrEmpty(propertyName))
            {
                view.SortDescriptions.Add(new SortDescription(propertyName, direction));
            }
        }

        #endregion
    }
}
Comment posted by jeremiah on Tuesday, February 22, 2011 10:31 AM
Thanks, I was looking for a very simple example of this functionality as a jumping off point for my own control. Your tutorial provided that.
Comment posted by Kostadin Mitev on Sunday, November 27, 2011 2:11 PM
Hi,
Thanks for sharing your code. It is a really simple way of implementing AutoSuggest control. It really puts somebody who wants to implement a control like this himself/herself on the right track. I was one of those guys several months ago and after I read several articles like this I though that it is an easy task. It turned out to be a full time job for a whole month.
We have shared our code here: http://code.google.com/p/kocontrols/downloads/list. Just wanted to show an alternative solution that we have and I will be happy to hear your feedback.

Regards,
Kos

Post your comment
Name:  
E-mail: (Will not be displayed)
Comment:
Insert Cancel