Применение фильтра к отсортированному ListCollectionView/ObservableCollection

Что у меня есть

У меня есть UserControl из TextBox и ListBox. ListBox имеет ItemsSource, связанное с ObservableCollection в DataContext через ListCollectionView с пользовательской сортировкой и фильтром, как мы увидим ниже. Целью элемента управления является отображение в ListBox только тех элементов (strings) в исходной коллекции, которые содержат текст в TextBox. Для этого я применяю фильтр к ListCollectionView.

У меня есть два дополнительных ограничения. 1 моя исходная коллекция не отсортирована по алфавиту, но элементы, отображаемые в ListBox, используют ListCollectionView CustomSort. 2, я должен отображать только первые 5 элементов (отсортированных по алфавиту), которые соответствуют строке в TextBox. Для этого я применяю фильтр к ListCollectionView.

Ожидание

Допустим, моя коллекция определена как таковая в моем DataContext:

this.AllItems = new ObservableCollection<string>
{
    "Banana", 
    "Watermelon",
    "Peach",
    "Grape",
    "Apple",
    "Pineapple",
    "Cherry",
    "Durian",
    "Rambutan",
    "Strawberry",
    "Raspberry",
    "Lemon",
    "Orange",
    "Sugar cane",
    "Guava",
    "Tomato",
    "Coconut",
    "Melon",
    "Äpple",
    "Glaçon",
    "Etape",
    "Étape"
};

И в моем TextBox я ввожу букву «e» (все сравнения выполняются без учета регистра). Я ожидаю, что ListBox отобразит следующие 5 элементов (для CurrentUICulture установлено значение fr-FR):

  • яблоко
  • Яблоко
  • вишня
  • Этап
  • Этап

потому что это первые 5 элементов, которые содержат букву «е» при сортировке по алфавиту. Однако я получаю следующие элементы в своем приложении:

  • яблоко
  • Виноград
  • Персик
  • Ананас
  • Арбуз

потому что это первые 5 предметов в моей коллекции, которые содержат букву "e" THEN, отсортированные по алфавиту.

Мой код

Вот код, чтобы понять, что у меня есть, и мою проблему. Это должно работать практически только с использованием копирования/вставки ниже (остерегайтесь пространств имен и CurrentUICulture). Я использую С# 4.0.

1) MainWindow

<Window x:Class="MyNamespace.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        xmlns:local="clr-namespace:MyNamespace">
    <Window.Resources>
        <local:FoobarViewModel x:Key="Foobar"/>
    </Window.Resources>
    <StackPanel>
        <local:Foobar DataContext="{StaticResource Foobar}" AllItems="{Binding AllItems}"/>
    </StackPanel>
</Window>

2) Класс, используемый как DataContext

public class FoobarViewModel : INotifyPropertyChanged
{
    private ObservableCollection<string> allItems;
    public event PropertyChangedEventHandler PropertyChanged;

    public FoobarViewModel()
    {
        this.AllItems = new ObservableCollection<string>
        {
            "Banana", 
            "Watermelon",
            "Peach",
            "Grape",
            "Apple",
            "Pineapple",
            "Cherry",
            "Durian",
            "Rambutan",
            "Strawberry",
            "Raspberry",
            "Lemon",
            "Orange",
            "Sugar cane",
            "Guava",
            "Tomato",
            "Coconut",
            "Melon",
            "Äpple",
            "Glaçon",
            "Etape",
            "Étape"
        };
    }

    public ObservableCollection<string> AllItems
    {
        get
        {
            return this.allItems;
        }
        set
        {
            this.allItems = value;
            this.OnPropertyChanged("AllItems");
        }
    }

    private void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

3) XAML моего UserControl

<UserControl x:Class="MyNamespace.Foobar"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <TextBox x:Name="textbox" Grid.Row="0"/>
        <ListBox x:Name="listbox" Grid.Row="1"/>
    </Grid>
</UserControl>

4) И, наконец, самый важный код моего UserControl Foobar.

public partial class Foobar : UserControl
{
    #region Fields
    public static readonly DependencyProperty AllItemsProperty = DependencyProperty.Register(
        "AllItems",
        typeof(IEnumerable<string>),
        typeof(Foobar),
        new PropertyMetadata(AllItemsChangedCallback));

