[WPF] Using Linq to shape data in a CollectionView

person Thomas Levesquefolder_openWPFlocal_offer, , access_time November 30, 2011

WPF provides a simple mechanism for shaping collections of data, via the ICollectionView interface and its Filter, SortDescriptions and GroupDescriptions properties:

// Collection to which the view is bound
public ObservableCollection People { get; private set; }
...

// Default view of the People collection
ICollectionView view = CollectionViewSource.GetDefaultView(People);

// Show only adults
view.Filter = o => ((Person)o).Age >= 18;

// Sort by last name and first name
view.SortDescriptions.Add(new SortDescription("LastName", ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("FirstName", ListSortDirection.Ascending));

// Group by country
view.GroupDescriptions.Add(new PropertyGroupDescription("Country"));

Even though this technique is not difficult to use, it has a few drawbacks:

  • The syntax is a bit clumsy and unnatural: the fact that the filter parameter is an object whereas we know it’s actually of type Person makes the code less readable because of the cast, and the specification of the sort and group descriptions is a little repetitive
  • Specifying the property names as strings introduces a risk of error, since they’re not verified by the compiler

In the last few years, we got used to use Linq to do this kind of things… it would be nice to be able to do the same for the shaping of an ICollectionView.

Let’s see what syntax we could use to do it with Linq… something like this perhaps?

People.Where(p => p.Age >= 18)
      .OrderBy(p => p.LastName)
      .ThenBy(p => p.FirstName)
      .GroupBy(p => p.Country);

Or, with the Linq query comprehension syntax:

from p in People
where p.Age >= 18
orderby p.LastName, p.FirstName
group p by p.Country;

Obviously, this is not enough: this code only creates a query on the collection, it doesn’t modify the CollectionView… but with just a little extra work, we can get the desired result:

var query =
    from p in People.ShapeView()
    where p.Age >= 18
    orderby p.LastName, p.FirstName
    group p by p.Country;

query.Apply();

The ShapeView method returns a wrapper which encapsulates the default view of the collection, and exposes Where, OrderBy and GroupBy methods with appropriate signatures to specify the shaping of the CollectionView. Creating the query has no direct effect, the changes are only applied to the view when Apply is called: that’s because it’s better to apply all changes at the same time, using ICollectionView.DeferRefresh, to avoid causing a refresh of the view for each clause of the query. When Apply is called, we can see that the view is correctly updated to reflect the query.

This solution allows to define the filter, sort and grouping in a strongly-typed way, which implies that the code is verified by the compiler. It’s also more concise and readable than the original code… Just be careful with one thing: some queries that are correct from the compiler’s point of view won’t be applicable to a CollectionView. For instance, if you try to group the data by the first letter of the last name (p.LastName.Substring(0, 1)), the GroupBy method will fail because only properties are supported by PropertyGroupDescription.

Note that the wrapper won’t overwrite the shaping properties of the CollectionView if you don’t specify the corresponding Linq clause, so you can just modify the current view without specifying everything again. If you need to clear the properties, you can use the ClearFilter, ClearSort and ClearGrouping methods:

// Remove the grouping and add a sort criteria
People.ShapeView()
      .ClearGrouping()
      .OrderBy(p => p.LastName);
      .Apply();

Note that as for a normal Linq query, it’s possible to use either the query comprehension syntax or to call the methods directly, since the former is just syntactic sugar for the latter.

Finally, here’s the complete code of the wrapper and the associated extension methods:

    public static class CollectionViewShaper
    {
        public static CollectionViewShaper<TSource> ShapeView<TSource>(this IEnumerable<TSource> source)
        {
            var view = CollectionViewSource.GetDefaultView(source);
            return new CollectionViewShaper<TSource>(view);
        }

        public static CollectionViewShaper<TSource> Shape<TSource>(this ICollectionView view)
        {
            return new CollectionViewShaper<TSource>(view);
        }
    }

    public class CollectionViewShaper<TSource>
    {
        private readonly ICollectionView _view;
        private Predicate<object> _filter;
        private readonly List<SortDescription> _sortDescriptions = new List<SortDescription>();
        private readonly List<GroupDescription> _groupDescriptions = new List<GroupDescription>();

        public CollectionViewShaper(ICollectionView view)
        {
            if (view == null)
                throw new ArgumentNullException("view");
            _view = view;
            _filter = view.Filter;
            _sortDescriptions = view.SortDescriptions.ToList();
            _groupDescriptions = view.GroupDescriptions.ToList();
        }

        public void Apply()
        {
            using (_view.DeferRefresh())
            {
                _view.Filter = _filter;
                _view.SortDescriptions.Clear();
                foreach (var s in _sortDescriptions)
                {
                    _view.SortDescriptions.Add(s);
                }
                _view.GroupDescriptions.Clear();
                foreach (var g in _groupDescriptions)
                {
                    _view.GroupDescriptions.Add(g);
                }
            }
        }
            
        public CollectionViewShaper<TSource> ClearGrouping()
        {
            _groupDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> ClearSort()
        {
            _sortDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> ClearFilter()
        {
            _filter = null;
            return this;
        }

        public CollectionViewShaper<TSource> ClearAll()
        {
            _filter = null;
            _sortDescriptions.Clear();
            _groupDescriptions.Clear();
            return this;
        }

        public CollectionViewShaper<TSource> Where(Func<TSource, bool> predicate)
        {
            _filter = o => predicate((TSource)o);
            return this;
        }

        public CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, true, ListSortDirection.Ascending);
        }

        public CollectionViewShaper<TSource> OrderByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, true, ListSortDirection.Descending);
        }

        public CollectionViewShaper<TSource> ThenBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, false, ListSortDirection.Ascending);
        }

        public CollectionViewShaper<TSource> ThenByDescending<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            return OrderBy(keySelector, false, ListSortDirection.Descending);
        }

        private CollectionViewShaper<TSource> OrderBy<TKey>(Expression<Func<TSource, TKey>> keySelector, bool clear, ListSortDirection direction)
        {
            string path = GetPropertyPath(keySelector.Body);
            if (clear)
                _sortDescriptions.Clear();
            _sortDescriptions.Add(new SortDescription(path, direction));
            return this;
        }

        public CollectionViewShaper<TSource> GroupBy<TKey>(Expression<Func<TSource, TKey>> keySelector)
        {
            string path = GetPropertyPath(keySelector.Body);
            _groupDescriptions.Add(new PropertyGroupDescription(path));
            return this;
        }

        private static string GetPropertyPath(Expression expression)
        {
            var names = new Stack<string>();
            var expr = expression;
            while (expr != null && !(expr is ParameterExpression) && !(expr is ConstantExpression))
            {
                var memberExpr = expr as MemberExpression;
                if (memberExpr == null)
                    throw new ArgumentException("The selector body must contain only property or field access expressions");
                names.Push(memberExpr.Member.Name);
                expr = memberExpr.Expression;
            }
            return String.Join(".", names.ToArray());
        }
    }

