基于文本输入的WPF组合框动态滤波器

时间:2010-01-04 20:04:04

标签: wpf combobox

我似乎找不到直接的方法来实现将文本输入过滤到WPF组合框中的项目列表中。
通过将IsTextSearchEnabled设置为true,comboBox下拉列表将跳转到第一个匹配项目的任何内容。我需要的是将列表过滤到与文本字符串匹配的任何内容(例如,如果我专注于我的组合框并键入'abc',我希望看到ItemsSource集合中的所有项目以(或最好包含)开头)'abc'作为下拉列表的成员。)

我怀疑它有所不同,但我的显示项目模板化为复杂类型的属性:

<ComboBox x:Name="DiagnosisComboBox" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" 
          ItemsSource="{Binding Path = ApacheDxList,
                                UpdateSourceTrigger=PropertyChanged,
                                Mode=OneWay}"
          IsTextSearchEnabled="True"
          ItemTemplate="{StaticResource DxDescriptionTemplate}" 
          SelectedValue="{Binding Path = SelectedEncounterDetails.Diagnosis,
                                  Mode=TwoWay,
                                  UpdateSourceTrigger=PropertyChanged}"/>

感谢。

6 个答案:

答案 0 :(得分:6)

我几天前刚使用此网站代码的修改版本执行此操作:Credit where credit is due

我的完整代码如下:

using System.Collections;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;

    namespace MyControls
    {
        public class FilteredComboBox : ComboBox
        {
            private string oldFilter = string.Empty;

            private string currentFilter = string.Empty;

            protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;


            protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
            {
                if (newValue != null)
                {
                    var view = CollectionViewSource.GetDefaultView(newValue);
                    view.Filter += FilterItem;
                }

                if (oldValue != null)
                {
                    var view = CollectionViewSource.GetDefaultView(oldValue);
                    if (view != null) view.Filter -= FilterItem;
                }

                base.OnItemsSourceChanged(oldValue, newValue);
            }

            protected override void OnPreviewKeyDown(KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.Tab:
                    case Key.Enter:
                        IsDropDownOpen = false;
                        break;
                    case Key.Escape:
                        IsDropDownOpen = false;
                        SelectedIndex = -1;
                        Text = currentFilter;
                        break;
                    default:
                        if (e.Key == Key.Down) IsDropDownOpen = true;

                        base.OnPreviewKeyDown(e);
                        break;
                }

                // Cache text
                oldFilter = Text;
            }

            protected override void OnKeyUp(KeyEventArgs e)
            {
                switch (e.Key)
                {
                    case Key.Up:
                    case Key.Down:
                        break;
                    case Key.Tab:
                    case Key.Enter:

                        ClearFilter();
                        break;
                    default:
                        if (Text != oldFilter)
                        {
                            RefreshFilter();
                            IsDropDownOpen = true;

                            EditableTextBox.SelectionStart = int.MaxValue;
                        }

                        base.OnKeyUp(e);
                        currentFilter = Text;
                        break;
                }
            }

            protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
            {
                ClearFilter();
                var temp = SelectedIndex;
                SelectedIndex = -1;
                Text = string.Empty;
                SelectedIndex = temp;
                base.OnPreviewLostKeyboardFocus(e);
            }

            private void RefreshFilter()
            {
                if (ItemsSource == null) return;

                var view = CollectionViewSource.GetDefaultView(ItemsSource);
                view.Refresh();
            }

            private void ClearFilter()
            {
                currentFilter = string.Empty;
                RefreshFilter();
            }

            private bool FilterItem(object value)
            {
                if (value == null) return false;
                if (Text.Length == 0) return true;

                return value.ToString().ToLower().Contains(Text.ToLower());
            }
        }
    }

WPF应该是这样的:

<MyControls:FilteredComboBox ItemsSource="{Binding MyItemsSource}"
    SelectedItem="{Binding MySelectedItem}"
    DisplayMemberPath="Name" 
    IsEditable="True" 
    IsTextSearchEnabled="False" 
    StaysOpenOnEdit="True">

    <MyControls:FilteredComboBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel VirtualizationMode="Recycling" />
        </ItemsPanelTemplate>
    </MyControls:FilteredComboBox.ItemsPanel>
</MyControls:FilteredComboBox>

这里要注意一些事项。您会注意到FilterItem实现在对象上执行ToString()。这意味着您要显示的对象的属性应该在object.ToString()实现中返回。 (或者已经是一个字符串)换句话说就是这样:

