Home > OS >  Why the ItemsSource in WPF DataGrid IGNORES GetEnumerator() in my bound collection?
Why the ItemsSource in WPF DataGrid IGNORES GetEnumerator() in my bound collection?

Time:10-06

I try to implement filtering in my own observable collection. My approach is following: I assume the control using ItemsSource should call IEnumerable.GetEnumerator() on my collection to get the items it should render. So I define my own IEnumerable.GetEnumerator() to apply filtering.

Here's relevant code:

public Func<T, bool>? Filter { get; set; }

public void Refresh() {
    OnCollectionChanged(
        new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Reset
        )
    );
}

IEnumerator IEnumerable.GetEnumerator()
    => Filter is null
        ? (IEnumerator)BaseIEnumerableGetEnumerator.Invoke(this, null)!
        : this.Where(Filter).GetEnumerator();

public new IEnumerator<T> GetEnumerator()
    => Filter is null
        ? base.GetEnumerator()
        : this.Where(Filter).GetEnumerator();

private static readonly MethodInfo BaseIEnumerableGetEnumerator
    = BaseType.GetMethod(
        "System.Collections.IEnumerable.GetEnumerator",
        BindingFlags.NonPublic | BindingFlags.Instance
    )!;

BTW, my base class is List<T>. It also implements IList, ICollection, INotifyCollectionChanged and INotifyPropertyChanged.

Now - I set the filter. Nothing happens. So I call Refresh().

And to my surprise also nothing happens. Why? When Reset is sent to the ItemsCollection - the control should reload, and while reloading it should call GetEnumerator().

I set the breakpoint on my GetEnumerator() method, but it is not called on Refresh. WHY?

To clarify - I try to replicate exact ListCollectionView feature. It contains Refresh() method that applies the filtering.

Another weird thing I see is that my new GetEnumerator() is called by my own control, but it is not called AT ALL by DataGrid.

UPDATE:

As I've recently researched - built-in WPF controls might use some undocumented magic to bind items. They can trigger events on view model objects - that is possible (AFAIK) with Reflection.

IDK, using Reflection in the view you could dig into "underlying System Type" and use it's indexer if it's available to get items. In that case - it would just not use GetEnumerator.

I also checked the source code of ListCollectionView: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/windows/Data/ListCollectionView.cs

It just uses a kind of shadow collection to achieve filtering. Well, that one way to achieve the effect for sure. But the easiest, if not the fastest way to filter any collection is to inject the filter into it's enumeration. No objects are created, no allocations, that should be fast. And easy. It works in my own control, that uses foreach on ItemsSource. It's obvious, foreach calls enumerator. There's no way around it. However, Microsoft's control obviously either don't use foreach or... they operate on something different than just the original items collection.

CodePudding user response:

ItemsControl (including DataGrid, Listbox, etc.) works with the collection not directly, but through ICollectionView. When a collection does not provide its own ICollectionView implementation (which is almost always the case), the ItemsControl itself creates the most suitable wrapper for it. Typically, this is a CollectionView and types derived from it. An example of a class that provides its own wrapper is CompositeCollection. It provides a wrapper for the CompositeCollectionView.

The CollectionViewSource is also a means to create an ICollectionView for your collection. Including using the GetDefaultView () method, you can return the default view of your collection. This is what the ItemsControl uses when you pass your collection to it. For almost all collections, a ListCollectionView will be a wrapper. With the resulting wrapper, you can set properties to filter, group, sort, and render the collection view.

If you want to create your own presentation rules for your collection, then you need to implement the ICollectionViewFactory interface. It has only one CreateView() method that returns the View wrapper for your collection. You will have to create your own ICollectionView implementation (it's easier to do this based on the CollectionView or ListCollectionView classes). And then return its instance in the CreateView() method.

CodePudding user response:

OK, DataGrid just doesn't use GetEnumerator to display items. It uses IList.this[index] and ICollection.Count for that.

So to implement filtering on my collection I had to create a ShadowList that contains filtered items.

Then I provided overrides for IList.this[index] and ICollection.Count to return items from the ShadowList.

Then I updated all adding to my collection to also update the ShadowList if the added item matches the Filter.

It works, however there's a catch: the filtered data is accessible only via IList indexer. So if the consuming control uses it - it will get filtered data, if not - it will get the original data.

I think it's preferable here. My view model needs original collection most of the time, and if it's not the case - I can always apply filter adding .Where(collection.Filter) to the enumerator.

GetEnumerator is called A LOT in a complex view model so it's best if it is the original List<T> enumerator.

The completed collection (ObservableList<T>) is available on GitHub: https://github.com/HTD/Woof.Windows/blob/master/Woof.Windows.Mvvm/ObservableList.cs

CodePudding user response:

It just uses a kind of shadow collection to achieve filtering.

For the WPF DataGrid, I believe it uses a backing CollectionViewSource, so filtering is a little bit different (as you say, it has a shadow collection).

MSDN has some info on filtering using the WPF DataGrid that I think might be helpful in your case.

  • Related