Reputation: 4577
I have a WPF combobox
<ComboBox BorderThickness="0" Name="cmb_songs_head" HorizontalAlignment="Right" SelectedItem="{Binding Path=T.SelectedSong, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding Path=T.SelectedSet.Songs, UpdateSourceTrigger=PropertyChanged}" />
When the combobox is selected and I type, it selects from the dropdown - which is what I want. e.g:
Hall
Hold
Hollow
Lead
So When I type H
the first item is selected, Ho
selects the second item, Holl
selects the third.
But, users of my program complain that they are often too slow in typing and so they end up typing Hol
which selects Hold
, followed by l
, which selects Lead
; instead of seeing it as one input for Hollow
.
Is there any way to extend the timeout between words?
Upvotes: 0
Views: 679
Reputation: 29018
You can set Binding.Delay
, when binding the ComboBox.SelectedItem
.
The following example sets the delay of the binding to 1500ms. Every change of the binding target or source that occurred before the delay has elapsed, will reset the delay timer:
<ComboBox Name="cmb_songs_head"
StaysOpenOnEdit="True"
IsEditable="True"
SelectedItem="{Binding T.SelectedSong, Delay=1500}"
ItemsSource="{Binding T.SelectedSet.Songs}" />
The bindings can be simplified to enhance readability:
ComboBox.SelectedItem
binds TwoWay
by default.
UpdateSourceTrigger.PropertyChanged
is the default trigger for properties of ItemsControl
.
That's the default search behavior. By typing, the matching item is searched and the match is actually selected.
Since the match is immediately assigned to ComboBox.SelectedItem
, it could have unwanted side effects to select something not matching. Especially when the selection triggers an operation.
If you want to auto-select the closest match or make suggestions, I recommend to use collection filtering instead.
I would write an Attached Behavior, which listens to the ComboBox.PreviewTextInput
and the TextBoxBase.PreviewKeyUp
events to handle the filtering.
The following example handles this events in code-behind instead and assumes that the ComboBox
item type is string
. The Binding.Delay
is set to 5s:
View
<ComboBox ItemsSource="{Binding T.SelectedSet.Songs}"
SelectedItem="{Binding T.SelectedSong, Delay=5000}"
StaysOpenOnEdit="True"
IsEditable="True"
TextBoxBase.PreviewKeyUp="EditTextBox_OnPreviewKeyUp"
PreviewTextInput="ComboBox_OnPreviewTextInput" />
Code-behind
private void EditTextBox _OnPreviewKeyUp(object sender, KeyEventArgs e)
{
var editTextBox = e.OriginalSource as TextBox;
var comboBox = sender as ComboBox;
switch (e.Key)
{
case Key.Back:
{
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
int selectionStart = comboBox.SelectedItem == null
? editTextBox.CaretIndex
: Math.Max(0, editTextBox.SelectionStart - 1);
int selectionLength = comboBox.SelectedItem == null
? 0
: editTextBox.Text.Length - selectionStart;
editTextBox.Select(selectionStart, selectionLength);
break;
}
case Key.Space:
{
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
break;
}
case Key.Delete:
{
int currentCaretIndex = editTextBox.CaretIndex;
MainWindow.FilterComboBoxItemsSource(sender as ComboBox, editTextBox.Text, editTextBox);
editTextBox.CaretIndex = currentCaretIndex;
break;
}
}
}
private void ComboBox_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = true;
var editTextBox = e.OriginalSource as TextBox;
string oldText = editTextBox.Text.Substring(0, editTextBox.SelectionStart);
string newText = oldText + e.Text;
FilterComboBoxItemsSource(sender as ComboBox, newText, editTextBox);
}
private void FilterComboBoxItemsSource(ComboBox comboBox, string predicateText, TextBox editTextBox)
{
ICollectionView collectionView = CollectionViewSource.GetDefaultView(comboBox.ItemsSource);
if (!string.IsNullOrWhiteSpace(predicateText)
&& !collectionView.SourceCollection
.Cast<string>()
.Any(item => item.StartsWith(predicateText, StringComparison.OrdinalIgnoreCase)))
{
int oldCaretIndex = editTextBox.CaretIndex == editTextBox.Text.Length
? predicateText.Length
: editTextBox.CaretIndex;
editTextBox.Text = predicateText;
editTextBox.CaretIndex = oldCaretIndex;
return;
}
collectionView.Filter = item => (item as string).StartsWith(string.IsNullOrWhiteSpace(predicateText)
? string.Empty
: predicateText, StringComparison.OrdinalIgnoreCase);
collectionView.MoveCurrentToFirst();
editTextBox.Text = collectionView.CurrentItem as string;
editTextBox.Select(predicateText.Length, editTextBox.Text.Length - predicateText.Length);
}
Upvotes: 1