Почему ItemsSource в WPF DataGrid ИГНОРИРУЕТ GetEnumerator() в моей связанной коллекции?

#c# #wpf #data-binding

Вопрос:

Я пытаюсь реализовать фильтрацию в своей собственной наблюдаемой коллекции. Мой подход следующий: я предполагаю, что используемый элемент управления ItemsSource должен вызывать IEnumerable.GetEnumerator() мою коллекцию, чтобы получить элементы, которые он должен отображать. Поэтому я определяю свой собственный IEnumerable.GetEnumerator() способ применения фильтрации.

Вот соответствующий код:

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

Кстати, мой базовый класс таков List<T> . Он также реализует IList , ICollection , INotifyCollectionChanged и INotifyPropertyChanged .

Теперь — я установил фильтр. Ничего не происходит. Поэтому я звоню Refresh() .

И, к моему удивлению, тоже ничего не происходит. Почему? Когда Reset отправляется в коллекцию элементов — элемент управления должен перезагрузиться, и во время перезагрузки он должен вызвать GetEnumerator() .

Я установил точку останова в своем GetEnumerator() методе, но она не вызывается Refresh . почему?

Чтобы уточнить — я пытаюсь воспроизвести точную ListCollectionView функцию. Он содержит Refresh() метод, который применяет фильтрацию.

Еще одна странная вещь, которую я вижу, заключается в том, что мое новое GetEnumerator() вызывается моим собственным контролем, но оно вообще не вызывается DataGrid .

Обновить:

Как я недавно исследовал, встроенные элементы управления WPF могут использовать некоторую недокументированную магию для привязки элементов. Они могут вызывать события на объектах модели представления — это возможно (AFAIK) с Reflection помощью .

IDK, используя Reflection в представлении, вы можете изучить «базовый тип системы» и использовать его индексатор, если он доступен для получения элементов. В таком случае — его бы просто не использовали GetEnumerator .

Я также проверил исходный код ListCollectionView : https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/windows/Data/ListCollectionView.cs

Он просто использует своего рода коллекцию теней для достижения фильтрации. Ну, это один из способов достичь эффекта наверняка. Но самый простой, если не самый быстрый способ фильтровать любую коллекцию-это ввести фильтр в ее перечисление. Никакие объекты не создаются, никаких распределений нет, это должно быть быстро. И легко. Это работает под моим собственным контролем, который использует foreach ItemsSource . Это очевидно, foreach звонит перечислитель. Тут ничего не поделаешь. Тем не менее, очевидно, что контроль Microsoft либо не использует foreach , либо не использует… они работают с чем-то иным, чем просто коллекция оригинальных предметов.

Комментарии:

1. Много особых случаев IList , связанных с привязкой данных-интересно, является ли это фактором здесь (праздные домыслы)

2. Хороший выстрел! Я протестировал его — он вызывает IList индексатор для получения элементов! Я еще не знаю, почему, но я расследую это… Кстати, странно, что он вызывает индексатор НЕСКОЛЬКО раз для одного элемента. Похоже, это не очень эффективный подход.

3. совет: если вы храните перечислитель, который вы получаете из . Где(…), и вы отслеживаете последний запрошенный индекс — вы можете сделать что-то вроде «это снова тот же индекс? затем верните итер. Текущий; опережает ли индекс? затем попробуйте использовать iter. MoveNext() , пока мы не окажемся в нужном месте (что обычно означает: следующее), а затем используйте iter. Текущий; отстает ли индексатор? получите полностью новый перечислитель (вы никогда не должны вызывать iter. Сброс(); этот API мертв и непригоден для использования)

4. Еще один хороший, я им воспользуюсь. Кстати, я проверил, он использует индексатор, поэтому я создал теневой список при применении фильтрации, и он просто работает. Моя коллекция ведет себя так же, как ListCollectionView (используя аналогичную технику). Если фильтрация не используется, используется исходный (базовый) список, нет снижения производительности, применяется стоимость теневого списка при фильтрации. Но это верно и для ListCollectionView .

Ответ №1:

