Search code examples
c#wpfmvvmcombobox

C# WPF ComboBox Selection Problem with Arrow Keys


I making a UserControl in my C# WPF MVVM application. When I type in the ComboBox Filtered products show, lets say 8 filtered products are showing in dropdown list, problem start from here if i try to navigate between these filtered products by arrow key it automaticly select first item in the list. view code

<UserControl ...>
    <Grid>
        <ComboBox Width="300" Height="25"
                          ItemsSource="{Binding FilteredProducts}"
                          SelectedItem="{Binding SelectedProduct}"
                          Text="{Binding SearchText ,Mode=TwoWay}" 
                          DisplayMemberPath="DisplayName"
                          IsTextSearchEnabled="True"
                          IsTextSearchCaseSensitive="False"
                          IsEditable="True">
            <ComboBox.Style>
                <Style TargetType="ComboBox">
                    <Style.Triggers>
                        <Trigger Property="IsKeyboardFocusWithin" Value="True">
                            <Setter Property="IsDropDownOpen" Value="True" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </ComboBox.Style>
        </ComboBox>
    </Grid>
</UserControl>

ViewModel Code

   public TestDropDownViewModel()
   {
        ProductList = new List<Products>...;
        FilteredProducts = ProductList;
    }
    [ObservableProperty]
    private List<Products> _productList;
    [ObservableProperty]
    private Products _selectedProduct;

    private string _searchText = "";
    public string SearchText
    {
        get { return _searchText; }
        set
        {
            _searchText = value;
            OnPropertyChanged();
            FilteredProducts = ProductList
                .Where(product => product.DisplayName.Contains(SearchText, StringComparison.OrdinalIgnoreCase))
                .ToList();
        }
    }

    private List<Products> _filteredProducts = new List<Products>();
    public List<Products> FilteredProducts
    {
        get { return _filteredProducts; }
        set
        {
            _filteredProducts = value;
            OnPropertyChanged();
        }
    }

and Model code

public partial class Products : ObservableObject
{
    [ObservableProperty]
    private string _code = "";
    [ObservableProperty]
    private string _title = "";
    public string DisplayName => Code + " - " + Title;
}

filtered products list shows as i type as i press arrow key down this happen I want that if i type something and list shows with some items i should be able to select by arrow up or down keys from this list. just like when we search in Google.com


Solution

  • It's not tha simple as you think it is. You are currently using a change of the ComboBox.Text property value to trigger the items filter. Now navigating with the arrow keys will also select items and also change the value of the ComboBox.Text property to show the selected item, which again triggers your filter (you don't want this last filter trigger), which removes all search results. Unless you have duplicate entries that satisfy the filter based on the selected item you can only select a single item using the arrow keys.

    You certainly don't want to clear the search/filter results.

    The solution is to suspend filtering when navigation keys are pressed and resume on every other key.

    You should also implement collection filtzering properly. Your current implementation is very expensive and will have a significant impact on the perfromance.

    First, use an ObservableCollection (or any other collection that implements INotifyCollectionChanged) when binding to a collection. Then, don't replace this collection (the instance) to update it. Replacing the collection is expensive as the ItemsControl has to perform a complete layout pass to render itself. Instead, clear/remove old items and add/insert new items. Now the ItemsControl only has to update the changed items and not all items.

    Next, you should always filter collection using the default ICollectionView of the source collection or CollectionViewSource.

    To suspend your filtering, you should set the Binding.UpdateSourceTrigger of the ComboBox.Text binding to UpdateSourceTrigger.Explicit.

    In case you still experience issues with the cursor key navigation you can handle selection yourself. In this case I recommend setting ComboBox.IsSynchronizedWithCurrentItem to true and then navigate the items using the collection view.

    An improved version of your code and a solution to your original problem could look as follows:

    TestDropDownViewModel.cs

    class TestDropDownViewModel : ObservableObject
    {
      public TestDropDownViewModel()
      {
        ProductList = new ObservableCollection<Products>...;
      }
    
      [ObservableProperty]
      // Use an ObservableCollection to improve rendering performance
      private ObservableCollection<Products> _productList;
    
      [ObservableProperty]
      private Products _selectedProduct;
    
      private string _searchText = "";
      public string SearchText
      {
        get => _searchText; 
        set
        {
          _searchText = value;
          OnPropertyChanged();
          FilterProducts();
        }
      }
    
      // Filtering using the ICollectionView to improve rendering performance
      private void FilterProducts()
      {
        var defaultCollectionView = CollectionViewSource.GetDefaultView(this.ProductList);
        defaultCollectionView.Filter = 
          item => ((Products)item).DisplayName.Contains(this.SearchText, StringComparison.OrdinalIgnoreCase);
      }
    }
    

    MainWindow.xaml

    <ComboBox ItemsSource="{Binding ProductList}"
              Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=Explicit}" 
              PreviewKeyUp="ComboBox_PreviewKeyUp"
              IsSynchronizedWithCurrentItem="True"
              ... >
    </ComboBox>
    

    MainWindow.xaml.cs

    partial class MainWindow : Window
    {
      public MainWindow()
      {
        InitializeComponent();
    
        // Related code is optional in case you experience cursor key navigation issues. 
        // For example, when you are not able to select items using the arrow down key 
        // while in the text box of the ComboBox then use this event handling code.
        //
        // Because the ComboBox marks cursor key related instance events 
        // as handled internally we must handle these events on class level.
        EventManager.RegisterClassHandler(typeof(ComboBox), PreviewKeyDownEvent, new KeyEventHandler(OnComboBoxPreviewKeyDown));
      }
    
      // Mandatory: use to prevent cursor key navigation from triggering the filter
      private void ComboBox_PreviewKeyUp(object sender, KeyEventArgs e)
      {
        var comboBox = (ComboBox)sender;
        if (e.Key is not Key.Down 
          and not Key.Up 
          and not Key.Left 
          and not Key.Right)
        {
          comboBox.GetBindingExpression(ComboBox.TextProperty).UpdateSource();
        }
      }
    
      // Optional: manually handle cursor kkey selection in case the ComboBox 
      // has issues (introduced by the source collection filtering)
      private void OnPreviewKeyDown(object sender, KeyEventArgs e)
      {
        var comboBox = (ComboBox)sender;
        switch (e.Key)
        {
          case Key.Down:
            _ = comboBox.Items.MoveCurrentToNext();
            e.Handled = true;
            break;
          case Key.Up:
            _ = comboBox.Items.MoveCurrentToPrevious();
            e.Handled = true;
            break;
        }
      }
    }