Динамический фильтр WPF combobox на основе ввода текста - PullRequest
4 голосов
/ 04 января 2010

Кажется, я не могу найти прямой метод для реализации фильтрации ввода текста в список элементов в выпадающем списке 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}"/>

Спасибо.

Ответы [ 5 ]

8 голосов
/ 01 февраля 2017

Я только что сделал это несколько дней назад, используя модифицированную версию кода с этого сайта: Кредит, причитающийся кредит

Мой полный код указан ниже:

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 и использовать отражение, чтобы свойство использовало его, но это было бы медленнее, поэтому я не рекомендую делать это без необходимости.

Также эта реализация НЕ мешает пользователю печатать все, что ему нравится, в части TextBox ComboBox. Если они введут что-то глупое, SelectedItem вернется к NULL, так что будьте готовы обработать это в вашем коде.

Кроме того, если у вас много элементов, я настоятельно рекомендую использовать VirtualizingStackPanel, как в моем примере выше, так как это существенно меняет время загрузки

1 голос
/ 01 февраля 2018

Ответ Келли великолепен. Однако существует небольшая ошибка, заключающаяся в том, что если вы выберете элемент в списке (выделив текст ввода), а затем нажмете BackSpace, то текст ввода вернется к выбранному элементу, а свойство SelectedItem ComboBox останется элементом, выбранным ранее.

Ниже приведен код для исправления ошибки и добавления возможности автоматического выбора элемента, когда ему соответствует текст ввода.

using System.Collections;
using System.Diagnostics;
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)
                    {
                        var temp = Text;
                        RefreshFilter(); //RefreshFilter will change Text property
                        Text = temp;

                        if (SelectedIndex != -1 && Text != Items[SelectedIndex].ToString())
                        {
                            SelectedIndex = -1; //Clear selection. This line will also clear Text property
                            Text = temp;
                        }


                        IsDropDownOpen = true;

                        EditableTextBox.SelectionStart = int.MaxValue;
                    }

                    //automatically select the item when the input text matches it
                    for (int i = 0; i < Items.Count; i++)
                    {
                        if (Text == Items[i].ToString())
                            SelectedIndex = i;
                    }

                    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());
        }
    }
}
1 голос
/ 28 июля 2015

Вы можете попробовать https://www.nuget.org/packages/THEFilteredComboBox/ и оставить отзыв. Я планирую получить как можно больше отзывов и создать идеальный отфильтрованный комбинированный список, который мы все пропускаем в WPF.

0 голосов
/ 15 марта 2019

Это мое мнение. Другой подход, который я разработал для себя и который я использую. Это работает с 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>
0 голосов
/ 04 января 2010

Звучит так, будто вы действительно ищете что-то похожее на текстовое поле с автозаполнением, которое предлагает всплывающие подсказки во всплывающем окне, аналогичном всплывающему списку.

Эта статья CodeProject может оказаться полезной:

Многоразовый текстовый блок автозаполнения WPF

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...