How to apply more than one filter

Today I will explain how you can apply more than one filter to a data bound view of a collection.
I showed in my last post the two ways of filtering. CollectionViewSource allows us to filter by attaching an event handler to the Filter event (of type FilterEventHandler). You may be wondering what happens if we attach more than one filter event handler to the same CollectionViewSource’s Filter event. It turns out that each of the event handlers is called in the sequence they were attached, once for each item. This allows each handler to override whatever filtering decision was made in the previous ones.
The good news is that within any event handler, you have access to the filtering decision made by the previous handlers. You can use this information to decide whether to filter the current item. I will show you how to do this next.
In my XAML file, I created an instance of an ObservableCollection with items of type AsterixCharacter, which contains two properties: one with the name of the character, and another one with its hair color. I then created a CollectionViewSource whose Source property points to this collection and bound the ListBox to it.
<Window.Resources>
<local:AsterixCharacters x:Key="asterix"/>
<CollectionViewSource Source="{StaticResource asterix}" x:Key="cvs"/>
<DataTemplate x:Key="characterTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Width="150" Text="{Binding Path=Name}" />
<TextBlock Text="{Binding Path=Hair}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<StackPanel Margin="10" Width="200" >
<CheckBox Content="Filter out A" Checked="AddAFilter" Unchecked="RemoveAFilter" Margin="5"/>
<CheckBox Content="Filter out white hair" Checked="AddWhiteHairFilter" Unchecked="RemoveWhiteHairFilter" Margin="5"/>
<ListBox ItemsSource="{Binding Source={StaticResource cvs}}" ItemTemplate="{StaticResource characterTemplate}" Margin="5"/>
</StackPanel>
When the first check box is checked, I want to filter out all Asterix characters whose names start with A. Likewise, when the second check box is checked, I want to filter out all characters with white hair. It is important to me that these two filter conditions are in different handlers (I could be reusing one of the handlers in another scenario, for example). Here is the code where I add and remove the filter event handler associated with the first check box:
CollectionViewSource cvs;
public Window1()
{
InitializeComponent();
cvs = (CollectionViewSource)(this.Resources["cvs"]);
}
private void AddAFilter(object sender, RoutedEventArgs e)
{
cvs.Filter += new FilterEventHandler(FilterOutA);
}
private void RemoveAFilter(object sender, RoutedEventArgs e)
{
cvs.Filter -= new FilterEventHandler(FilterOutA);
}
In order for the filter event handlers to work properly without interfering with each other, I made sure they only set the Accepted property of the FilterEventArgs to false when necessary to filter an item out. I never set the Accepted property to true, since that might override another filter’s decision to keep an item out of the view. Here is my implementation:
private void FilterOutA(object sender, FilterEventArgs e)
{
AsterixCharacter character = e.Item as AsterixCharacter;
if ((character == null) || character.Name.StartsWith("A"))
{
e.Accepted = false;
}
}
private void FilterOutWhiteHair(object sender, FilterEventArgs e)
{
AsterixCharacter character = e.Item as AsterixCharacter;
if ((character == null) || (character.Hair == HairColor.White))
{
e.Accepted = false;
}
}
Here is a screenshot of the completed sample:
Here you can find the VS project with this sample code. This works with July CTP WPF bits.
shao
shao said…
Hi,
I’ve been trying to create filters for a scattered plot graph where the data is binding to a xml file. The problem I’m encountering is that when I create my CollectionViewSource, it looks like this:
[CollectionViewSource x:Key="DataView" Source="{Binding Source={StaticResource tableDS}, XPath=/table/row/column.5}" /]
Since I need column.5 is the one I need to filter out information from to determine whether this node should appear on the graph or not, I am passing the XPath /table/row/column.5, however, I need other columns to construct my ellipses in my graph. For example, the size of each of the ellipse is based on column.4 and its coordinates are based on column.2 and column.3.
This is my DataTemplate:
[DataTemplate x:Key="rowTemplate" ]
[Ellipse Stroke="#FF979798" Width="{Binding Converter={StaticResource convertSize}, XPath=column.4}" Height="{Binding Converter={StaticResource convertSize}, XPath=column.4}" RenderTransformOrigin="0.282,0.316" ]
[Ellipse.Fill]
[RadialGradientBrush GradientOrigin="0.307,0.241"]
[GradientStop Color="#FFFFFFFF" Offset="0"/]
[GradientStop Color="#FF8983AA" Offset="1"/]
[/RadialGradientBrush]
[/Ellipse.Fill]
[Ellipse.ToolTip]
[StackPanel Width="250" TextBlock.FontSize="12"]
[TextBlock FontWeight="Bold" Text="{Binding XPath=column.0}" /]
[StackPanel Orientation="Horizontal"]
[TextBlock Text="Velocity: " /]
[TextBlock Text="{Binding XPath=column.2}" /]
[TextBlock Text=" Change: " /]
[TextBlock Text="{Binding XPath=column.3}" /]
[/StackPanel]
[TextBlock Text="{Binding XPath=column.5}" TextWrapping="Wrap"/]
[/StackPanel]
[/Ellipse.ToolTip]
[/Ellipse]
[/DataTemplate]
And this is my ItemsControl:
[ItemsControl ItemTemplate="{DynamicResource rowTemplate}" ItemsSource="{Binding Source={StaticResource DataView}, XPath=/table/row }" Margin="8,-285,8,0" VerticalAlignment="Top" Height="420.952"]
[ItemsControl.ItemsPanel]
[ItemsPanelTemplate]
[Canvas Width="600" Height="440" /]
[/ItemsPanelTemplate]
[/ItemsControl.ItemsPanel]
[ItemsControl.ItemContainerStyle]
[Style TargetType="{x:Type ContentPresenter}" ]
[Setter Property="Canvas.Left" Value="{Binding Converter={StaticResource convertOrbit}, XPath=column.2}"/]
[Setter Property="Canvas.Bottom" Value="{Binding Converter={StaticResource convertHeight}, XPath=column.3}"/]
[/Style]
[/ItemsControl.ItemContainerStyle]
[/ItemsControl]
I thought about passing just /table/row in my CollectionViewSource, but it simply passes the whole thing and leaves me with no way to navigate through it. The Select method from XPathNavigator class simply doesn’t work here..
Below is the code I’m using:
private void ShowOnlyTechnologyFilter(object sender, FilterEventArgs e)
{
XmlElement elem = e.Item as XmlElement;
XPathNavigator nav = elem.CreateNavigator();
XPathNodeIterator nodes = nav.Select(“/table/row/column.5″);
if (nodes != null)
{
if (nodes.Current.Value.ToString().Equals(“Technology”))
{
e.Accepted = true;
}
else
{
e.Accepted = false;
}
}
}
private void TechnologyFilter_checked(object sender, RoutedEventArgs e)
{
DataView.Filter += new FilterEventHandler(ShowOnlyTechnologyFilter);
}
private void TechnologyFilter_Unchecked(object sender, RoutedEventArgs e)
{
DataView.Filter -= new FilterEventHandler(ShowOnlyTechnologyFilter);
}
Can you please give me some suggestions on how I should create this filter?
Thanks,
Susan
September 5, 2007 at 7:44 am
Bea
Hi Susan,
Have you tried using a MultiBinding in the Source property of the CollectionViewSource? Within that MultiBinding, you can have a Binding per column that you’re interested in. You will need to write a MultiBinding Converter that takes all the different data items you bind to as parameters. As the return value, I would probably create a custom object with a property for each of the data items you care about. You will also need to change your DataTemplate so that it understands the custom object you returned from the MultiBinding Converter.
Let me know if this makes sense.
Bea
September 18, 2007 at 6:40 pm