kick it on DotNetKicks.com

Comments

    1. Hi,
      Sorry, but your question is not related to my article in any way… I can”t just answer random questions on SO any time someone asks me to !

  1. The DRY freak in me thinks ClearAll() s/b { return ClearFilter().ClearSort().ClearGrouping(); } but that”s minor and maybe I”m missing something.

    Very cool, elegant, useful code.

  2. Andreas Gullberg Larsen

    Awesome solution. Works like a charm.

  3. this is worth releasing as nuget package, but in that case live shaping should be supported as well. Great work.

    1. Thomas Levesque

      I guess it would be possible to support live shaping as well. IIRC it wasn’t available yet when I published this. Will look into it when I have the time.

  4. Frederico Padilha

    Congrats on the solution, it’s brilliant.
    I have a situation where I need to sort a Listview using a comparison with a foreign dictionary object (which I managed to do using a regular LINQ expression in another element), so my question is if you have any thoughts on ways to implement a custom Comparer or an actual expression parser on the selector body (idk if its even possible). Tks! =)

    1. Thomas Levesque

      Hi Frederico,

      The only way to sort an arbitrary ICollectionView is to use the SortDescriptions, which only lets you specify a property name and order. So my solution won’t let you specify a custom comparer for sorting. However, if the ICollectionView is a ListCollectionView, you can set a custom comparer to the CustomSort property.

      1. Frederico Padilha

        Tks Thomas, I saw some solutions suggesting the use of ListCollectionView using a Comparer with CustomSort. It’s actually doable in my case (it is a ListCollectionView), I will try something along the lines then, thank you so much for the pointer. Also, I’m thinking in implementing the information I need as a property directly, both sub-optimal ways in my view (ListView is a horrible component when it comes to sorting), but your solution has already helped so much in that sense.

      2. Frederico Padilha

        I´ve done it, here’s the quick and dirty solution:

        public class ListCollectionViewShaper
        {
        private readonly ListCollectionView _view;
        private Predicate _filter;
        private readonly List _sortDescriptions = new List();
        private readonly List _groupDescriptions = new List();
        private IComparer _customComparer;

        public ListCollectionViewShaper(ListCollectionView view)
        {
        if (view == null)
        throw new ArgumentNullException(“view”);
        _view = view;
        _filter = view.Filter;
        _sortDescriptions = view.SortDescriptions.ToList();
        _groupDescriptions = view.GroupDescriptions.ToList();
        }

        public void Apply()
        {
        using (_view.DeferRefresh())
        {
        _view.Filter = _filter;
        _view.SortDescriptions.Clear();
        foreach (var s in _sortDescriptions)
        {
        _view.SortDescriptions.Add(s);
        }
        _view.GroupDescriptions.Clear();
        foreach (var g in _groupDescriptions)
        {
        _view.GroupDescriptions.Add(g);
        }

        }

        }
        public void ApplyWithComparer()
        {
        using (_view.DeferRefresh())
        {
        _view.Filter = _filter;
        _view.SortDescriptions.Clear();
        foreach (var s in _sortDescriptions)
        {
        _view.SortDescriptions.Add(s);
        }
        _view.GroupDescriptions.Clear();
        foreach (var g in _groupDescriptions)
        {
        _view.GroupDescriptions.Add(g);
        }
        _view.CustomSort = _customComparer;
        }

        }

        public ListCollectionViewShaper ClearGrouping()
        {
        _groupDescriptions.Clear();
        return this;
        }

        public ListCollectionViewShaper ClearSort()
        {
        _sortDescriptions.Clear();
        return this;
        }

        public ListCollectionViewShaper ClearFilter()
        {
        _filter = null;
        return this;
        }

        public ListCollectionViewShaper ClearAll()
        {
        _filter = null;
        _sortDescriptions.Clear();
        _groupDescriptions.Clear();
        return this;
        }

        public ListCollectionViewShaper Where(Func predicate)
        {
        _filter = o => predicate((TSource)o);
        return this;
        }

        public ListCollectionViewShaper OrderBy(Expression<Func> keySelector)
        {
        return OrderBy(keySelector, true, ListSortDirection.Ascending);
        }

        public ListCollectionViewShaper OrderBy(Expression<Func> keySelector, IComparer Comparer)
        {
        return OrderBy(keySelector, true, ListSortDirection.Ascending, Comparer);
        }

        public ListCollectionViewShaper OrderByDescending(Expression<Func> keySelector)
        {
        return OrderBy(keySelector, true, ListSortDirection.Descending);
        }

        public ListCollectionViewShaper ThenBy(Expression<Func> keySelector)
        {
        return OrderBy(keySelector, false, ListSortDirection.Ascending);
        }

        public ListCollectionViewShaper ThenByDescending(Expression<Func> keySelector)
        {
        return OrderBy(keySelector, false, ListSortDirection.Descending);
        }

        private ListCollectionViewShaper OrderBy(Expression<Func> keySelector, bool clear, ListSortDirection direction)
        {
        string path = GetPropertyPath(keySelector.Body);

        if (clear)
        _sortDescriptions.Clear();
        _sortDescriptions.Add(new SortDescription(path, direction));
        return this;
        }

        private ListCollectionViewShaper OrderBy(Expression<Func> keySelector, bool clear, ListSortDirection direction, IComparer Comparer)
        {
        string path = GetPropertyPath(keySelector.Body);
        _customComparer = Comparer;

        if (clear)
        _sortDescriptions.Clear();
        _sortDescriptions.Add(new SortDescription(path, direction));
        return this;
        }

        public ListCollectionViewShaper GroupBy(Expression<Func> keySelector)
        {
        string path = GetPropertyPath(keySelector.Body);
        _groupDescriptions.Add(new PropertyGroupDescription(path));
        return this;
        }

        private static string GetPropertyPath(System.Linq.Expressions.Expression expression)
        {
        var names = new Stack();
        var expr = expression;
        while (expr != null && !(expr is ParameterExpression) && !(expr is ConstantExpression))
        {
        var memberExpr = expr as MemberExpression;
        if (memberExpr == null)
        throw new ArgumentException(“The selector body must contain only property or field access expressions”);
        names.Push(memberExpr.Member.Name);
        expr = memberExpr.Expression;
        }
        return String.Join(“.”, names.ToArray());
        }

        }

        public static class ListCollectionViewShaper
        {

        public static ListCollectionViewShaper Shape(this ListCollectionView view)
        {
        return new ListCollectionViewShaper(view);
        }
        }

        public class CustomComparer : IComparer
        {

        public CustomComparer()
        {

        }

        public int Compare(object x, object y)
        {
        //YOUR CUSTOM COMPARER GOES HERE

        }
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {

        ListCollectionView dataView = (ListCollectionView)CollectionViewSource.GetDefaultView(lvWindows.ItemsSource);

        ListCollectionViewShaper query = dataView.Shape().OrderBy(x => x, (IComparer)new CustomComparer());
        query.ApplyWithComparer();
        }

      3. Frederico Padilha

        I’ll probably do a generic Apply() instead of having to call ApplyWithComparer(), but it works like a charm! =)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>