Let's say we want to populate a ComboBox with some titles. At design time, the source code looks like this:
List<String> names = new List<string>();
names.Add("WPF rocks"); names.Add("WCF rocks"); names.Add("XAML is fun"); names.Add("WPF rules"); names.Add("WCF rules"); names.Add("WinForms not"); FilteredComboBox1.IsEditable = true; FilteredComboBox1.IsTextSearchEnabled = false;FilteredComboBox1.ItemsSource = names;
At run time, the editable combo box will apply filtering if the text in the editable textbox passes a treshold (e.g. 3 characters):

The FilteredComboBox class looks like this, largely decorated with comments:
//-----------------------------------------------------------------------// <copyright file="FilteredComboBox.cs" company="DockOfTheBay">// http://www.dotbay.be// </copyright>// <summary>Defines the FilteredComboBox class.</summary>//-----------------------------------------------------------------------namespace DockOfTheBay{ using System.Collections; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input;/// <summary>
/// Editable combo box which uses the text in its editable textbox to perform a lookup
/// in its data source.
/// </summary>
public class FilteredComboBox : ComboBox
{ //// // Public Fields /////// <summary>
/// The search string treshold length.
/// </summary>
/// <remarks>
/// It's implemented as a Dependency Property, so you can set it in a XAML template
/// </remarks>
public static readonly DependencyProperty MinimumSearchLengthProperty =
DependencyProperty.Register( "MinimumSearchLength",typeof(int),
typeof(FilteredComboBox),
new UIPropertyMetadata(3));
//// // Private Fields //// /// <summary>
/// Caches the previous value of the filter.
/// </summary>
private string oldFilter = string.Empty;
/// <summary>
/// Holds the current value of the filter.
/// </summary>
private string currentFilter = string.Empty;
//// // Constructors //// /// <summary>
/// Initializes a new instance of the FilteredComboBox class.
/// </summary>
/// <remarks>
/// You could set 'IsTextSearchEnabled' to 'false' here,
/// to avoid non-intuitive behavior of the control
/// </remarks>
public FilteredComboBox() {}
//// // Properties //// /// <summary>
/// Gets or sets the search string treshold length.
/// </summary>
/// <value>The minimum length of the search string that triggers filtering.</value>
[Description("Length of the search string that triggers filtering.")]
[Category("Filtered ComboBox")]
[DefaultValue(3)]public int MinimumSearchLength
{ [System.Diagnostics.DebuggerStepThrough] get {return (int)this.GetValue(MinimumSearchLengthProperty);
}
[System.Diagnostics.DebuggerStepThrough] set {this.SetValue(MinimumSearchLengthProperty, value);
}
}
/// <summary>
/// Gets a reference to the internal editable textbox.
/// </summary>
/// <value>A reference to the internal editable textbox.</value>
/// <remarks>
/// We need this to get access to the Selection.
/// </remarks>
protected TextBox EditableTextBox
{ get {return this.GetTemplateChild("PART_EditableTextBox") as TextBox;
}
}
//// // Event Raiser Overrides //// /// <summary>
/// Keep the filter if the ItemsSource is explicitly changed.
/// </summary>
/// <param name="oldValue">The previous value of the filter.</param>
/// <param name="newValue">The current value of the filter.</param>
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{if (newValue != null)
{ICollectionView view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += this.FilterPredicate;}
if (oldValue != null)
{ICollectionView view = CollectionViewSource.GetDefaultView(oldValue);
view.Filter -= this.FilterPredicate;}
base.OnItemsSourceChanged(oldValue, newValue);}
/// <summary>
/// Confirm or cancel the selection when Tab, Enter, or Escape are hit.
/// Open the DropDown when the Down Arrow is hit.
/// </summary>
/// <param name="e">Key Event Args.</param>
/// <remarks>
/// The 'KeyDown' event is not raised for Arrows, Tab and Enter keys.
/// It is swallowed by the DropDown if it's open.
/// So use the Preview instead.
/// </remarks>
protected override void OnPreviewKeyDown(KeyEventArgs e)
{if (e.Key == Key.Tab || e.Key == Key.Enter)
{ // Explicit Selection -> Close ItemsPanelthis.IsDropDownOpen = false;
}
else if (e.Key == Key.Escape)
{ // Escape -> Close DropDown and redisplay Filterthis.IsDropDownOpen = false;
this.SelectedIndex = -1;this.Text = this.currentFilter;
}
else {if (e.Key == Key.Down)
{ // Arrow Down -> Open DropDownthis.IsDropDownOpen = true;
}
base.OnPreviewKeyDown(e);}
// Cache textthis.oldFilter = this.Text;
}
/// <summary>
/// Modify and apply the filter.
/// </summary>
/// <param name="e">Key Event Args.</param>
/// <remarks>
/// Alternatively, you could react on 'OnTextChanged', but navigating through
/// the DropDown will also change the text.
/// </remarks>
protected override void OnKeyUp(KeyEventArgs e)
{if (e.Key == Key.Up || e.Key == Key.Down)
{ // Navigation keys are ignored}
else if (e.Key == Key.Tab || e.Key == Key.Enter)
{ // Explicit Select -> Clear Filter this.ClearFilter();}
else { // The text was changedif (this.Text != this.oldFilter)
{ // Clear the filter if the text is empty, // apply the filter if the text is long enoughif (this.Text.Length == 0 || this.Text.Length >= this.MinimumSearchLength)
{ this.RefreshFilter();this.IsDropDownOpen = true;
// Unselectthis.EditableTextBox.SelectionStart = int.MaxValue;
}
}
base.OnKeyUp(e); // Update Filter Valuethis.currentFilter = this.Text;
}
}
/// <summary>
/// Make sure the text corresponds to the selection when leaving the control.
/// </summary>
/// <param name="e">A KeyBoardFocusChangedEventArgs.</param>
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{ this.ClearFilter();int temp = this.SelectedIndex;
this.SelectedIndex = -1;this.Text = string.Empty;
this.SelectedIndex = temp; base.OnPreviewLostKeyboardFocus(e);}
//// // Helpers /////// <summary>
/// Re-apply the Filter.
/// </summary>
private void RefreshFilter()
{if (this.ItemsSource != null)
{ICollectionView view = CollectionViewSource.GetDefaultView(this.ItemsSource);
view.Refresh();
}
}
/// <summary>
/// Clear the Filter.
/// </summary>
private void ClearFilter()
{this.currentFilter = string.Empty;
this.RefreshFilter();}
/// <summary>
/// The Filter predicate that will be applied to each row in the ItemsSource.
/// </summary>
/// <param name="value">A row in the ItemsSource.</param>
/// <returns>Whether or not the item will appear in the DropDown.</returns>
private bool FilterPredicate(object value)
{ // No filter, no textif (value == null)
{return false;
}
// No text, no filterif (this.Text.Length == 0)
{return true;
}
// Case insensitive searchreturn value.ToString().ToLower().Contains(this.Text.ToLower());
}
}
}
As far as I know, the control is not allergic to templating nor databinding, even with large (2000+ items) datasources. For large datasources it makes sense to display the DropDown via a virtualization panel, like in the following XAML:
<Window x:Class="FilteredComboBoxSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DockOfTheBay"
Title="Filtered ComboBox" Height="120" Width="300">
<Window.Resources>
<!-- For large content, better go for a Virtualizing StackPanel -->
<ItemsPanelTemplate x:Key="ItemsTemplate">
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</Window.Resources>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Top"
Margin="10 10">
<TextBlock Text="Select: "
Padding="4 3"/>
<local:FilteredComboBox
x:Name="FilteredComboBox1"
ItemsPanel="{DynamicResource ItemsTemplate}"
Padding="4 3"
MinWidth="200"/>
</StackPanel>
</Window>
Here's how the control looks like in a real life application, filtering a list of over 2000 possible tumor locations on a human body: