In WPF can you filter a CollectionViewSource without code behind?
Asked Answered
T

4

18

Really the subject says it all.

<CollectionViewSource x:Key="MyData"
    Source="{Binding}" Filter="{ SomethingMagicInXaml? }" />

It's not that I can't have code behind. It just nags at me.

Thready answered 23/6, 2011 at 23:31 Comment(0)
A
26

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)

Armanda answered 24/6, 2011 at 1:5 Comment(21)
Did you actually test it? Last time I tried, it wasn't possible to use markup extension on an event... but perhaps it has changed in 4.0Cheerless
That's neat, I wasn't aware of this new feature!Cheerless
@H.B. I get an exception : Filters does not support values of Type PropertyFilter.Triton
@Vishal: does it compile?Armanda
@H.B. No, It does not compile.Triton
@Vishal: Did you properly set the ContentProperty attribute?Armanda
@H.B. I just copy-pasted your code and xaml. Also made the namespce changes according to my project.Triton
@H.B. I also get an error saying : The type me:Filter was not found. Verify that.........Triton
@H.B. The error about PropertyFilter is gone automatically. Now, I got a new error : Only Public or internal classes can be used within the Markup. FilterExtension type is not Public or internal.Triton
@Vishal: Make it public then? (Though it should be internal by default, which won't help you if you try to use it from another assembly of course)Armanda
@H.B. I made the class public. In addition to that I changed the Filters proeprty to Collection<IFilter> instead of ICollection<IFilter>, added a setter to this property and removed readonly from _filters. Now it works fine. Thank you for helping me.Triton
@Vishal: Making the property settable should not be necessary if the type is one where the system can add items to the current instance. Well, if that helps in your case that's better than nothing.Armanda
@H.B. In my case If I set the value of Value Property of PropertyFilter to a static string it works fine. And when I change it to a binding to my ViewModel's Property of type string then it stops sorting. In short I am having a textBox and a DataGrid. When I type something in textbox I want to filter DataGrid.Triton
@Vishal: There probably is no binding context, do you know how to debug bindings? If you think you have done everything to research the problem and you are still stuck, then ask a separate question. I will not explain the binding system in this comments section.Armanda
@H.B. Ok thanks. I every time look at the binding errors in output window. If there is a better way then can you tell me?Triton
@Vishal: That's the most important part of it, you can only up the detail using TraceLevel which usually is not necessary. Well, you also need to understand what the various errors mean and what common causes are.Armanda
@H.B. OK, I will check that. Thanks for your valuable time and help.Triton
@H.B. I have posted a new question here : #27157660 . If you become free then please take a look at it.Triton
I think I might have found the culprit for not compiling, might have to do with public ICollection<IFilter> Filters instead of public Collection<IFilter> Filters, see my answerKesterson
There's no CollectionViewSource.FilterLove
@stigzler: There is, it's an event.Armanda
C
18

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

Cheerless answered 24/6, 2011 at 1:13 Comment(7)
While this may be correct and valuable information and all that, this strictly speaking does not answer the question that was asked i think.Armanda
@H.B., the title said "without code behind"; for me it usually means "in the ViewModel", but now I realize the OP specifically asked for a XAML solution...Cheerless
@Jerry Nixon, this is not code-behind; this is the code of a ViewModel. Unless, of course, you consider any C# code as code-behind...Cheerless
+1 because your answer is NOT code-behind, it is in the ViewModel.Ringleader
If I may, the ViewModel providing view with a view sounds like a Controller.Windrow
@XAMlMAX, it's not a view in the UI sense; a CollectionView is more like a view in the DB sense...Cheerless
I will leave my previous comment for other people who might want to ask similar question. thanks @ThomasLevesque.Windrow
E
8

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

Emancipate answered 5/6, 2018 at 11:4 Comment(2)
I Very much appreciate the concept discussion in a clear fashion. Thanks.Psychometrics
-1 from me since instead of much discussion, @Glenn Slayden should've provided some actual implementation, at least relevant details - after all, that's what this website is all about...Towns
K
1

I had the following issues with the accepted solution provided by H.B. using .NET FrameWork 4.6.1 (old, I know, but unfortunately a limitation for my current situation):

Severity Code Description
Error XDG0012 The member "Filter" is not recognized or is not accessible.
Error Cannot set content property 'Filters' on element 'FilterExtension'. 'Filters' has incorrect access level or its assembly does not allow access. Line xx Position yy.

This was easily resolved by changing

public ICollection<IFilter> Filters { get { return _filters; } }

to

public Collection<IFilter> Filters { get { return _filters; } }
Kesterson answered 28/3, 2022 at 14:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.