最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

xaml - WPF Custom ComboBox with filtering becomes unresponsive when deleting characters - Stack Overflow

programmeradmin1浏览0评论

Scenario:

I'm implementing a custom ComboBox in WPF with a filtering feature:

  • The ComboBox contains a Popup with a TextBox for filtering.
  • Below the TextBox, a list displays only the items that match the filter text.
  • An item remains visible if its text contains the filter text.
  • Items that don’t match are hidden using Visibility.Collapsed, which is applied via ItemContainerStyle and a converter.

Find the full implementation on the bottom.

Issue:

When the total number of items (before filtering) is below 140, everything works correctly:

  • Typing in the TextBox filters the list dynamically.
  • Deleting characters behaves as expected.

However, when the total number of items exceeds 150, strange behavior occurs when deleting characters from the TextBox:

  • If I type "1a", filtering works properly.
  • Then, if I delete only the "a", the TextBox becomes unresponsive:
    • The cursor does not reset to its expected position - in the screenshot below the cursor's position should be one character further to the left, but it remains in the wrong place after deleting a character.
    • Left/right arrow keyboard navigation stops working.
    • If I then delete "1" the cursor resets properly and everything works again.

Questions:

My requirement is that the TextBox to behave as expected in all cases. I find this cursor behavior very unexpected and unusual.

  • What is causing this behavior?
  • How can I fix this behavior with the following restrictions:
    • The solution should not involve virtualizing the ComboBox — so I cannot use VirtualizingStackPanel due to limitations in my actual use case.
    • The solution should not include any code-behind changes - must be XAML-only.
<Window x:Class="FilterableComboBox.MainWindow"
        xmlns=";
        xmlns:x=";
        xmlns:local="clr-namespace:FilterableComboBox"
        Title="MainWindow"
        Height="100"
        Width="400">
    <Window.Resources>
        <local:StringContainsConverter x:Key="StringContainsConverter" />
    </Window.Resources>

    <Grid>
        <ComboBox x:Name="FilterableComboBox"
                  HorizontalAlignment="Stretch"
                  Height="25"
                  Width="300"
                  ItemsSource="{Binding CustomValues}">

            <ComboBox.ItemContainerStyle>
                <Style TargetType="ComboBoxItem">
                    <Setter Property="Visibility">
                        <Setter.Value>
                            <MultiBinding Converter="{StaticResource StringContainsConverter}">
                                <Binding  />
                                <Binding ElementName="FilterTextBox" Path="Text" />
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ComboBox.ItemContainerStyle>
            
            <ComboBox.Template>
                <ControlTemplate TargetType="ComboBox">
                    <Grid>
                        <ToggleButton Content="{TemplateBinding SelectedItem}"
                                      IsChecked="{Binding Path=IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" />
                        <Popup x:Name="Popup"
                               Width="{Binding ElementName=FilterableComboBox, Path=ActualWidth}"
                               IsOpen="{TemplateBinding IsDropDownOpen}"
                               Placement="Bottom"
                               MaxHeight="200">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="auto" />
                                    <RowDefinition Height="*" />
                                </Grid.RowDefinitions>
                                <TextBox Grid.Row="0"
                                         x:Name="FilterTextBox"
                                         HorizontalAlignment="Stretch" />
                                <ScrollViewer Grid.Row="1"
                                              Background="White">
                                    <ItemsPresenter />
                                </ScrollViewer>
                            </Grid>
                        </Popup>
                    </Grid>
                </ControlTemplate>
            </ComboBox.Template>
        </ComboBox>
    </Grid>
</Window>
using System.ComponentModel;
using System.Windows;

