Перейти к содержанию

Поиск в выпадающем списке ComboBox

Для использования необходимо подключить nuget-пакет ModPlusStyle версии не ниже 16.0 (если в пакете модуля версия ниже)!

<PackageReference Include="ModPlusStyle" Version="16.0.0" ExcludeAssets="runtime" />


ComboBoxFilterAssist

Назначение

ComboBoxFilterAssist — присоединённое свойство, которое добавляет в выпадающий список ComboBox поле поиска. Пользователь открывает список, сразу начинает печатать, и в списке остаются только элементы, содержащие введённый текст.

Используйте его для длинных списков: семейства, типоразмеры, листы, параметры, уровни и т.п. Если в списке 5–10 элементов, поиск не нужен — он только занимает место.

Подключение

Пространство имён — то же, что и для остальных контролов библиотеки:

xmlns:mps="https://modplus.org/winfx/xaml/controls"

Достаточно одного свойства:

<ComboBox mps:ComboBoxFilterAssist.IsFilterEnabled="True"
          ItemsSource="{Binding Levels}"
          SelectedItem="{Binding SelectedLevel}"
          DisplayMemberPath="Name" />

Поле поиска встроено в стандартный стиль ComboBox библиотеки и работает во всех вариантах выпадающего списка: при открытии вниз, вверх и в классическом режиме (mps:ComboBoxAssist.ClassicMode="True"). Подсказка в пустом поле локализуется автоматически, у поля есть кнопка очистки.

Как ищется текст

Поиск — по вхождению подстроки без учёта регистра: запрос эт найдёт «Этаж 1» и «Отметка».

Текст, по которому сравнивается элемент, берётся в следующем порядке (используется первый найденный):

# Источник Когда срабатывает
1 TextSearch.Text элемента Элемент — контейнер (ComboBoxItem) с заданным TextSearch.Text
2 Content элемента Элемент — ComboBoxItem (или другой ContentControl), объявленный в XAML
3 DisplayMemberPath Задан у ComboBox
4 TextSearch.TextPath Задан у ComboBox
5 ToString() Во всех остальных случаях

DisplayMemberPath и TextSearch.TextPath поддерживают вложенные пути вида Address.City.

Элементы с ItemTemplate

Это самый частый случай, когда поиск «не работает». Если элементы отображаются через ItemTemplate, фильтр не знает, какой текст видит пользователь, и сравнивает с ToString() — обычно это имя типа. Укажите свойство для поиска через TextSearch.TextPath:

<ComboBox mps:ComboBoxFilterAssist.IsFilterEnabled="True"
          TextSearch.TextPath="Name"
          ItemsSource="{Binding FamilySymbols}"
          SelectedItem="{Binding SelectedFamilySymbol}">
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding FamilyName}" Opacity="0.6" />
                <TextBlock Text="{Binding Name}" Margin="{mps:Margin '1 0 0 0'}" />
            </StackPanel>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

Если искать нужно по нескольким полям сразу, добавьте в модель элемента свойство со склеенным текстом (например, SearchText => $"{FamilyName} {Name}") и укажите его в TextSearch.TextPath. Переопределение ToString() тоже работает, но менее очевидно для того, кто будет читать код.

TextSearch.TextPath используется и стандартным поиском WPF по первым буквам в закрытом ComboBox, так что его стоит задавать для любого списка с ItemTemplate.

Элементы, объявленные в XAML

Для ComboBoxItem с текстовым Content ничего дополнительно задавать не нужно:

<ComboBox mps:ComboBoxFilterAssist.IsFilterEnabled="True">
    <ComboBoxItem Content="Сверху" />
    <ComboBoxItem Content="Снизу" />
    <ComboBoxItem Content="По центру" />
</ComboBox>

Если Content не строка (цветной маркер + текст, иконка + текст и т.п.), задайте текст для поиска явно:

<ComboBoxItem TextSearch.Text="Красный">
    <StackPanel Orientation="Horizontal">
        <Rectangle Width="12" Height="12" Fill="Red" />
        <TextBlock Text="Красный" Margin="{mps:Margin '1 0 0 0'}" />
    </StackPanel>
</ComboBoxItem>

Работа с клавиатурой

Действие Результат
Открытие списка Фокус сразу в поле поиска — можно печатать без клика
Ввод символов Список фильтруется по мере ввода. Если фокус «ушёл» на элемент списка (например, после наведения мыши), ввод всё равно попадает в поле поиска
Backspace Удаляет символ в поле поиска, даже если фокус на элементе списка
Up / Down / Page Up / Page Down Переход из поля поиска в список и навигация по нему
Enter в поле поиска Выбирает первый элемент, подходящий под запрос, и закрывает список. Если совпадений нет — список просто закрывается без изменения выбора
Esc Закрывает список без изменения выбора

При закрытии списка поле поиска очищается, фильтр сбрасывается — при следующем открытии виден весь список.

Для редактируемого ComboBox (IsEditable="True") перенаправление ввода в поле поиска отключено: там ввод принадлежит самому ComboBox.

Особенности поведения

Выбранный элемент всегда в списке

Текущий SelectedItem не скрывается фильтром, даже если не подходит под запрос. Это сделано намеренно: если выбранный элемент выпадает из отфильтрованного представления, Selector сбрасывает выбор, и TwoWay-привязка записывает null в модель представления.

Поэтому в отфильтрованном списке может оказаться «лишний» элемент — это выбранный, а не ошибка фильтра.

Размер выпадающего списка не меняется

Пока поиск активен, выпадающий список сохраняет размер, с которым был открыт, и не «прыгает» по мере ввода. Без этого список, открытый вверх, мог бы оторваться от ComboBox или перескочить вниз и закрыться.

Общее представление коллекции

Фильтр ставится на представление коллекции, которое использует ComboBox. Если задан ItemsSource, это представление по умолчанию для исходной коллекции — то же самое, что получают все другие контролы, привязанные к ней без CollectionViewSource.

Следствие: пока список открыт и в поле поиска есть текст, другие контролы, привязанные к той же коллекции, тоже видят отфильтрованные данные. После закрытия списка всё возвращается.

Обычно это незаметно, но если, например, ComboBox и DataGrid рядом привязаны к одной и той же коллекции, дайте ComboBox отдельное представление:

<Window.Resources>
    <CollectionViewSource x:Key="LevelsForComboBox" Source="{Binding Levels}" />
</Window.Resources>

<ComboBox mps:ComboBoxFilterAssist.IsFilterEnabled="True"
          ItemsSource="{Binding Source={StaticResource LevelsForComboBox}}"
          DisplayMemberPath="Name" />

По той же причине не стоит одновременно использовать ComboBoxFilterAssist и собственный Filter на представлении коллекции, к которой привязан ComboBox: на время поиска фильтр будет заменён, а после закрытия списка — сброшен.

Когда фильтрация не работает

Фильтр применяется только если представление коллекции поддерживает фильтрацию (CanFilter). Для обычных коллекций (List<T>, ObservableCollection<T>, массивы) это так. Не поддерживает фильтрацию, например, DataView (DataTable.DefaultView) — для такого источника поле поиска отображается, но список не фильтруется. Преобразуйте данные в коллекцию объектов.

Когда использовать

  • Длинные списки, в которых пользователю проще набрать часть названия, чем прокручивать.
  • Списки, в которых названия похожи и отличаются окончанием (типоразмеры, листы, марки).

Когда не использовать:

  • Короткие списки из нескольких элементов.
  • Редактируемый ComboBox с автодополнением — у него уже есть своё поле ввода.