Introduction
The WinForms ComboBox-Control offers a functionality called AutoComplete
if
DropDownStyle
is set to
DropDown
. There are different options available, but the focus of this article is on
AutoCompleteMode = Suggest
with
AutoCompleteSource = ListItems
. This setting provides a ListBox of suggested items when you type some text in the ComboBox.
The problem is: you cannot define the way the suggested items are filtered/determined. It's always a 'StartsWith'-search.
That's why i decided to write my own class (SuggestComboBox) based on ComboBox which looks more or less the same, but is in fact self-made.
The screenshot below shows the difference: on the left hand side the input 'j' matches one more item because it's a Contains-search.
The code and how it works
At first you need a new class that inherits from ComboBox and a ListBox + BindingList as DataSource that will contain the suggested items.
The filtering is done in the OnTextChanged
- method via LINQ, the Lamda-Expressions for filtering and ordering can be set by the Properties
FilterRule
and SuggestListOrderRule
(default values are a Contains-filter and ascending alphabetic order).
To support the expected behavior for keyboard actions (UP, DOWN, ENTER, ESCAPE) you primarily need to override two methods:
OnPreviewKeyDown
, that is called first when you hit a key. Here the actions to be done are listed in a switch-statement for the KeyCode.ProcessCmdKey
, that is always called after the first one. Here only the base functionality is interrupted.
The remaining methods are mainly to manage the visibility and location/size of the ListBox and need no further explanation. Just read the code! (examples are included in the zip-file)
Update:
To support any DataSource-Type (not just string), i added the PropertySelector
: you can pass the selection rule for the databound items. The default setting still assumes that a list of strings is the datasource.
Example:
suggestComboBox.DataSource = new List<person>();
suggestComboBox.DisplayMember = "Name";
suggestComboBox.PropertySelector = collection => collection.Cast<person>().Select(p => p.Name);
class Person
{
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public int Height { get; set; }
}</person></person>
I also fixed some UI-bugs regarding the location and size of the suggest box (2 Changed-EventHandler added).
public class SuggestComboBox : ComboBox
{
#region fields and properties
private readonly ListBox _suggLb = new ListBox { Visible = false, TabStop = false };
private readonly BindingList<string> _suggBindingList = new BindingList<string>();
private Expression<Func<ObjectCollection, IEnumerable<string>>> _propertySelector;
private Func<ObjectCollection, IEnumerable<string>> _propertySelectorCompiled;
private Expression<Func<string, string, bool>> _filterRule;
private Func<string, bool> _filterRuleCompiled;
private Expression<Func<string, string>> _suggestListOrderRule;
private Func<string, string> _suggestListOrderRuleCompiled;
public int SuggestBoxHeight
{
get { return _suggLb.Height; }
set { if (value > 0) _suggLb.Height = value; }
}
public Expression<Func<ObjectCollection, IEnumerable<string>>> PropertySelector
{
get { return _propertySelector; }
set
{
if (value == null) return;
_propertySelector = value;
_propertySelectorCompiled = value.Compile();
}
}
public Expression<Func<string, string, bool>> FilterRule
{
get { return _filterRule; }
set
{
if (value == null) return;
_filterRule = value;
_filterRuleCompiled = item => value.Compile()(item, Text);
}
}
public Expression<Func<string, string>> SuggestListOrderRule
{
get { return _suggestListOrderRule; }
set
{
if (value == null) return;
_suggestListOrderRule = value;
_suggestListOrderRuleCompiled = value.Compile();
}
}
#endregion
public SuggestComboBox()
{
_filterRuleCompiled = s => s.ToLower().Contains(Text.Trim().ToLower());
_suggestListOrderRuleCompiled = s => s;
_propertySelectorCompiled = collection => collection.Cast<string>();
_suggLb.DataSource = _suggBindingList;
_suggLb.Click += SuggLbOnClick;
ParentChanged += OnParentChanged;
}
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
if (!Focused) return;
_suggBindingList.Clear();
_suggBindingList.RaiseListChangedEvents = false;
_propertySelectorCompiled(Items)
.Where(_filterRuleCompiled)
.OrderBy(_suggestListOrderRuleCompiled)
.ToList()
.ForEach(_suggBindingList.Add);
_suggBindingList.RaiseListChangedEvents = true;
_suggBindingList.ResetBindings();
_suggLb.Visible = _suggBindingList.Any();
if (_suggBindingList.Count == 1 &&
_suggBindingList.Single().Length == Text.Trim().Length)
{
Text = _suggBindingList.Single();
Select(0, Text.Length);
_suggLb.Visible = false;
}
}
private void OnParentChanged(object sender, EventArgs e)
{
Parent.Controls.Add(_suggLb);
Parent.Controls.SetChildIndex(_suggLb, 0);
_suggLb.Top = Top + Height - 3;
_suggLb.Left = Left + 3;
_suggLb.Width = Width - 20;
_suggLb.Font = new Font("Segoe UI", 9);
}
protected override void OnLostFocus(EventArgs e)
{
if (!_suggLb.Focused)
HideSuggBox();
base.OnLostFocus(e);
}
protected override void OnLocationChanged(EventArgs e)
{
base.OnLocationChanged(e);
_suggLb.Top = Top + Height - 3;
_suggLb.Left = Left + 3;
}
protected override void OnSizeChanged(EventArgs e)
{
base.OnSizeChanged(e);
_suggLb.Width = Width - 20;
}
private void SuggLbOnClick(object sender, EventArgs eventArgs)
{
Text = _suggLb.Text;
Focus();
}
private void HideSuggBox()
{
_suggLb.Visible = false;
}
protected override void OnDropDown(EventArgs e)
{
HideSuggBox();
base.OnDropDown(e);
}
#region keystroke events
protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
{
if (!_suggLb.Visible)
{
base.OnPreviewKeyDown(e);
return;
}
switch (e.KeyCode)
{
case Keys.Down:
if (_suggLb.SelectedIndex < _suggBindingList.Count - 1)
_suggLb.SelectedIndex++;
return;
case Keys.Up:
if (_suggLb.SelectedIndex > 0)
_suggLb.SelectedIndex--;
return;
case Keys.Enter:
Text = _suggLb.Text;
Select(0, Text.Length);
_suggLb.Visible = false;
return;
case Keys.Escape:
HideSuggBox();
return;
}
base.OnPreviewKeyDown(e);
}
private static readonly Keys[] KeysToHandle = new[]
{ Keys.Down, Keys.Up, Keys.Enter, Keys.Escape };
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (_suggLb.Visible && KeysToHandle.Contains(keyData))
return true;
return base.ProcessCmdKey(ref msg, keyData);
}
#endregion
}
Remark
One thing is really annoying i think: every time you hit ENTER or ESCAPE a 'bing'-sound is generated by the framework. I read this is a bug of .NET 4. Anybody has an idea how to fix it properly?