Search code examples
wpfinputcombobox

WPF Combobox search typing speed


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?


Solution

  • 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}" />
    

    Remarks

    The bindings can be simplified to enhance readability:
    ComboBox.SelectedItem binds TwoWay by default.
    UpdateSourceTrigger.PropertyChanged is the default trigger for properties of ItemsControl.


    Update

    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);
    }