Reputation: 732
We have to requirement to virtualize a ListView/ItemsControl with a VirtualizingStackPanel. Although everything works as expected, the Control's ItemTemplate adheres a complex control with a lot of computation during its initialization phase - which has to be done on the UI thread. In other words, scrolling leads to UI freezes - which is fine if it only has to be done once. As we can't use the VirtualizingStackPanel.VirtualizationMode="Recycle"
(due to several other restrictions) we have to try something different.
I thought of a "cached" virtualizingStackPanel which doesn't actually dispose the ItemTemplate's Template, but rather 'freezes' the control. When the user scrolls back to a - previously loaded template - we could simply 'unfreeze' the control.
The 'freeze' can be implemented by overwriting OnCleanUpVirtualizedItem
, such as:
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);
}
}
That works pretty well. The control stays within the VisualTree and it simply 'freezes' and avoids any user-input and the potential resulting workload. However, I couldn't figure out on howto 'unfreeze' the control when it comes back into view. I dug through the reference-source and found the BringIndexIntoView
, which could potentially solve my issue like the following:
protected override void BringIndexIntoView(int index)
{
if (_buffer.ContainsKey(index))
{
FindChild<HeavyStuff>(_buffer[index]).UnFreeze();
}
else
{
base.BringIndexIntoView(index);
}
}
However, that method never gets called by the internal VirtualizingStackPanel logic. My second thought was to override the IItemContainerGenerator
, as the generator does provide the DependencyObjects
on demand. But again without any luck. One can't inherit the ItemContainerGenerator
, because it is sealed. Secondly, defining a proxy and overwriting the ItemContainerGenerator
properties doesn't help either, as the base class doesn't call that VirtualizingStackPanel's ItemContainerGenerator property at all:
public new IItemContainerGenerator ItemContainerGenerator => generator;
Is there any way to obtain the information when a control scrolls back into the view, without the VirtualizingStackPanel re-creating an instance?
Addon: I also thought about virtualizing the data-source itself. However, even if we would virtualize the data source, the global user input would lead the controls to perform CPU and UI-thread intensive operations. Hence, it doesn't matter which way we choose, we do have to 'freeze' and 'unfreeze' certain, non-viewport-related controls. In other words, we need UI virtualization nevertheless.
EDIT: "Freeze" and "Unfreeze" does not refer to the .NET object freezing. My poor choice of words may cause that confusion. With "freeze" and "unfreeze" I do refer to some internal logic which subscribes or unsubscribes from various event handlers, such that controls, beeing out of the viewport, don't require to process that input.
Upvotes: 2
Views: 581
Reputation: 28968
You can use the following example implementation that extends the StackPanel
to tracks the visibility of its hosted containers (in terms of the parent scroll viewer's viewport).
Simply set the custom Panel
as ItemsPanel
to the ListBox
.
It's important that the parent ScrollViewer
has the CanContentScroll
property set to true
(which is the default for the ListBox
).
Since StackPanel
already implements IScrollInfo
, observing the scroll event and viewport is very straight forward.
Add your actual implementation, to handle the changed containers and/or their hosted models, to the OnHiddenContainersChanged
method to complete the 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.
}
}
Upvotes: 1