public class Customer
{
    public string Name { get; set; }
    public string Address { get; set; }
    public string PhoneNumber { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

如果这不能满足您的需求,我想您可以获得DisplayMemberPath的值并使用反射来获取属性以使用它,但这样会慢一点,所以除非必要,否则我不建议这样做。 / p>

此实现也不会阻止用户在ComboBox的TextBox部分中键入他们喜欢的内容。如果他们输入了一些愚蠢的东西,那么SelectedItem将恢复为NULL,所以要准备好在你的代码中处理它。

此外,如果您有很多项目,我强烈建议您使用VirtualizingStackPanel,就像上面的示例一样,因为它在加载时间方面有很大差异

答案 1 :(得分:1)

您可以尝试https://www.nuget.org/packages/THEFilteredComboBox/并提供反馈。我计划尽可能多地获得反馈并创建完美的滤波组合框,我们都错过了WPF。

答案 2 :(得分:1)

凯莉的答案很棒。但是,如果您在列表中选择一个项目(突出显示输入文本)然后按BackSpace,则会出现一个小错误,输入文本将恢复为所选项目,而ComboBox的SelectedItem属性仍然是您之前选择的项目。

以下是修复错误的代码,并添加了在输入文本与其匹配时自动选择项目的功能。

using S = struct { int x; }

答案 3 :(得分:1)

基于this answer,我添加了:

  • 使用InputSource属性将用户输入限制为OnlyValuesInList中提供的值的功能。
  • 使用Esc键清除过滤器
  • 使用向下箭头键打开组合框。
  • 处理Backspace键不会清除选择,只会过滤文本。
  • 隐藏辅助类和方法
  • 删除了不必要的方法
  • 添加的SelectionEffectivelyChanged事件仅在用户离开控件或按Enter时触发,如从标准ComboBox过滤SelectionChanged事件时触发多次。
  • 添加的EffectivelySelectedItem属性仅在用户离开控件或按Enter时才会更改,就像从标准ComboBox过滤SelectedItem项的过程中更改一样。
public class FilterableComboBox : ComboBox
{
    /// <summary>
    /// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present
    /// in the list, it leaves it blank.
    /// </summary>
    public bool OnlyValuesInList {
        get => (bool)GetValue(OnlyValuesInListProperty);
        set => SetValue(OnlyValuesInListProperty, value);
    }
    public static readonly DependencyProperty OnlyValuesInListProperty =
        DependencyProperty.Register(nameof(OnlyValuesInList), typeof(bool), typeof(FilterableComboBox));

    /// <summary>
    /// Selected item, changes only on lost focus or enter key pressed
    /// </summary>
    public object EffectivelySelectedItem {
        get => (bool)GetValue(EffectivelySelectedItemProperty);
        set => SetValue(EffectivelySelectedItemProperty, value);
    }
    public static readonly DependencyProperty EffectivelySelectedItemProperty =
        DependencyProperty.Register(nameof(EffectivelySelectedItem), typeof(object), typeof(FilterableComboBox));

    private string CurrentFilter = string.Empty;
    private bool TextBoxFreezed;
    protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
    private UserChange<bool> IsDropDownOpenUC;

    /// <summary>
    /// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
    /// </summary>
    public event Action<FilterableComboBox, object> SelectionEffectivelyChanged;

    public FilterableComboBox()
    {
        IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
        DropDownOpened += FilteredComboBox_DropDownOpened;

        IsEditable = true;
        IsTextSearchEnabled = true;
        StaysOpenOnEdit = true;
        IsReadOnly = false;

        Loaded += (s, e) => {
            if (EditableTextBox != null)
                new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
        };

        SelectionChanged += (_, __) => shouldTriggerSelectedItemChanged = true;

        SelectionEffectivelyChanged += (_, o) => EffectivelySelectedItem = o;
    }

    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        base.OnPreviewKeyDown(e);
        if (e.Key == Key.Down && !IsDropDownOpen) {
            IsDropDownOpen = true;
            e.Handled = true;
        }
        else if (e.Key == Key.Escape) {
            ClearFilter();
            Text = "";
            IsDropDownOpen = true;
        }
        else if (e.Key == Key.Enter || e.Key == Key.Tab) {
            CheckSelectedItem();
            TriggerSelectedItemChanged();
        }
    }

    protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
    {
        base.OnPreviewLostKeyboardFocus(e);
        CheckSelectedItem();
        if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
            TriggerSelectedItemChanged();
    }

    private void CheckSelectedItem()
    {
        if (OnlyValuesInList)
            Text = SelectedItem?.ToString() ?? "";
    }

    private bool shouldTriggerSelectedItemChanged = false;
    private void TriggerSelectedItemChanged()
    {
        if (shouldTriggerSelectedItemChanged) {
            SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
            shouldTriggerSelectedItemChanged = false;
        }
    }

    public void ClearFilter()
    {
        if (string.IsNullOrEmpty(CurrentFilter)) return;
        CurrentFilter = "";
        CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
    }

    private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
    {
        if (IsDropDownOpenUC.IsUserChange)
            ClearFilter();
    }

    private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
    {
        if (TextBoxFreezed) return;
        var tb = EditableTextBox;
        if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
            CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
        else
            CurrentFilter = tb.Text.ToLower();
        RefreshFilter();
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (newValue != null) {
            var view = CollectionViewSource.GetDefaultView(newValue);
            view.Filter += FilterItem;
        }

        if (oldValue != null) {
            var view = CollectionViewSource.GetDefaultView(oldValue);
            if (view != null) view.Filter -= FilterItem;
        }

        base.OnItemsSourceChanged(oldValue, newValue);
    }

    private void RefreshFilter()
    {
        if (ItemsSource == null) return;

        var view = CollectionViewSource.GetDefaultView(ItemsSource);
        FreezTextBoxState(() => {
            var isDropDownOpen = IsDropDownOpen;
            //always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
            IsDropDownOpenUC.Set(false);
            view.Refresh();

            if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
                IsDropDownOpenUC.Set(true);

            if (SelectedItem == null) {
                foreach (var itm in ItemsSource)
                    if (itm.ToString() == Text) {
                        SelectedItem = itm;
                        break;
                    }
            }
        });
    }

    private void FreezTextBoxState(Action action)
    {
        TextBoxFreezed = true;
        var tb = EditableTextBox;
        var text = Text;
        var selStart = tb.SelectionStart;
        var selLen = tb.SelectionLength;
        action();
        Text = text;
        tb.SelectionStart = selStart;
        tb.SelectionLength = selLen;
        TextBoxFreezed = false;
    }

    private bool FilterItem(object value)
    {
        if (value == null) return false;
        if (CurrentFilter.Length == 0) return true;

        return value.ToString().ToLower().Contains(CurrentFilter);
    }

    private class TextBoxBaseUserChangeTracker
    {
        private bool IsTextInput { get; set; }

        public TextBoxBase TextBoxBase { get; set; }
        private List<Key> PressedKeys = new List<Key>();
        public event EventHandler UserTextChanged;
        private string LastText;

        public TextBoxBaseUserChangeTracker(TextBoxBase textBoxBase)
        {
            TextBoxBase = textBoxBase;
            LastText = TextBoxBase.ToString();

            textBoxBase.PreviewTextInput += (s, e) => {
                IsTextInput = true;
            };

            textBoxBase.TextChanged += (s, e) => {
                var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
                IsTextInput = false;
                LastText = TextBoxBase.ToString();
                if (isUserChange)
                    UserTextChanged?.Invoke(this, e);
            };

            textBoxBase.PreviewKeyDown += (s, e) => {
                switch (e.Key) {
                    case Key.Back:
                    case Key.Space:
                        if (!PressedKeys.Contains(e.Key))
                            PressedKeys.Add(e.Key);
                        break;
                }
                if (e.Key == Key.Back) {
                    var textBox = textBoxBase as TextBox;
                    if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) {
                        textBox.SelectionStart--;
                        textBox.SelectionLength++;
                        e.Handled = true;
                        UserTextChanged?.Invoke(this, e);
                    }
                }
            };

            textBoxBase.PreviewKeyUp += (s, e) => {
                if (PressedKeys.Contains(e.Key))
                    PressedKeys.Remove(e.Key);
            };

            textBoxBase.LostFocus += (s, e) => {
                PressedKeys.Clear();
                IsTextInput = false;
            };
        }
    }

