Saturday, July 24, 2010

Using ICollectionView for Sorting and Filtering

WPF allows us to great support for manipulating the display which is data bound to different collections. Most of these collections support ICollectionView functionality. In this discussion, we will discuss how we can use ICollectionView for filtering and sorting our collection. It must be remembered that ICollectionView also supports grouping.

In this example, we want to create a window as follows:


So we have a list box showing the list of names students. We want to provide the functionality to search within the list based on the user input in the Search text box on top left of the window. We also want to provide sorting functionality using the "Toggle Sort" on top right. When the form opens the list should be sorted in ascending order of names of students.

Let's start by creating a Student class. This holds the name of student and his ID.
class Student
{
public int StudentID { get; set; }
public string StudentName { get; set; }
}

First we create the view for the above in XAML.
<Window x:Class="ICollectionViewExampleCS.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ICollectionViewExampleCS"
Title="Window1" Height="356" Width="616" DataContext="{DynamicResource ViewModel}">
<Window.Resources>
<local:Window1ViewModel x:Key="ViewModel" x:Name="_vm" />
</Window.Resources>
<Grid>
<ListBox Margin="12,41,7,7" Name="listBox1" ItemsSource="{Binding StudentList}" DisplayMemberPath="StudentName" />
<TextBox Height="23" x:Name="txtSearchPattern" Text="{Binding Path = SearchPattern, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="12,12,0,0" VerticalAlignment="Top" Width="118" />
<Button Height="23" HorizontalAlignment="Right" Margin="0,12,7,0" Name="button1" VerticalAlignment="Top" Width="74" Click="button1_Click">Toggle Sort</Button>
</Grid>
</Window>

As you can see, we are using ViewModel as the DataContext of the window (Window tag) using dynamic resource extension. The definition of ViewModel is provided in Window.Resources section as an instance of Window1ViewModel class. We are binding the Search textbox with SearchPattern property from the DataContext. The UpdateSourceTrigger for this binding is set as PropertyChanged. So as the user would be typing and deleting the text, it would automatically update itself in the datacontext. The Click event of ToggleSort button has been provided with a button1_Click handler. This handler should use the sort functionality from the DataContext. The list box is bound with StudentList property from the datacontext. This should be a collection. We are displaying StudentName from the individual item of this list.

The code behind of this window is as follows:
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}

private void button1_Click(object sender, RoutedEventArgs e)
{
((Window1ViewModel)this.Resources["ViewModel"]).sortStudents();
}
}

Here we have provided the definition of button1_Click handler. It is calling sortStudents method from the DataContext. Being a big fan of MVVM, I don't recommend calling the DataContext events like this but in order to keep the example simple I am making this call.

Now we have the idea of everything we would be requiring from Window1ViewModel. It should be providing two properties StudentList and SearchPattern. The StudentList should be a collection of students. It must have a StudentName field. Additionally SearchPattern should filter StudentList based on updates in its value.
internal class Window1ViewModel : DependencyObject
{
public ObservableCollection StudentList { get; private set; }

public Window1ViewModel()
{
StudentList = new ObservableCollection();
addStudents();
sortStudents();
SearchPatternDescriptor.AddValueChanged(this, (o, e) => filterStudents());
}

private void addStudents()
{
StudentList.Add(new Student() { StudentID = 1, StudentName = "Muhammad" });
StudentList.Add(new Student() { StudentID = 2, StudentName = "Michael" });
StudentList.Add(new Student() { StudentID = 3, StudentName = "Shoaib" });
StudentList.Add(new Student() { StudentID = 4, StudentName = "Shahbaz" });
StudentList.Add(new Student() { StudentID = 5, StudentName = "Maria" });
StudentList.Add(new Student() { StudentID = 6, StudentName = "Mehreen" });
StudentList.Add(new Student() { StudentID = 7, StudentName = "Imran" });
StudentList.Add(new Student() { StudentID = 8, StudentName = "Naveed" });
StudentList.Add(new Student() { StudentID = 9, StudentName = "Muhammad" });
StudentList.Add(new Student() { StudentID = 10, StudentName = "Mustafa" });
StudentList.Add(new Student() { StudentID = 11, StudentName = "Asad" });
}

public static DependencyProperty SearchPatternProperty =
DependencyProperty.Register("SearchPattern", typeof(string), typeof(Window1ViewModel));
public string SearchPattern
{
get
{
return (string)GetValue(SearchPatternProperty);
}
set
{
SetValue(SearchPatternProperty, value);
}
}
DependencyPropertyDescriptor SearchPatternDescriptor =
DependencyPropertyDescriptor.FromProperty(SearchPatternProperty, typeof(Window1ViewModel));

public void filterStudents()
{
ICollectionView view = CollectionViewSource.GetDefaultView(this.StudentList);
if (string.IsNullOrEmpty(SearchPattern))
view.Filter = null;
else
view.Filter = new Predicate<object>((o) => ((Student)o).StudentName.StartsWith(SearchPattern));
}

public void sortStudents()
{
ICollectionView view = CollectionViewSource.GetDefaultView(this.StudentList);

if (view.SortDescriptions.Count > 0)
{
ListSortDirection direction = view.SortDescriptions[0].Direction ==
ListSortDirection.Ascending ? ListSortDirection.Descending : ListSortDirection.Ascending;
view.SortDescriptions.Clear();
view.SortDescriptions.Add(new SortDescription("StudentName", direction));
}
else
view.SortDescriptions.Add(new SortDescription("StudentName", ListSortDirection.Ascending));
}

}

For keeping a track of updates in SearchPattern property, we needed to define a DependencyPropertyDescriptor. We named it as SearchPatternDescriptor. We have provided a lambda expression for its delegate which is just calling filterStudents method.

In the filterStudents() method, we are using ICollectionView to filter the StudentList. ICollectionView supports filtering using its Filter property. We can specify a predicate for this property. The collection is filtered by this predicate then. If SearchPattern is empty, then we are removing all the Filter, otherwise, we are updating the filter with the new Predicate.

For sorting, as expected by the view, we have provided the definition of sortStudents() method. This also uses ICollectionView for sorting. ICollectionView supports sorting by adding SortDescription to its SortDescriptions collection. Each SortDescription can have ascending or descending order. In our example, we are checking for sort direction. We are clearing all sort descriptions and providing new SorDescription with the opposite direction.

2 comments:

sara d said...

Nice demo i liked much realy thanks for your help and time
I have a question if this list was a rad treeview and it has children how would the code be changed?

Muhammad Shujaat Siddiqi said...

Thanks for your comments.

I haven't used Rad TreeView yet.