Question

What I have

I have a UserControl made of a TextBox and a ListBox. The ListBox has its ItemsSource bound to an ObservableCollection in a DataContext via a ListCollectionView with a custom sorting and filter as we'll see below. The purpose of the control is to display in the ListBox only the items (strings) in the source collection that contain the text in the TextBox. For that purpose I apply a filter on the ListCollectionView.

I have two additional constraints. 1, my original collection is not alphabetically sorted but the items displayed in the ListBox are using a ListCollectionView CustomSort. 2, I must display only the first 5 items (alphabetically sorted) that match the string in the TextBox. I apply a filter on the ListCollectionView for that.

Expectation

Let's say my collection is defined as such in my 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"
};

And in my TextBox I input the letter 'e' (all comparisons done are case-insenstive). I expect the ListBox to display the following 5 items (CurrentUICulture is set to fr-FR):

  • Apple
  • Äpple
  • Cherry
  • Etape
  • Étape

because they are the first 5 items that contain the letter 'e' when sorted alphabetically. However I get the following items in my application:

  • Apple
  • Grape
  • Peach
  • Pineapple
  • Watermelon

because they are the first 5 items in my collection that contain the letter 'e' THEN sorted alphabetically.

My code

Here's the code to understand what I have and my problem. It should work virtually only using a copy/paste of the below (beware of namespaces and CurrentUICulture). I'm using C# 4.0.

1) The 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) The class used as the 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) The XAML of my 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) And finally, most important the code behind of my 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);
        }
    }
}

The whole code is working as expected, except the filtering which is done in the ApplyFilter method. Basically, this method just checks each and every item in the collection against whatever is in the TextBox and provided there is not already more than the maximum number of items returned, the item will be included in the filter. When I debug this method, I can see that items are browsed in the original order of the collection and not in the sorted order althought the filter seem to be done on the ListCollectionView and not the ObservableCollection<string>.

It seems the filter is applied first, then the sorting. I want the sorting to apply first then the filtering.

My question

How can I apply the filter on the sorted ListCollectionView and not on the original non-sorted collection?

Was it helpful?

Solution

Why not create a generic IComparer<T> and use the Enumerable.OrderBy<TSource, TKey>(IEnumerable<TSource>, Func<TSource, TKey>, IComparer<TKey>) extension method before you create the collection view.

So you end up with something like:

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);
    }
}

Then your collection view is already sorted and filtering can be applied.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top