    private const int MaxItems = 5;
    #endregion

    #region Constructors
    public Foobar()
    {
        InitializeComponent();

        textbox.KeyUp += TextboxKeyUp;
    }
    #endregion

    #region Properties
    public IEnumerable<string> AllItems
    {
        get { return (IEnumerable<string>)this.GetValue(AllItemsProperty); }
        set { this.SetValue(AllItemsProperty, value); }
    }
    #endregion

    #region Methods
    private void TextboxKeyUp(object sender, KeyEventArgs e)
    {
        TextBox localTextBox = sender as TextBox;
        if (localTextBox != null)
        {
            var items = ((ListCollectionView)listbox.ItemsSource).SourceCollection;

            if (items.Cast<string>().Any(x => x.ToLower(CultureInfo.CurrentUICulture).Contains(localTextBox.Text.ToLower(CultureInfo.CurrentUICulture))))
            {
                this.ApplyFilter();
                listbox.Visibility = Visibility.Visible;
            }
            else
            {
                listbox.Visibility = Visibility.Collapsed;
            }
        }
    }

    private static void AllItemsChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        Foobar control = sender as Foobar;
        if (control != null)
        {
            List<string> source = new List<string>((IEnumerable<string>)e.NewValue);

            ListCollectionView view = (ListCollectionView)CollectionViewSource.GetDefaultView(source);
            view.CustomSort = new CustomSort();
            control.listbox.ItemsSource = view;
            control.ApplyFilter();
        }
    }

    private void ApplyFilter()
    {
        ListCollectionView view = (ListCollectionView)listbox.ItemsSource;

        int index = 0;
        view.Filter = x =>
        {
            bool result = x.ToString().ToLower(CultureInfo.CurrentUICulture).Contains(textbox.Text.ToLower(CultureInfo.CurrentUICulture));
            if (result)
            {
                index++;
            }

            return index <= MaxItems && result;
        };
    }
    #endregion

    private class CustomSort : IComparer
    {
        public int Compare(object x, object y)
        {
            return String.Compare(x.ToString(), y.ToString(), CultureInfo.CurrentUICulture, CompareOptions.IgnoreCase);
        }
    }
}

Весь код работает как положено, за исключением фильтрации, которая выполняется в методе ApplyFilter. По сути, этот метод просто проверяет каждый элемент в коллекции на соответствие тому, что находится в TextBox, и при условии, что число возвращенных элементов не превышает максимально допустимого, элемент будет включен в фильтр. Когда я отлаживаю этот метод, я вижу, что элементы просматриваются в исходном порядке коллекции, а не в порядке сортировки, хотя фильтр, похоже, выполняется на ListCollectionView, а не на ObservableCollection<string>.

Кажется, сначала применяется фильтр, а затем сортировка. Я хочу, чтобы сначала применялась сортировка, а затем фильтрация.

Мой вопрос

Как я могу применить фильтр к отсортированному ListCollectionView, а не к исходной несортированной коллекции?


person Guillaume    schedule 16.02.2013    source источник


Ответы (1)


Почему бы не создать общий IComparer<T> и использовать Enumerable.OrderBy<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>, IComparer<TKey>) перед созданием представления коллекции.

Таким образом, вы получите что-то вроде:

private static void AllItemsChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    Foobar control = sender as Foobar;
    if (control != null)
    {
        var newEnumerable = (IEnumerable<string>)e.NewValue;
        var sorted = newEnumerable.OrderBy(s => s, new CustomSort());
        var source = new List<string>(sorted);

        var view = (ListCollectionView)CollectionViewSource.GetDefaultView(source);
        control.listbox.ItemsSource = view;
        control.ApplyFilter();
    }
}

private class CustomSort : IComparer<string>
{
    public int Compare(string x, string y)
    {
        return String.Compare(x, y, CultureInfo.CurrentUICulture, CompareOptions.IgnoreCase);
    }
}

Тогда представление коллекции уже отсортировано и можно применить фильтрацию.

person Lukazoid    schedule 16.02.2013
comment
Мужик, твое решение такое простое, но оно так прекрасно работает! Прошло 3 дня, я пытался найти решение и ничего не мог придумать... Большое спасибо! - person Guillaume; 16.02.2013