Reputation: 1792
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 (string
s) 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):
because they are the first 5 items that contain the letter 'e' when sorted alphabetically. However I get the following items in my application:
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?
Upvotes: 1
Views: 4653
Reputation: 19416
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.
Upvotes: 1