namespace FilterableComboBox
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainViewModel();
        }
    }

    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public MainViewModel()
        {
            _customValues = new List<string>();
            for (int i = 0; i < 500; i++)
                _customValues.Add(i.ToString());
        }


        private string _filterText = string.Empty;
        public string FilterText
        {
            get
            {
                return _filterText;
            }
            set
            {
                _filterText = value;
                OnPropertyChanged(nameof(FilterText));
            }
        }

        private IList<string> _customValues;
        public IList<string> CustomValues
        {
            get
            {
                return _customValues;
            }
            set
            {
                _customValues = value;
                OnPropertyChanged(nameof(CustomValues));
            }
        }
    }
}
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace FilterableComboBox
{
    public class StringContainsConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter,CultureInfo culture)
        {
            string text = (string)values[0];
            string filterText = (string)values[1];

            return text.Contains(filterText)
                ? Visibility.Visible
                : Visibility.Collapsed;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Scenario:

I'm implementing a custom ComboBox in WPF with a filtering feature:

  • The ComboBox contains a Popup with a TextBox for filtering.
  • Below the TextBox, a list displays only the items that match the filter text.
  • An item remains visible if its text contains the filter text.
  • Items that don’t match are hidden using Visibility.Collapsed, which is applied via ItemContainerStyle and a converter.

Find the full implementation on the bottom.

Issue:

When the total number of items (before filtering) is below 140, everything works correctly:

  • Typing in the TextBox filters the list dynamically.
  • Deleting characters behaves as expected.

However, when the total number of items exceeds 150, strange behavior occurs when deleting characters from the TextBox:

  • If I type "1a", filtering works properly.
  • Then, if I delete only the "a", the TextBox becomes unresponsive:
    • The cursor does not reset to its expected position - in the screenshot below the cursor's position should be one character further to the left, but it remains in the wrong place after deleting a character.
    • Left/right arrow keyboard navigation stops working.
    • If I then delete "1" the cursor resets properly and everything works again.

Questions:

My requirement is that the TextBox to behave as expected in all cases. I find this cursor behavior very unexpected and unusual.

  • What is causing this behavior?
  • How can I fix this behavior with the following restrictions:
    • The solution should not involve virtualizing the ComboBox — so I cannot use VirtualizingStackPanel due to limitations in my actual use case.
    • The solution should not include any code-behind changes - must be XAML-only.
<Window x:Class="FilterableComboBox.MainWindow"
        xmlns="http://schemas.microsoft/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft/winfx/2006/xaml"
        xmlns:local="clr-namespace:FilterableComboBox"
        Title="MainWindow"
        Height="100"
        Width="400">
    <Window.Resources>
        <local:StringContainsConverter x:Key="StringContainsConverter" />
    </Window.Resources>

    <Grid>
        <ComboBox x:Name="FilterableComboBox"
                  HorizontalAlignment="Stretch"
                  Height="25"
                  Width="300"
                  ItemsSource="{Binding CustomValues}">

            <ComboBox.ItemContainerStyle>
                <Style TargetType="ComboBoxItem">
                    <Setter Property="Visibility">
                        <Setter.Value>
                            <MultiBinding Converter="{StaticResource StringContainsConverter}">
                                <Binding  />
                                <Binding ElementName="FilterTextBox" Path="Text" />
                            </MultiBinding>
                        </Setter.Value>
                    </Setter>
                </Style>
            </ComboBox.ItemContainerStyle>
            
            <ComboBox.Template>
                <ControlTemplate TargetType="ComboBox">
                    <Grid>
                        <ToggleButton Content="{TemplateBinding SelectedItem}"
                                      IsChecked="{Binding Path=IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}}" />
                        <Popup x:Name="Popup"
                               Width="{Binding ElementName=FilterableComboBox, Path=ActualWidth}"
                               IsOpen="{TemplateBinding IsDropDownOpen}"
                               Placement="Bottom"
                               MaxHeight="200">
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="auto" />
                                    <RowDefinition Height="*" />
                                </Grid.RowDefinitions>
                                <TextBox Grid.Row="0"
                                         x:Name="FilterTextBox"
                                         HorizontalAlignment="Stretch" />
                                <ScrollViewer Grid.Row="1"
                                              Background="White">
                                    <ItemsPresenter />
                                </ScrollViewer>
                            </Grid>
                        </Popup>
                    </Grid>
                </ControlTemplate>
            </ComboBox.Template>
        </ComboBox>
    </Grid>
</Window>
using System.ComponentModel;
using System.Windows;

namespace FilterableComboBox
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainViewModel();
        }
    }

    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler? PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public MainViewModel()
        {
            _customValues = new List<string>();
            for (int i = 0; i < 500; i++)
                _customValues.Add(i.ToString());
        }


        private string _filterText = string.Empty;
        public string FilterText
        {
            get
            {
                return _filterText;
            }
            set
            {
                _filterText = value;
                OnPropertyChanged(nameof(FilterText));
            }
        }

        private IList<string> _customValues;
        public IList<string> CustomValues
        {
            get
            {
                return _customValues;
            }
            set
            {
                _customValues = value;
                OnPropertyChanged(nameof(CustomValues));
            }
        }
    }
}
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace FilterableComboBox
{
    public class StringContainsConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter,CultureInfo culture)
        {
            string text = (string)values[0];
            string filterText = (string)values[1];

            return text.Contains(filterText)
                ? Visibility.Visible
                : Visibility.Collapsed;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}
Share Improve this question edited Feb 2 at 12:54 Szabolcs Antal asked Feb 2 at 12:16 Szabolcs AntalSzabolcs Antal 9574 gold badges15 silver badges31 bronze badges 8
  • Maybe the root problem is that you are not implementing a custom ComboBox, you are customizing existing ComboBox. You could create a new FilterableComboBox class derived from, say, System.Windows.Controls.ItemsControl. You could put a list box, button(s), text element, whatever can group in a combo-box-type control. The problem is: System.Windows.Controls.ComboBox provides poor access to its internal object tree, it is notoriously difficult in customization. Not only in WPF. People create alternative controls for extra functionality. – Sergey A Kryukov Commented Feb 2 at 14:37
  • "No code behind changes". I do incremental searching. I find LINQ queries while typing plenty fast enough to repeatedly load a result set with less code. For internet queries, while typing (i.e. global address verification), I use an extra async retrieval thread. – Gerry Schmitz Commented Feb 2 at 19:32
  • 1 @SergeyAKryukov OOPS MY BAD!!! I'll fix that comment I grabbed the wrong tag. I'm extremely sorry, sir. You know, I typed the S and accepted the first suggestion. – IV. Commented Feb 2 at 23:18
  • @SzabolcsAntal I'm having a real hard getting it to freeze up on this end using your code. I went all the way up to N=10000 on the combo list items. And, to be sure, that slowed it down. But I never actually got it to freeze. Sorry! Wish I could help. – IV. Commented Feb 2 at 23:19
  • @IV. The point is not the ComboBox itself to freeze, but the TextBox inside the Popup to become unresponsive when following the steps in the descrption. – Szabolcs Antal Commented Feb 3 at 12:38
 |  Show 3 more comments

1 Answer 1

Reset to default 0

Unfortunately, I couldn’t find a solution that fully meets all the restrictions outlined in my question.

The solution I ended up with subscribing to the KeyUp event of the TextBox directly in XAML and updating the FilterText property in the code-behind event handler. This ensures that FilterText is updated only after the user types a letter and releases the key. As a result, the binding events occur only after the user has finished typing each character, preventing any responsiveness issues.

<TextBox x:Name="FilterTextBox" KeyUp="FilterTextBox_KeyUp"/>
private void FilterTextBox_KeyUp(object sender, KeyEventArgs e)
{
    this.FilterText = ((TextBox)sender).Text;
}
发布评论

评论列表(0)

  1. 暂无评论