In WPF can you filter a CollectionViewSource without code behind?

我只是一个虾纸丫 提交于 2019-11-26 20:47:40

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)

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>)

WPF automatically creates a CollectionView--or derived type such as ListCollectionView, BindingListCollectionView, etc. (it depends on the capabilities detected on your source collection)--for any ItemsSource binding, if you don't supply one when you bind your IEnumerable-derived source directly to an ItemsControl.ItemsSource property.

This automatically-supplied CollectionView instance is created and maintained by the system on a per collection basis (note: not per-UI control or binding target). In other words, there will be exactly one globally-shared "Default" view for each s̲o̲u̲r̲c̲e̲ that you bind to, and this unique CollectionView instance can be retrieved (or created on demand) at any time by passing the IEnumerable to the static method CollectionViewSource.GetDefaultView().

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).

In any case, 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 its "Default" CollectionView, 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 you didn't provide any view at all--will all share the 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 an ItemsControl (as a binding target), you can also "simultaneously" access the effective collection of applied filter/sort results--exposed as a CollectionView-derived instance of System.Windows.Controls.ItemCollection--by binding from the Control's Items property (as a binding source).

This enables numerous simplified XAML scenarios:

  1. If having a single, globally-shared filter/sort capability for the given IEnumerable source is sufficient for your app, then just bind directly to ItemsSource. Still in XAML only, you can then filter/sort the items by treating the Items property on the same Control as an ItemCollection 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 source IEnumerable in this way.   --or--

  2. 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 the ItemsControl.Items property (as a binding source) in order to access the effective CollectionView.

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 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 trouble of otherwise mentioning a 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 more utilitarian to bind to the view promulgated by the UI, since such is likely better attuned to the capabilities of both the data source and its UI control target.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!