带缓存的 VirtualizingStackPanel
VirtualizingStackPanel with cache
我们必须要求使用 VirtualizingStackPanel 虚拟化 ListView/ItemsControl。尽管一切都按预期工作,但 Control 的 ItemTemplate 在其初始化阶段坚持使用大量计算的复杂控件 - 这必须在 UI 线程上完成。换句话说,滚动会导致 UI 冻结——如果只需要执行一次就可以了。由于我们不能使用 VirtualizingStackPanel.VirtualizationMode="Recycle"
(由于其他一些限制),我们必须尝试一些不同的东西。
我想到了一个“缓存的”virtualizingStackPanel,它实际上并没有处理 ItemTemplate 的模板,而是 'freezes' 控件。当用户滚动回之前加载的模板时,我们可以简单地 'unfreeze' 控件。
可以通过覆盖OnCleanUpVirtualizedItem
实现'freeze',如:
protected override void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs args)
{
var stuff = FindChild<HeavyStuff>(args.UIElement);
if (stuff != null)
{
int idx = Children.IndexOf(args.UIElement);
if (!_buffer.ContainsKey(idx))
_buffer.Add(idx, args.UIElement);
stuff.Freeze();
args.Handled = true;
args.Cancel = true;
}
else
{
base.OnCleanUpVirtualizedItem(args);
}
}
效果很好。该控件保留在 VisualTree 中,它只是 'freezes' 并避免了任何用户输入和可能产生的工作量。但是,当控件重新出现时,我无法弄清楚如何 'unfreeze' 控件。我仔细研究了 reference-source and found the BringIndexIntoView
,这可能会像下面这样解决我的问题:
protected override void BringIndexIntoView(int index)
{
if (_buffer.ContainsKey(index))
{
FindChild<HeavyStuff>(_buffer[index]).UnFreeze();
}
else
{
base.BringIndexIntoView(index);
}
}
但是,该方法永远不会被内部 VirtualizingStackPanel 逻辑调用。我的第二个想法是覆盖 IItemContainerGenerator
,因为生成器确实按需提供 DependencyObjects
。但又没有任何运气。不能继承ItemContainerGenerator
,因为它是密封的。其次,定义代理并覆盖 ItemContainerGenerator
属性也无济于事,因为基础 class 根本不调用 VirtualizingStackPanel 的 ItemContainerGenerator 属性:
public new IItemContainerGenerator ItemContainerGenerator => generator;
有没有什么方法可以在控件滚动回视图时获取信息,而无需 VirtualizingStackPanel 重新创建实例?
插件:我还考虑过虚拟化数据源本身。然而,即使我们将数据源虚拟化,全局用户输入也会导致控件执行 CPU 和 UI 线程密集型操作。因此,无论我们选择哪种方式,我们都必须 'freeze' 和 'unfreeze' 某些与视口无关的控件。换句话说,我们仍然需要 UI 虚拟化。
编辑:“冻结”和“解冻”不是指 .NET 对象冻结。我用词不当可能会造成这种混乱。对于“冻结”和“解冻”,我确实指的是一些订阅或取消订阅各种事件处理程序的内部逻辑,这样控件,从视口中发出蜂鸣声,不需要处理该输入。
您可以使用以下扩展 StackPanel
的示例实现来跟踪其托管容器的可见性(根据父滚动查看器的视口)。
只需将自定义 Panel
设置为 ItemsPanel
到 ListBox
.
重要的是父级 ScrollViewer
将 CanContentScroll
属性 设置为 true
(这是 ListBox
的默认值)。
由于 StackPanel
已经实现了 IScrollInfo
,因此观察滚动事件和视口非常简单。
添加您的实际实现,以处理更改的容器 and/or 他们的托管模型,到 OnHiddenContainersChanged
方法以完成 Panel
.
public class ScrollWatcherPanel : StackPanel
{
public ScrollWatcherPanel()
{
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (!this.ScrollOwner.CanContentScroll)
{
throw new InvalidOperationException("ScrollViewer.CanContentScroll must be enabled.");
}
this.ScrollOwner.ScrollChanged += OnScrollChanged;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
HandleAllContainers();
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
=> HandleContainerVisibilityChanges((int)e.VerticalChange);
private void HandleAllContainers()
{
int containersBeforeViewportStartCount = (int)this.VerticalOffset;
int containersBeforeViewportEndCount = containersBeforeViewportStartCount + (int)this.ViewportHeight + 1;
var newHiddenContainers = new List<FrameworkElement>();
var newVisibleContainers = new List<FrameworkElement>();
for (int childContainerIndex = 0; childContainerIndex < this.InternalChildren.Count; childContainerIndex++)
{
bool isContainerHiddenBeforeViewport = childContainerIndex < containersBeforeViewportStartCount;
bool isContainerVisibleInViewport = childContainerIndex < containersBeforeViewportEndCount;
var childContainer = (FrameworkElement)this.InternalChildren[childContainerIndex];
if (isContainerHiddenBeforeViewport)
{
newHiddenContainers.Add(childContainer);
}
else if (isContainerVisibleInViewport)
{
newVisibleContainers.Add(childContainer);
}
else // Container is hidden after viewport
{
newHiddenContainers.Add(childContainer);
}
}
OnHiddenContainersChanged(newHiddenContainers, newVisibleContainers);
}
private void HandleContainerVisibilityChanges(int verticalChange)
{
int containersBeforeViewportStartCount = (int)this.VerticalOffset;
int containersBeforeViewportEndCount = containersBeforeViewportStartCount + (int)this.ViewportHeight + 1;
int newHiddenContainerCount = Math.Abs(verticalChange);
int newVisibleContainerCount = Math.Abs(verticalChange);
bool isScrollingDown = verticalChange > 0;
int changeCount = Math.Abs(verticalChange);
var newHiddenContainers = new List<FrameworkElement>();
var newVisibleContainers = new List<FrameworkElement>();
int changesIndex = Math.Max(0, containersBeforeViewportStartCount - changeCount);
for (int childContainerIndex = changesIndex; childContainerIndex < this.InternalChildren.Count; childContainerIndex++)
{
bool isContainerHiddenBeforeViewport = childContainerIndex < containersBeforeViewportStartCount;
bool isContainerVisibleInViewport = childContainerIndex < containersBeforeViewportEndCount;
var childContainer = (FrameworkElement)this.InternalChildren[childContainerIndex];
if (isContainerHiddenBeforeViewport)
{
if (isScrollingDown)
{
bool isContainerNewHidden = childContainerIndex >= containersBeforeViewportStartCount - changeCount
&& newHiddenContainerCount > 0;
if (isContainerNewHidden)
{
newHiddenContainers.Add(childContainer);
newHiddenContainerCount--;
}
}
}
else if (isContainerVisibleInViewport)
{
if (isScrollingDown)
{
bool isContainerNewVisible = childContainerIndex >= containersBeforeViewportEndCount - changeCount
&& newVisibleContainerCount > 0;
if (isContainerNewVisible)
{
newVisibleContainers.Add(childContainer);
newVisibleContainerCount--;
}
}
else
{
bool isContainerNewVisible = childContainerIndex >= containersBeforeViewportStartCount
&& newVisibleContainerCount > 0;
if (isContainerNewVisible)
{
newVisibleContainers.Add(childContainer);
newVisibleContainerCount--;
}
}
}
else // Container is hidden after viewport (on scroll up)
{
if (!isScrollingDown)
{
bool isContainerNewHidden = childContainerIndex >= containersBeforeViewportEndCount
&& newHiddenContainerCount > 0;
if (isContainerNewHidden)
{
newHiddenContainers.Add(childContainer);
newHiddenContainerCount--;
if (newHiddenContainerCount == 0)
{
break;
}
}
}
}
}
OnHiddenContainersChanged(newHiddenContainers, newVisibleContainers);
}
protected virtual void OnHiddenContainersChanged(IEnumerable<FrameworkElement> newHiddenContainers, IEnumerable<FrameworkElement> newVisibleContainers)
{
// TODO::Handle "hidden"/"visible" item containers, that are just scrolled out of/into the viewport.
// You can access the DataContext of the containers to get a reference to the underlying data model.
}
}
我们必须要求使用 VirtualizingStackPanel 虚拟化 ListView/ItemsControl。尽管一切都按预期工作,但 Control 的 ItemTemplate 在其初始化阶段坚持使用大量计算的复杂控件 - 这必须在 UI 线程上完成。换句话说,滚动会导致 UI 冻结——如果只需要执行一次就可以了。由于我们不能使用 VirtualizingStackPanel.VirtualizationMode="Recycle"
(由于其他一些限制),我们必须尝试一些不同的东西。
我想到了一个“缓存的”virtualizingStackPanel,它实际上并没有处理 ItemTemplate 的模板,而是 'freezes' 控件。当用户滚动回之前加载的模板时,我们可以简单地 'unfreeze' 控件。
可以通过覆盖OnCleanUpVirtualizedItem
实现'freeze',如:
protected override void OnCleanUpVirtualizedItem(CleanUpVirtualizedItemEventArgs args)
{
var stuff = FindChild<HeavyStuff>(args.UIElement);
if (stuff != null)
{
int idx = Children.IndexOf(args.UIElement);
if (!_buffer.ContainsKey(idx))
_buffer.Add(idx, args.UIElement);
stuff.Freeze();
args.Handled = true;
args.Cancel = true;
}
else
{
base.OnCleanUpVirtualizedItem(args);
}
}
效果很好。该控件保留在 VisualTree 中,它只是 'freezes' 并避免了任何用户输入和可能产生的工作量。但是,当控件重新出现时,我无法弄清楚如何 'unfreeze' 控件。我仔细研究了 reference-source and found the BringIndexIntoView
,这可能会像下面这样解决我的问题:
protected override void BringIndexIntoView(int index)
{
if (_buffer.ContainsKey(index))
{
FindChild<HeavyStuff>(_buffer[index]).UnFreeze();
}
else
{
base.BringIndexIntoView(index);
}
}
但是,该方法永远不会被内部 VirtualizingStackPanel 逻辑调用。我的第二个想法是覆盖 IItemContainerGenerator
,因为生成器确实按需提供 DependencyObjects
。但又没有任何运气。不能继承ItemContainerGenerator
,因为它是密封的。其次,定义代理并覆盖 ItemContainerGenerator
属性也无济于事,因为基础 class 根本不调用 VirtualizingStackPanel 的 ItemContainerGenerator 属性:
public new IItemContainerGenerator ItemContainerGenerator => generator;
有没有什么方法可以在控件滚动回视图时获取信息,而无需 VirtualizingStackPanel 重新创建实例?
插件:我还考虑过虚拟化数据源本身。然而,即使我们将数据源虚拟化,全局用户输入也会导致控件执行 CPU 和 UI 线程密集型操作。因此,无论我们选择哪种方式,我们都必须 'freeze' 和 'unfreeze' 某些与视口无关的控件。换句话说,我们仍然需要 UI 虚拟化。
编辑:“冻结”和“解冻”不是指 .NET 对象冻结。我用词不当可能会造成这种混乱。对于“冻结”和“解冻”,我确实指的是一些订阅或取消订阅各种事件处理程序的内部逻辑,这样控件,从视口中发出蜂鸣声,不需要处理该输入。
您可以使用以下扩展 StackPanel
的示例实现来跟踪其托管容器的可见性(根据父滚动查看器的视口)。
只需将自定义 Panel
设置为 ItemsPanel
到 ListBox
.
重要的是父级 ScrollViewer
将 CanContentScroll
属性 设置为 true
(这是 ListBox
的默认值)。
由于 StackPanel
已经实现了 IScrollInfo
,因此观察滚动事件和视口非常简单。
添加您的实际实现,以处理更改的容器 and/or 他们的托管模型,到 OnHiddenContainersChanged
方法以完成 Panel
.
public class ScrollWatcherPanel : StackPanel
{
public ScrollWatcherPanel()
{
this.Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
if (!this.ScrollOwner.CanContentScroll)
{
throw new InvalidOperationException("ScrollViewer.CanContentScroll must be enabled.");
}
this.ScrollOwner.ScrollChanged += OnScrollChanged;
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
HandleAllContainers();
}
private void OnScrollChanged(object sender, ScrollChangedEventArgs e)
=> HandleContainerVisibilityChanges((int)e.VerticalChange);
private void HandleAllContainers()
{
int containersBeforeViewportStartCount = (int)this.VerticalOffset;
int containersBeforeViewportEndCount = containersBeforeViewportStartCount + (int)this.ViewportHeight + 1;
var newHiddenContainers = new List<FrameworkElement>();
var newVisibleContainers = new List<FrameworkElement>();
for (int childContainerIndex = 0; childContainerIndex < this.InternalChildren.Count; childContainerIndex++)
{
bool isContainerHiddenBeforeViewport = childContainerIndex < containersBeforeViewportStartCount;
bool isContainerVisibleInViewport = childContainerIndex < containersBeforeViewportEndCount;
var childContainer = (FrameworkElement)this.InternalChildren[childContainerIndex];
if (isContainerHiddenBeforeViewport)
{
newHiddenContainers.Add(childContainer);
}
else if (isContainerVisibleInViewport)
{
newVisibleContainers.Add(childContainer);
}
else // Container is hidden after viewport
{
newHiddenContainers.Add(childContainer);
}
}
OnHiddenContainersChanged(newHiddenContainers, newVisibleContainers);
}
private void HandleContainerVisibilityChanges(int verticalChange)
{
int containersBeforeViewportStartCount = (int)this.VerticalOffset;
int containersBeforeViewportEndCount = containersBeforeViewportStartCount + (int)this.ViewportHeight + 1;
int newHiddenContainerCount = Math.Abs(verticalChange);
int newVisibleContainerCount = Math.Abs(verticalChange);
bool isScrollingDown = verticalChange > 0;
int changeCount = Math.Abs(verticalChange);
var newHiddenContainers = new List<FrameworkElement>();
var newVisibleContainers = new List<FrameworkElement>();
int changesIndex = Math.Max(0, containersBeforeViewportStartCount - changeCount);
for (int childContainerIndex = changesIndex; childContainerIndex < this.InternalChildren.Count; childContainerIndex++)
{
bool isContainerHiddenBeforeViewport = childContainerIndex < containersBeforeViewportStartCount;
bool isContainerVisibleInViewport = childContainerIndex < containersBeforeViewportEndCount;
var childContainer = (FrameworkElement)this.InternalChildren[childContainerIndex];
if (isContainerHiddenBeforeViewport)
{
if (isScrollingDown)
{
bool isContainerNewHidden = childContainerIndex >= containersBeforeViewportStartCount - changeCount
&& newHiddenContainerCount > 0;
if (isContainerNewHidden)
{
newHiddenContainers.Add(childContainer);
newHiddenContainerCount--;
}
}
}
else if (isContainerVisibleInViewport)
{
if (isScrollingDown)
{
bool isContainerNewVisible = childContainerIndex >= containersBeforeViewportEndCount - changeCount
&& newVisibleContainerCount > 0;
if (isContainerNewVisible)
{
newVisibleContainers.Add(childContainer);
newVisibleContainerCount--;
}
}
else
{
bool isContainerNewVisible = childContainerIndex >= containersBeforeViewportStartCount
&& newVisibleContainerCount > 0;
if (isContainerNewVisible)
{
newVisibleContainers.Add(childContainer);
newVisibleContainerCount--;
}
}
}
else // Container is hidden after viewport (on scroll up)
{
if (!isScrollingDown)
{
bool isContainerNewHidden = childContainerIndex >= containersBeforeViewportEndCount
&& newHiddenContainerCount > 0;
if (isContainerNewHidden)
{
newHiddenContainers.Add(childContainer);
newHiddenContainerCount--;
if (newHiddenContainerCount == 0)
{
break;
}
}
}
}
}
OnHiddenContainersChanged(newHiddenContainers, newVisibleContainers);
}
protected virtual void OnHiddenContainersChanged(IEnumerable<FrameworkElement> newHiddenContainers, IEnumerable<FrameworkElement> newVisibleContainers)
{
// TODO::Handle "hidden"/"visible" item containers, that are just scrolled out of/into the viewport.
// You can access the DataContext of the containers to get a reference to the underlying data model.
}
}