ItemsControl (включая сетку данных, список и т.д.) Работает с коллекцией не напрямую, а через ICollectionView. Когда коллекция не предоставляет свою собственную реализацию ICollectionView (что почти всегда имеет место), ItemsControl сам создает для нее наиболее подходящую оболочку. Как правило, это представление коллекции и типы, производные от него. Примером класса, предоставляющего собственную оболочку, является CompositeCollection. Он предоставляет оболочку для CompositeCollectionView.

CollectionViewSource также является средством для создания ICollectionView для вашей коллекции. В том числе с помощью метода GetDefaultView () вы можете вернуть представление вашей коллекции по умолчанию. Это то, что использует ItemsControl, когда вы передаете ему свою коллекцию. Почти для всех коллекций ListCollectionView будет оболочкой. С помощью полученной оболочки вы можете задать свойства для фильтрации, группировки, сортировки и отображения представления коллекции.

Если вы хотите создать свои собственные правила представления для своей коллекции, вам необходимо реализовать интерфейс ICollectionViewFactory. В нем есть только один метод CreateView (), который возвращает оболочку представления для вашей коллекции. Вам придется создать собственную реализацию ICollectionView (это проще сделать на основе классов CollectionView или ListCollectionView). А затем верните его экземпляр в методе CreateView ().

Комментарии:

1. Спасибо, это завершает головоломку. Мне нужно оптимизировать свое представление, которое позволяет выполнять поиск в большой коллекции довольно больших строк. Это довольно медленно при вводе первых 2 букв, поэтому, безусловно, будет быстрее, если он не скопирует элементы 2 раза, что, вероятно, происходит в моем приложении.

2. Я думаю, что вы вряд ли можете придумать что-то лучше, чем использовать CollectionViewSource. Вы пробовали просто установить фильтр для своей коллекции через него? Если коллекция может измениться, то она должна быть наблюдаемой коллекцией.

Ответ №2:

Хорошо, DataGrid просто не используется GetEnumerator для отображения элементов. Он использует IList.this[index] и ICollection.Count для этого.

Поэтому, чтобы реализовать фильтрацию в моей коллекции, мне пришлось создать ShadowList коллекцию, содержащую отфильтрованные элементы.

Затем я предоставил переопределения для IList.this[index] и ICollection.Count для возврата элементов из ShadowList .

Затем я обновил все добавления в свою коллекцию, чтобы также обновить ShadowList , соответствует ли добавленный элемент Filter .

Это работает, однако есть одна загвоздка: отфильтрованные данные доступны только через IList индексатор. Поэтому, если элемент управления — потребитель использует его — он получит отфильтрованные данные, если нет-он получит исходные данные.

Я думаю, что здесь это предпочтительнее. Моя модель представления большую часть времени нуждается в оригинальной коллекции, и если это не так — я всегда могу применить добавление фильтра .Where(collection.Filter) к перечислителю.

GetEnumerator часто вызывается в сложной модели представления, поэтому лучше всего, если это исходный List<T> перечислитель.

Полная коллекция ( ObservableList<T> ) доступна на GitHub: https://github.com/HTD/Woof.Окна/большой двоичный объект/мастер/Гав.Windows.Mvvm/ObservableList.cs

Ответ №3:

Он просто использует своего рода коллекцию теней для достижения фильтрации.

Для сетки данных WPF я считаю, что она использует резервную CollectionViewSource копию , поэтому фильтрация немного отличается (как вы сказали, у нее есть коллекция теней).

В MSDN есть некоторая информация о фильтрации с использованием сетки данных WPF, которая, я думаю, может быть полезна в вашем случае.

Комментарии:

1. Он ItemsSource определен так же IEnumerable , как и для отображения практически любой коллекции, однако он МОЖЕТ использовать CollectionViewSource то, что обеспечивает фильтрацию. Моя цель состояла в том, чтобы воспроизвести некоторые из его действий в моем собственном классе, чтобы реализовать дополнительные функции, такие как заранее установленная емкость, настраиваемый элемент и тому подобное. Мне было интересно, какие именно функции CollectionViewSource использовались, и я понял это.