    private class UserChange<T>
    {
        private Action<T> action;

        public bool IsUserChange { get; private set; } = true;

        public UserChange(Action<T> action)
        {
            this.action = action;
        }

        public void Set(T val)
        {
            try {
                IsUserChange = false;
                action(val);
            }
            finally {
                IsUserChange = true;
            }
        }
    }
}

答案 4 :(得分:0)

这是我的看法。一种不同的方法,一种是我为自己制定的方法,另一种是我正在使用的方法。它与IsTextSearchEnabled =“ true”一起使用。我刚刚完成它,所以可能会有一些错误。

    public class TextBoxBaseUserChangeTracker
{
    private bool IsTextInput { get; set; }

    public TextBoxBase TextBox { get; set; }
    private List<Key> PressedKeys = new List<Key>();
    public event EventHandler UserTextChanged;
    private string LastText;

    public TextBoxBaseUserChangeTracker(TextBoxBase textBox)
    {
        TextBox = textBox;
        LastText = TextBox.ToString();

        textBox.PreviewTextInput += (s, e) =>
        {
            IsTextInput = true;
        };

        textBox.TextChanged += (s, e) =>
        {
            var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBox.ToString();
            IsTextInput = false;
            LastText = TextBox.ToString();
            if (isUserChange)
                UserTextChanged?.Invoke(this, e);
        };

        textBox.PreviewKeyDown += (s, e) =>
        {
            switch (e.Key)
            {
                case Key.Back:
                case Key.Space:
                case Key.Delete:
                    if (!PressedKeys.Contains(e.Key))
                        PressedKeys.Add(e.Key);
                    break;
            }
        };

        textBox.PreviewKeyUp += (s, e) =>
        {
            if (PressedKeys.Contains(e.Key))
                PressedKeys.Remove(e.Key);
        };

        textBox.LostFocus += (s, e) =>
        {
            PressedKeys.Clear();
            IsTextInput = false;
        };
    }
}

    public static class ExtensionMethods
{
    #region DependencyObject
    public static T FindParent<T>(this DependencyObject child) where T : DependencyObject
    {
        //get parent item
        DependencyObject parentObject = VisualTreeHelper.GetParent(child);

        //we've reached the end of the tree
        if (parentObject == null) return null;

        //check if the parent matches the type we're looking for
        T parent = parentObject as T;
        if (parent != null)
            return parent;
        else
            return parentObject.FindParent<T>();
    }
    #endregion

