Introduction
In my last article, I presented a small framework that can be used with the Microsoft Silverlight DataGrid
if you want to provide auto-filter support for your grid columns. But, there is one thing left to do, and that is getting the framework ready to work with the WPF DataGrid
provided by Microsoft. The source code and binaries for the WPFToolkit library can be found here, you will need it as the projects reference it. Since the changes made for WPF are minor, I am not going to go over it again, so I would ask you to read the previous article to get a view on how everything is designed and it works.
Code Design
Event though the Silverlight article presents all the code design, I will like to focus a little bit on the FilteredCollectionView
I have written. This collection class implements ICollectionView
and INotifyCollectionChanged
, and takes an IList<T>
where T
is INotifyPropertyChanged
. For each item in the list or any added one (in case an ObservableCollection<T>
is passed in), it will create a CollectionItem
instance which will be stored in an OrderedSet
.
private class CollectionItem
{
private readonly int _originalIndex;
private int _orderedIndex = -1;
public CollectionItem(int originalIndex)
{
_originalIndex = originalIndex;
}
public int OriginalIndex
{
get { return _originalIndex; }
}
public int OrderedIndex
{
get { return _orderedIndex; }
set { _orderedIndex = value; }
}
}
The first private member is the index to the original collection whereas the second one is the index in the OrderedSet
. I will come back to why I am storing these two indexes and why not one is enough.
The OrderedSet
is implemented using a red-black tree (I have used the PowerCollections library). Since this article doesn't focus on the data structure, please have a read on the Internet if you are interested in finding out more on what a red-black tree is. In order to place the items in the tree, the set makes use of a comparer, and here is where the first problem might occur if you let the default one used internally by the OrderedSet
. Say, T
is a IComparable
, and you have two different items in the collection that are equal but not reference equal. The set will not allow duplicated items, hence out of the two items, your collection will only have one. That is the reason why I have implemented my own comparer to make use of the item initial source collection index:
private class LocalCollectionComparer<T> : IComparer<CollectionItem>
where T : INotifyPropertyChanged
{
private FilteredCollectionView<T> _filteredCollection;
internal LocalCollectionComparer(FilteredCollectionView<T> filteredCollection)
{
if (filteredCollection == null)
{
throw new ArgumentNullException("collection");
}
_filteredCollection = filteredCollection;
}
#region IComparer<int> Members
public int Compare(CollectionItem x, CollectionItem y)
{
if (x.OrderedIndex != -1 && y.OrderedIndex != -1)
{
return x.OrderedIndex.CompareTo(y.OrderedIndex);
}
if (!_filteredCollection._isRemoving && _filteredCollection._comparer != null)
{
int compare = _filteredCollection._comparer.Compare(
_filteredCollection._collection[x.OriginalIndex],
_filteredCollection._collection[y.OriginalIndex]);
if (compare == 0)
{
return x.OriginalIndex.CompareTo(y.OriginalIndex);
}
return compare;
}
return x.OriginalIndex.CompareTo(y.OriginalIndex);
}
#endregion
}
But, using just the initial collection index is not enough when you have a sorting applied. Once you have a sorted description added to the collection view, while removing an item (say, which does not meet the filter criteria), it might not be possible, because of the internal layout of the tree, to find the item as it used the original source collection index for comparison.
As I mentioned about the sort description scenario, I will mention about the comparer used in that case: FilteredSortComparer
. Whenever the SortDescription
changes and the collection is not empty, a new instance of FilteredSortComparer
is created, and the collection view is refreshed to accommodate the changes:
((INotifyCollectionChanged)_sortDescription).CollectionChanged += (sender, e) =>
{
if (_sortDescription.Count > 0)
{
_comparer = new FilteredSortComparer<T>(_sortDescription);
}
else
{
_comparer = null;
}
Refresh();
};
For every SortDescription
available, the comparer will create the IComparer
based on the property type involved, and will create an internal class ComparerInfo
. Whenever the comparer is invoked, it will iterate the ComparerInfo
list, calling each comparer created on the initialization stage:
public int Compare(T x, T y)
{
if (x == null)
{
if (y == null)
{
return 0;
}
return -1;
}
if (y == null)
{
return 1;
}
for (int i = 0; i < _comparers.Count; ++i)
{
ComparerInfo ci = _comparers[i];
int comparison =
ci.Comparer.Compare(ci.GetValue(x), ci.GetValue(y));
if (comparison != 0)
{
if (!ci.IsAscending)
{
comparison = -comparison;
}
return comparison;
}
}
return 0;
}
Using the Code
The first thing you have to do to get this working is defining the ContentTemplate
for the DataGridColumnHeader
. Within the source code, you can find an example. Here is how the template looks:
xmlns:stepi="clr-namespace:Stepi.UIFilters;assembly=Stepi.UIFilters"
xmlns:dg="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
xmlns:primitives="clr-namespace:Microsoft.Windows.Controls.Primitives;assembly=WPFToolkit"
…
<Style TargetType="primitives:DataGridColumnHeader" x:Key="columnHeaderStyle" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type primitives:DataGridColumnHeader}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<dg:DataGridHeaderBorder
SortDirection="{TemplateBinding SortDirection}"
IsHovered="{TemplateBinding IsMouseOver}"
IsPressed="{TemplateBinding IsPressed}"
IsClickable="{TemplateBinding CanUserSort}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding ="{TemplateBinding Padding}"
SeparatorVisibility="{TemplateBinding SeparatorVisibility}"
SeparatorBrush="{TemplateBinding SeparatorBrush}"
Grid.ColumnSpan="2"/>
<ContentPresenter Margin="2"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
<Thumb x:Name="PART_LeftHeaderGripper" HorizontalAlignment="Left"
Style="{StaticResource ColumnHeaderGripperStyle}"/>
<Thumb x:Name="PART_RightHeaderGripper"
HorizontalAlignment="Right" Grid.Column="1"
Style="{StaticResource ColumnHeaderGripperStyle}"/>
-->
<stepi:DataGridColumnFilter Grid.Row="0" Grid.Column="1"
Background="Transparent"
Width="Auto" Height="Auto"
Margin="4,1,4,1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="DisplayIndex" Value="0">
<Setter Property="Visibility" Value="Collapsed"
TargetName="PART_LeftHeaderGripper"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
If you want to control the list of IFilterViewInitializer
s available, you can define a local resource and then bind the InitalizersManager
property of the DataGridColumnFilter
to it.
<stepi:FilterViewInitializersManager x:Key="filtersViewInitializersManager"/>
The last step is to set up the datagrid column header style to be the one defined above:
<dg:DataGrid x:Name="dg" ColumnHeaderStyle="{StaticResource columnHeaderStyle}">
History
- 27 Jan. 2009 - Version 1.0.0.