为什么 WPF DataGrid 中的 ItemsSource 忽略绑定集合中的 GetEnumerator()?
Why the ItemsSource in WPF DataGrid IGNORES GetEnumerator() in my bound collection?
我尝试在我自己的可观察集合中实现过滤。
我的方法如下:
我假设使用 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
)!;
顺便说一句,我的基础 class 是 List<T>
。它还实现了 IList
、ICollection
、INotifyCollectionChanged
和 INotifyPropertyChanged
.
现在 - 我设置了过滤器。
什么都没发生。
所以我打电话给 Refresh()
.
令我惊讶的是什么也没发生。为什么?当 Reset
被发送到 ItemsCollection - 控件应该重新加载,并且在重新加载时它应该调用 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
它只是使用一种影子集合来实现过滤。好吧,那是肯定能达到效果的一种方法。但过滤任何集合的最简单(如果不是最快)方法是将过滤器注入到它的枚举中。没有创建对象,没有分配,应该很快。而且容易。它在我自己的控制下工作,在 ItemsSource
上使用 foreach
。很明显,foreach
调用了枚举器。没有办法解决它。但是,Microsoft 的控件显然要么不使用 foreach
,要么...它们对不同于原始项目集合的内容进行操作。
好的,DataGrid
只是不使用 GetEnumerator
来显示项目。
为此,它使用 IList.this[index]
和 ICollection.Count
。
因此,要在我的 collection 上实施过滤,我必须创建一个包含过滤项目的 ShadowList
。
然后我为 IList.this[index]
和 ICollection.Count
提供覆盖 ShadowList
.
到 return 项目
然后我更新了所有添加到我的 collection 以更新 ShadowList
如果添加的项目与 Filter
相匹配。
它有效,但有一个问题:过滤后的数据只能通过 IList
索引器访问。因此,如果消费控件使用它 - 它会得到过滤后的数据,如果没有 - 它会得到原始数据。
我认为这里更可取。我的视图模型大部分时间都需要原始 collection,如果不是这种情况 - 我总是可以应用过滤器将 .Where(collection.Filter)
添加到枚举器。
GetEnumerator
在复杂的视图模型中被称为很多,所以最好是原始的 List<T>
枚举器。
已完成的 collection (ObservableList<T>
) 在 GitHub 上可用:
https://github.com/HTD/Woof.Windows/blob/master/Woof.Windows.Mvvm/ObservableList.cs
It just uses a kind of shadow collection to achieve filtering.
对于 WPF DataGrid,我相信它使用了一个支持 CollectionViewSource
,所以过滤有点不同(正如你所说,它有一个影子集合)。
MSDN has some info on filtering using the WPF DataGrid 我认为可能对您的情况有所帮助。
ItemsControl(包括 DataGrid、Listbox 等)不是直接与 collection 一起工作,而是通过 ICollectionView。
当 collection 不提供自己的 ICollectionView 实现时(几乎总是如此),ItemsControl 本身会为它创建最合适的包装器。
通常,这是一个 CollectionView 和从它派生的类型。
提供自己的包装器的 class 的一个示例是 CompositeCollection。
它为 CompositeCollectionView 提供了一个包装器。
CollectionViewSource 也是为您的 collection 创建 ICollectionView 的一种方法。
包括使用GetDefaultView()方法,你可以return你的默认视图collection。
这是 ItemsControl 在您将 collection 传递给它时使用的内容。
对于几乎所有 collection,ListCollectionView 将是一个包装器。
使用生成的包装器,您可以设置属性以过滤、分组、排序和呈现 collection 视图。
如果您想为 collection 创建自己的表示规则,则需要实现 ICollectionViewFactory 接口。
它只有一个 CreateView() 方法 return 是您 collection 的视图包装器。
您将必须创建自己的 ICollectionView 实现(基于 CollectionView 或 ListCollectionView classes 更容易做到这一点)。
然后 return 它在 CreateView() 方法中的实例。
我尝试在我自己的可观察集合中实现过滤。
我的方法如下:
我假设使用 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
)!;
顺便说一句,我的基础 class 是 List<T>
。它还实现了 IList
、ICollection
、INotifyCollectionChanged
和 INotifyPropertyChanged
.
现在 - 我设置了过滤器。
什么都没发生。
所以我打电话给 Refresh()
.
令我惊讶的是什么也没发生。为什么?当 Reset
被发送到 ItemsCollection - 控件应该重新加载,并且在重新加载时它应该调用 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
它只是使用一种影子集合来实现过滤。好吧,那是肯定能达到效果的一种方法。但过滤任何集合的最简单(如果不是最快)方法是将过滤器注入到它的枚举中。没有创建对象,没有分配,应该很快。而且容易。它在我自己的控制下工作,在 ItemsSource
上使用 foreach
。很明显,foreach
调用了枚举器。没有办法解决它。但是,Microsoft 的控件显然要么不使用 foreach
,要么...它们对不同于原始项目集合的内容进行操作。
好的,DataGrid
只是不使用 GetEnumerator
来显示项目。
为此,它使用 IList.this[index]
和 ICollection.Count
。
因此,要在我的 collection 上实施过滤,我必须创建一个包含过滤项目的 ShadowList
。
然后我为 IList.this[index]
和 ICollection.Count
提供覆盖 ShadowList
.
然后我更新了所有添加到我的 collection 以更新 ShadowList
如果添加的项目与 Filter
相匹配。
它有效,但有一个问题:过滤后的数据只能通过 IList
索引器访问。因此,如果消费控件使用它 - 它会得到过滤后的数据,如果没有 - 它会得到原始数据。
我认为这里更可取。我的视图模型大部分时间都需要原始 collection,如果不是这种情况 - 我总是可以应用过滤器将 .Where(collection.Filter)
添加到枚举器。
GetEnumerator
在复杂的视图模型中被称为很多,所以最好是原始的 List<T>
枚举器。
已完成的 collection (ObservableList<T>
) 在 GitHub 上可用:
https://github.com/HTD/Woof.Windows/blob/master/Woof.Windows.Mvvm/ObservableList.cs
It just uses a kind of shadow collection to achieve filtering.
对于 WPF DataGrid,我相信它使用了一个支持 CollectionViewSource
,所以过滤有点不同(正如你所说,它有一个影子集合)。
MSDN has some info on filtering using the WPF DataGrid 我认为可能对您的情况有所帮助。
ItemsControl(包括 DataGrid、Listbox 等)不是直接与 collection 一起工作,而是通过 ICollectionView。 当 collection 不提供自己的 ICollectionView 实现时(几乎总是如此),ItemsControl 本身会为它创建最合适的包装器。 通常,这是一个 CollectionView 和从它派生的类型。 提供自己的包装器的 class 的一个示例是 CompositeCollection。 它为 CompositeCollectionView 提供了一个包装器。
CollectionViewSource 也是为您的 collection 创建 ICollectionView 的一种方法。 包括使用GetDefaultView()方法,你可以return你的默认视图collection。 这是 ItemsControl 在您将 collection 传递给它时使用的内容。 对于几乎所有 collection,ListCollectionView 将是一个包装器。 使用生成的包装器,您可以设置属性以过滤、分组、排序和呈现 collection 视图。
如果您想为 collection 创建自己的表示规则,则需要实现 ICollectionViewFactory 接口。 它只有一个 CreateView() 方法 return 是您 collection 的视图包装器。 您将必须创建自己的 ICollectionView 实现(基于 CollectionView 或 ListCollectionView classes 更容易做到这一点)。 然后 return 它在 CreateView() 方法中的实例。