    #region TextBoxBase
    public static TextBoxBaseUserChangeTracker TrackUserChange(this TextBoxBase textBox)
    {
        return new TextBoxBaseUserChangeTracker(textBox);
    }
    #endregion
}

    public class UserChange<T>
{
    private Action<T> action;

    private bool isUserChange = true;
    public bool IsUserChange
    {
        get
        {
            return isUserChange;
        }
    }

    public UserChange(Action<T> action)
    {
        this.action = action;
    }

    public void Set(T val)
    {
        try
        {
            isUserChange = false;
            action(val);
        }
        finally
        {
            isUserChange = true;
        }
    }
}


public class FilteredComboBox : ComboBox
{
    // private string oldFilter = string.Empty;

    private string CurrentFilter = string.Empty;
    private bool TextBoxFreezed;
    protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
    private UserChange<bool> IsDropDownOpenUC;

    public FilteredComboBox()
    {
        IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
        DropDownOpened += FilteredComboBox_DropDownOpened;

        Loaded += (s, e) =>
        {
            if (EditableTextBox != null)
            {
                EditableTextBox.TrackUserChange().UserTextChanged += FilteredComboBox_UserTextChange;
            }
        };
    }

    public void ClearFilter()
    {
        if (string.IsNullOrEmpty(CurrentFilter)) return;
        CurrentFilter = "";
        CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
    }

    private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
    {
        //if user opens the drop down show all items
        if (IsDropDownOpenUC.IsUserChange)
            ClearFilter();
    }

    private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
    {
        if (TextBoxFreezed) return;
        var tb = EditableTextBox;
        if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
            CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
        else
            CurrentFilter = tb.Text.ToLower();
        RefreshFilter();
    }

    protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
    {
        if (newValue != null)
        {
            var view = CollectionViewSource.GetDefaultView(newValue);
            view.Filter += FilterItem;
        }

        if (oldValue != null)
        {
            var view = CollectionViewSource.GetDefaultView(oldValue);
            if (view != null) view.Filter -= FilterItem;
        }

        base.OnItemsSourceChanged(oldValue, newValue);
    }

    private void RefreshFilter()
    {
        if (ItemsSource == null) return;

        var view = CollectionViewSource.GetDefaultView(ItemsSource);
        FreezTextBoxState(() =>
        {
            var isDropDownOpen = IsDropDownOpen;
            //always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
            IsDropDownOpenUC.Set(false);
            view.Refresh();

            if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
                IsDropDownOpenUC.Set(true);

            if (SelectedItem == null)
            {
                foreach (var itm in ItemsSource)
                {
                    if (itm.ToString() == Text)
                    {
                        SelectedItem = itm;
                        break;
                    }
                }
            }
        });
    }

    private void FreezTextBoxState(Action action)
    {
        TextBoxFreezed = true;
        var tb = EditableTextBox;
        var text = Text;
        var selStart = tb.SelectionStart;
        var selLen = tb.SelectionLength;
        action();
        Text = text;
        tb.SelectionStart = selStart;
        tb.SelectionLength = selLen;
        TextBoxFreezed = false;
    }

    private bool FilterItem(object value)
    {
        if (value == null) return false;
        if (CurrentFilter.Length == 0) return true;

        return value.ToString().ToLower().Contains(CurrentFilter);
    }
}

Xaml:

        <local:FilteredComboBox ItemsSource="{Binding List}" IsEditable="True" IsTextSearchEnabled="true" StaysOpenOnEdit="True" x:Name="cmItems" SelectionChanged="CmItems_SelectionChanged">

    </local:FilteredComboBox>

答案 5 :(得分:-1)

听起来你正在寻找的东西类似于自动完成的文本框,它在类似于组合框弹出窗口的弹出窗口中提供完成建议。

您可能会发现此CodeProject文章很有用:

A Reusable WPF Autocomplete TextBox