In WPF can you filter a CollectionViewSource without code behind?
You can do pretty much anything in XAML if you "try hard enough", up to writing whole programs in it.
You will never get around code behind (well, if you use libraries you don't have to write any but the application still relies on it of course), here's an example of what you can do in this specific case:
<CollectionViewSource x:Key="Filtered" Source="{Binding DpData}"
xmlns:me="clr-namespace:Test.MarkupExtensions">
<CollectionViewSource.Filter>
<me:Filter>
<me:PropertyFilter PropertyName="Name" Value="Skeet" />
</me:Filter>
</CollectionViewSource.Filter>
</CollectionViewSource>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using System.Windows.Data;
using System.Collections.ObjectModel;
using System.Windows;
using System.Text.RegularExpressions;
namespace Test.MarkupExtensions
{
[ContentProperty("Filters")]
class FilterExtension : MarkupExtension
{
private readonly Collection<IFilter> _filters = new Collection<IFilter>();
public ICollection<IFilter> Filters { get { return _filters; } }
public override object ProvideValue(IServiceProvider serviceProvider)
{
return new FilterEventHandler((s, e) =>
{
foreach (var filter in Filters)
{
var res = filter.Filter(e.Item);
if (!res)
{
e.Accepted = false;
return;
}
}
e.Accepted = true;
});
}
}
public interface IFilter
{
bool Filter(object item);
}
// Sketchy Example Filter
public class PropertyFilter : DependencyObject, IFilter
{
public static readonly DependencyProperty PropertyNameProperty =
DependencyProperty.Register("PropertyName", typeof(string), typeof(PropertyFilter), new UIPropertyMetadata(null));
public string PropertyName
{
get { return (string)GetValue(PropertyNameProperty); }
set { SetValue(PropertyNameProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object), typeof(PropertyFilter), new UIPropertyMetadata(null));
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty RegexPatternProperty =
DependencyProperty.Register("RegexPattern", typeof(string), typeof(PropertyFilter), new UIPropertyMetadata(null));
public string RegexPattern
{
get { return (string)GetValue(RegexPatternProperty); }
set { SetValue(RegexPatternProperty, value); }
}
public bool Filter(object item)
{
var type = item.GetType();
var itemValue = type.GetProperty(PropertyName).GetValue(item, null);
if (RegexPattern == null)
{
return (object.Equals(itemValue, Value));
}
else
{
if (itemValue is string == false)
{
throw new Exception("Cannot match non-string with regex.");
}
else
{
return Regex.Match((string)itemValue, RegexPattern).Success;
}
}
}
}
}
Markup extensions are your friend if you want to do something in XAML.
(You might want to spell out the name of the extension, i.e. me:FilterExtension
as the on-the-fly checking in Visual Studio may complain without reason, it still compiles and runs of course but the warnings might be annoying.
Also do not expect the CollectionViewSource.Filter
to show up in the IntelliSense, it does not expect you to set that handler via XML-element-notation)
WPF automatically creates a CollectionView
—or one of its derived types such as ListCollectionView
, or BindingListCollectionView
—whenever you bind any IEnumerable
-derived source data to an ItemsControl.ItemsSource
property. Which type of CollectionView
you get depends on the capabilities detected at runtime on the data source you provide.
Sometimes even if you try to explicitly bind your own specific CollectionView
-derived type to an ItemsSource
, the WPF data binding engine may wrap it (using the internal type CollectionViewProxy
).
The automatically-supplied CollectionView
instance is created and maintained by the system on a per collection basis (note: not per- UI control or per- bound target). In other words, there will be exactly one globally-shared "Default" view for each s̲o̲u̲r̲c̲e̲ collection that you bind to, and this unique CollectionView
instance can be retrieved (or created on demand) at any time by passing the same "original" IEnumerable
instance back to the static method CollectionViewSource.GetDefaultView()
again.
CollectionView
is a shim that is able to keep track of the sorting and/or filtering state without actually altering the source. Therefore, if the same source data is referenced by several different Binding
usages each with a different CollectionView
, they won't interfere with each other. The "Default" view is intended to optimize the very common--and much simpler--situations where filtering and sorting are not required or expected.
In short, every ItemsControl
with a data-bound ItemsSource
property will always end up with sorting and filtering capabilities, courtesy of some prevailing CollectionView
. You can easily perform filtering/sorting for any given IEnumerable
by grabbing and manipulating the "Default" CollectionView
from the ItemsControl.Items
property, but note that all the data-bound targets in the UI that end up using that view--either because you explicitly bound to CollectionViewSource.GetDefaultView()
, or because your source wasn't a CollectionView
at all--will all share those same sorting/filtering effects.
What's not often mentioned on this subject is, in addition to binding the source collection to the
ItemsSource
property of anItemsControl
(as a binding target), you can also "simultaneously" access the effective collection of applied filter/sort results--exposed as aCollectionView
-derived instance ofSystem.Windows.Controls.ItemCollection
--by binding from the Control'sItems
property (as a binding source).
This enables numerous simplified XAML scenarios:
If having a single, globally-shared filter/sort capability for the given
IEnumerable
source is sufficient for your app, then just bind directly toItemsSource
. Still in XAML only, you can then filter/sort the items by treating theItems
property on the same Control as anItemCollection
binding source. It has many useful bindable properties for controlling the filter/sort. As noted, filtering/sorting will be shared amongst all UI elements which are bound to the same sourceIEnumerable
in this way. --or--Create and apply one or more distinct (non-"Default")
CollectionView
instances yourself. This allows each data-bound target to have independent filter/sort settings. This can also be done in XAML, and/or you can create your own(List)CollectionView
-derived classes. This type of approach is well-covered elsewhere, but what I wanted to point out here is that in many cases the XAML can be simplified by using the same technique of data-binding to theItemsControl.Items
property (as a binding source) in order to access the effectiveCollectionView
.
Summary:
With XAML alone, you can data-bind to a collection representing the effective results of any current CollectionView
filtering/sorting on a WPF ItemsControl
by treating its Items
property as a read-only binding source. This will be a System.Windows.Controls.ItemCollection
which exposes bindable/mutable properties for controlling the active filter and sort criteria.
[edit] - further thoughts:
Note that in the simple case of binding your IEnumerable
directly to ItemsSource
, the ItemCollection
you can bind to at ItemsControl.Items
will be a wrapper on the original collection's CollectionViewSource.GetDefaultView()
. As discussed above, in the case of XAML usage it's a no-brainer to bind to this UI wrapper (via ItemsControl.Items
), as opposed to binding to the underlying view it wraps (via CollectionViewSource.GetDefaultView
), since the former approach saves you the (in XAML, awkward) trouble of having to explicitly mention any CollectionView
at all.
But further, because that ItemCollection
wraps the default CollectionView
, it seems to me that, even in code-behind (where the choice is less obvious) it's perhaps also more utilitarian to bind to the view promulgated by the UI, since such is best attuned to the de-facto runtime capabilities of both the data source and its UI control target.
Actually you don't even need access to the CollectionViewSource
instance, you can filter the source collection directly in the ViewModel:
ICollectionView view = CollectionViewSource.GetDefaultView(collection);
view.Filter = predicate;
(note that ICollectionView.Filter
is not an event like CollectionViewSource.Filter
, it's a property of type Predicate<object>
)