If you have been working with WPF for a long time, you might have already have come across ICollectionView
. It is the primary Data object for any WPF list controls (like ComboBox
, ListBox
, ListView
, etc.) that allows flexibilities like Sorting, Filtering, Grouping, Current Record Management, etc. Thus, it ensures that all the related information like filtering, sorting, etc. is decoupled from the actual control. It has been very popular to those working with data object because of inbuilt support for all WPF List controls. So I thought I would consider describing it a bit so that people might easily plug in the same to their own solution.
What is a CollectionView?
It is a layer that runs over the Data Objects which allow you to define rules for Sorting, Filtering, Grouping, etc. and manipulate the display of data rather than modifying the actual data objects. Therefore in other words, a CollectionView
is a class which takes care of the View
totally and giving us the capability to handle certain features incorporated within it.
How to Get a CollectionView?
Practically speaking, getting a CollectionView
from an Enumerable
is the most easiest thing I have ever seen in WPF. You only need to pass the Enumerable
to CollectionViewSource.GetDefaultView
. Thus, rather than defining:
this.ListboxControl.ItemsSource = this.Source;
you need to write:
this.ListboxControl.ItemsSource = CollectionViewSource.GetDefaultView(this.Source);
The List
will get the CollectionView
it requires.
So the CollectionView
actually separates the View object List Control with the actual DataSource
and hence gives an interface
to manipulate the data before reflecting to the View
objects. Now let us look at how to implement the basic features for the ICollectionView
.
Sorting
Sorting can be applied to the CollectionView
in a very easy way. You need to add a SortDescription
to the CollectionView
. The CollectionView
actually maintains a stack of SortDescription
objects, each of them being a Structure
can hold information of a Column and the Direction of Sorting. You can add them in the collectionView
to get the desired output.
Say, I store the CollectionView
as a property:
ICollectionView Source { get; set; }
Now if you want to sort the existing collection:
this.Source.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Descending));
Hence the CollectionView
will be sorted based on Name
and in Descending order.
Note: The default behavior of CollectionView
automatically refreshes when a new SortDescription
is added to it. For performance issue, you might use DeferRefresh()
if you want to refresh only once to add SortDescription
more than once.
Grouping
You can create custom group for ICollectionView
in the same way as you do for sorting. To create Group
of elements, you need to use GroupStyle
to define the Template
for the Group
and to show the name of the Group
in Group Header.
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
Here, we define the Group HeaderTemplate
for each group so that the TextBlock
shows the name of the Group
item by which the grouping is made. You can specify more than one Grouping information for a single collection. To group a collection, you need to use:
this.Source.GroupDescriptions.Add(new PropertyGroupDescription("Department"));
Note: Grouping turns off virtualization. So if you are dealing with a large amount of data, Grouping may lead to performance issue.
You can apply custom grouping as well by defining IValueConverter
in PropertyGroupDescription
as well.
Filtering
Filtering requires a delegate (Predicate) based on which the filter will occur. The Predicate takes in the item and based on the value true
or false
it returns, it selects or unselect an element.
this.Source.Filter = item =>
{
ViewItem vitem = item as ViewItem;
if (vitem == null) return false;
return vitem.Name.Contains("A");
};
This will select only the elements which have A
in their names.
Current Record Manipulation
ICollectionView
also allows to synchronize items with the Current position of the element in the CollectionView
. Each ItemsControl
which is the base class of any ListControl
in WPF exposes a property called IsSynchronizedWithCurrentItem
when set to true
will automatically keep the current position of the CollectionView
in sync.
There are methods like:
this.Source.MoveCurrentToFirst();
this.Source.MoveCurrentToPrevious();
this.Source.MoveCurrentToNext();
this.Source.MoveCurrentToLast();
These allows you to navigate around the CurrentItem
of the CollectionView
. You can also use CurrentChanged
event to intercept your selection logic around the object.
Sample Application
To demonstrate all the features, I have created a demo application which allows you to Sort, Filter, Group and navigate between data objects. Let's see how it works:
The application contains a ListView
with a little data in it. The header is created using GridView
which can be clicked and based on which the items will sort.
<ListView ItemsSource="{Binding}" x:Name="lvItems"
GridViewColumnHeader.Click="ListView_Click"
IsSynchronizedWithCurrentItem="True" Grid.Row="1">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
<ListView.View>
<GridView AllowsColumnReorder="True">
<GridViewColumn Header="Id"
DisplayMemberBinding="{Binding Id}" />
<GridViewColumn Header="Name"
DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Developer">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Developer}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Salary">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Salary}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
In the above image, it is shown how the items automatically gets sorted when the header is clicked. To handle this, I have used GridViewColumnHeader.Click
which allows you to get control over the column in which it is clicked.
private void ListView_Click(object sender, RoutedEventArgs e)
{
GridViewColumnHeader currentHeader = e.OriginalSource as GridViewColumnHeader;
if(currentHeader != null && currentHeader.Role != GridViewColumnHeaderRole.Padding)
{
using (this.Source.DeferRefresh())
{
Func<SortDescription, bool> lamda = item =>
item.PropertyName.Equals(currentHeader.Column.Header.ToString());
if (this.Source.SortDescriptions.Count(lamda) > 0)
{
SortDescription currentSortDescription =
this.Source.SortDescriptions.First(lamda);
ListSortDirection sortDescription = currentSortDescription.Direction ==
ListSortDirection.Ascending ? ListSortDirection.Descending :
ListSortDirection.Ascending;
currentHeader.Column.HeaderTemplate =
currentSortDescription.Direction == ListSortDirection.Ascending ?
this.Resources["HeaderTemplateArrowDown"]
as DataTemplate : this.Resources["HeaderTemplateArrowUp"] as DataTemplate;
this.Source.SortDescriptions.Remove(currentSortDescription);
this.Source.SortDescriptions.Insert(0, new SortDescription
(currentHeader.Column.Header.ToString(), sortDescription));
}
else
this.Source.SortDescriptions.Add(new SortDescription
(currentHeader.Column.Header.ToString(), ListSortDirection.Ascending));
}
}
}
In the above code, I need to handle the Sorting gracefully so that we always remove the current Sorting before we insert a new sort.
The FilterBy
section allows you to handle Filtering. You can select the ColumnName
from the ComboBox
and then apply the selection criteria for the column. To do this, I have used FilterButton Click
command.
this.Source.Filter = item =>
{
ViewItem vitem = item as ViewItem;
if (vitem == null) return false;
PropertyInfo info = item.GetType().GetProperty(cmbProperty.Text);
if (info == null) return false;
return info.GetValue(vitem,null).ToString().Contains(txtFilter.Text);
};
Hence, the predicate will be applied to the filter criteria.
You can use Grouping Section to group based on Column Name. Here, you can see that I have grouped items on Developer names. The applied group header will be shown in the Group Header section.
this.Source.GroupDescriptions.Clear();
PropertyInfo pinfo = typeof(ViewItem).GetProperty(cmbGroups.Text);
if (pinfo != null)
this.Source.GroupDescriptions.Add(new PropertyGroupDescription(pinfo.Name));
Navigation is handled using a sets of buttons which eventually calls the respective methods to automatically keep CollectionView
in sync with ListView
items.
Notice: I have used reflection to get PropertyInfo
for many cases. If you don't want to use, you might also do this statically. I have used reflection only to handle this dynamically.