I need an autocomplete combobox for WPF C#. I've tried several approaches but nothing works. For example I've tried a combobox:
<ComboBox Width="200"
IsEditable="True"
ItemsSource="{Binding Names}"
IsTextSearchEnabled="True"
HorizontalAlignment="Left"/>
Names
is a List of Strings: Peter John, John, John Doe, Cathy, Howard, John Richards and so on
If you type in a name e.g. John the combobox should expand and I should see
But that doesn't work. How can I do that?
After a lot of fiddling, I have managed to arrive at a complete, working solution. (Or so it seems.)
You need to modify your ComboBox like so:
<ComboBox
...
IsTextSearchEnabled="False"
...
PreviewTextInput="PreviewTextInput_EnhanceComboSearch"
PreviewKeyUp="PreviewKeyUp_EnhanceComboSearch"
DataObject.Pasting="Pasting_EnhanceComboSearch" />
ie. to disable default text search, and add events handlers that will take care of user adding, deleting and pasting text.
In order for PreviewTextInput_EnhanceComboSearch
and Pasting_EnhanceComboSearch
to work at all, you will need to access your ComboBox's caret. Unfortunately, to do this, you need to traverse, er, visual tree (hat tip to Matt Hamilton). You can do that in an extension method, but I used a static one in my Page
class:
public static T GetChildOfType<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
Please note I used
s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1
which is equivalent to case-insensitive s => s.Contains(e.Text)
check. Remember to change that part to suit your needs.
When a PreviewTextInput
handler is run, the .Text
property inside the ComboBox contains the text from before it was modified. Therefore, we need to get ComboBox's internal TextBox using GetChildOfType
method in order to obtain its caret, so we know where exactly was the typed character inserted.
private void PreviewTextInput_EnhanceComboSearch(object sender, TextCompositionEventArgs e)
{
ComboBox cmb = (ComboBox)sender;
cmb.IsDropDownOpen = true;
if (!string.IsNullOrEmpty(cmb.Text))
{
string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, e.Text);
cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
}
else if (!string.IsNullOrEmpty(e.Text))
{
cmb.ItemsSource = Names.Where(s => s.IndexOf(e.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
}
else
{
cmb.ItemsSource = Names;
}
}
DataObject.Pasting
handler behaves in a similar fashion to PreviewTextInput
hanlder, so we need the caret again.
private void Pasting_EnhanceComboSearch(object sender, DataObjectPastingEventArgs e)
{
ComboBox cmb = (ComboBox)sender;
cmb.IsDropDownOpen = true;
string pastedText = (string)e.DataObject.GetData(typeof(string));
string fullText = cmb.Text.Insert(GetChildOfType<TextBox>(cmb).CaretIndex, pastedText);
if (!string.IsNullOrEmpty(fullText))
{
cmb.ItemsSource = Names.Where(s => s.IndexOf(fullText, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
}
else
{
cmb.ItemsSource = Names;
}
}
This will trigger when the user depresses either Delete or Backspace.
And also Space, because Space is ignored by PreviewTextInput
, so it would be difficult to filter out "John" from "John Doe" and "John Richards" in the example.
private void PreviewKeyUp_EnhanceComboSearch(object sender, KeyEventArgs e)
{
if (e.Key == Key.Back || e.Key == Key.Delete)
{
ComboBox cmb = (ComboBox)sender;
cmb.IsDropDownOpen = true;
if (!string.IsNullOrEmpty(cmb.Text))
{
cmb.ItemsSource = Names.Where(s => s.IndexOf(cmb.Text, StringComparison.InvariantCultureIgnoreCase) != -1).ToList();
}
else
{
cmb.ItemsSource = Names;
}
}
}
...and that should probably be enough.
use PreviewTextInput event for do filter and show drop down like this:
private void ComboBox_TextInput_1(object sender, TextCompositionEventArgs e)
{
cmbperson.IsDropDownOpen = true;
cmbperson.ItemsSource = DataBase.Persons.Where(p => p.Name.Contains(e.Text)).ToList();
}
I suggest you use a control made for auto complete instead of a combobox. Many companies offer such controls, this one is free and considered good.
I created an autocomplete to WPF
that could help you.
Follow the link below to github:
https://github.com/rhpontes/AutocompleteWpf
I hope it helps you.
Here is the implementation that works for me:
<ComboBox
Name="ItemsControl"
IsEditable="True"
KeyUp="OnItemsControlKeyUp"
I check if the text has changed since the last time the filter was applied (to avoid filtering when a non-alphanumeric key is pressed).
private string _textBeforeFilter;
private void OnItemsControlKeyUp(object sender, KeyEventArgs e)
{
var arrowKey = e.Key >= Key.Left && e.Key <= Key.Down;
// if arrow key (navigating) or the text hasn't changed, then a we don't need to filter
if (arrowKey || ItemsControl.Text.EqualsIgnoreCase(_textBeforeFilter)) return;
_textBeforeFilter = ItemsControl.Text;
var textIsEmpty = string.IsNullOrWhiteSpace(ItemsControl.Text);
var itemsViewOriginal = (CollectionView) CollectionViewSource.GetDefaultView(ItemsControl.ItemsSource);
// if the text is empty, then we show everything, otherwise filter based on the text
itemsViewOriginal.Filter = o => textIsEmpty || ((string) o).ContainsIgnoreCase(ItemsControl.Text);
}
NOTE: EqualsIgnoreCase
and ContainsIgnoreCase
are extension methods:
public static bool EqualsIgnoreCase(this string source, string value)
{
return source.Equals(value, StringComparison.OrdinalIgnoreCase);
}
public static bool ContainsIgnoreCase(this string source, string value)
{
return source.Contains(value, StringComparison.OrdinalIgnoreCase);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With